lit-element#customElement TypeScript Examples

The following examples show how to use lit-element#customElement. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: analytics-home.ts    From litelement-website with MIT License 6 votes vote down vote up
@customElement('lit-analytics-home')
export class AnalyticsHome extends LitElement {
  render() {
    return html`
      <h3>Select a period</h3>
      <ul>
        <li><a href="${router.urlForPath(`/analytics/day`)}">Last Day</a></li>
        <li><a href="${router.urlForPath(`/analytics/week`)}">Last Week</a></li>
        <li>
          <a href="${router.urlForPath(`/analytics/month`)}">Last Month</a>
        </li>
        <li><a href="${router.urlForPath(`/analytics/year`)}">Last Year</a></li>
      </ul>
    `;
  }
}
Example #2
Source File: app.ts    From web with MIT License 6 votes vote down vote up
@customElement('my-app')
class MyApp extends LitElement {
  @property({ type: String })
  foo = 'bar';

  render() {
    return html`
      <p>Hello world</p>
    `;
  }
}
Example #3
Source File: checkbox.ts    From medblocks-ui with Apache License 2.0 6 votes vote down vote up
@customElement('mb-checkbox')
export default class MbCheckBox extends EhrElement {
  @property({ type: Boolean }) data: boolean | undefined = undefined;
  @property({ type: Boolean, reflect: true }) disabled: boolean = false;
  @property({ type: Boolean, reflect: true }) required: boolean = false;

  _handleChange(e: CustomEvent) {
    const checkbox = e.target as SlCheckbox;
    this.data = checkbox.checked ? true : false;
    this._mbInput.emit();
  }
  reportValidity() {
  const checked = this.shadowRoot!.querySelector('sl-checkbox') as any;
  return checked.reportValidity();
  }

  render() {
    return html`<sl-checkbox
      ?required=${this.required}
      ?disabled=${this.disabled}
      ?checked=${this.data}
      ?indeterminate=${this.data == null}
      @sl-change=${this._handleChange}
      >${this.label}</sl-checkbox
    >`;
  }
}
Example #4
Source File: blog-posts.ts    From litelement-website with MIT License 6 votes vote down vote up
@customElement('lit-blog-posts')
export class BlogPosts extends LitElement {
  static styles = css`
    h2 {
      margin: 20px;
    }
  `;

  @property({ type: Array }) blogPosts?: Post[];

  constructor() {
    super();
  }

  render() {
    this.loadBlogCard();

    return html`
      <h2>Blog Posts</h2>
      ${this.blogPosts?.map(
        post => html`<blog-card .post="${post}"></blog-card>`
      )}
    `;
  }

  firstUpdated() {
    this.blogPosts = POSTS;
    this.addEventListener('readMore', event => {
      const post = (event as CustomEvent).detail as Post;
      Router.go(`/blog/posts/${post.id}`);
    });
  }

  async loadBlogCard() {
    await import('./blog-card');
  }
}
Example #5
Source File: checkboxAny.ts    From medblocks-ui with Apache License 2.0 6 votes vote down vote up
@customElement('mb-checkbox-any')
export default class MbCheckBox extends EhrElement {
  @property({ type: Object }) data: any = undefined;
  @property({ type: Object }) checked: boolean;
  @property({ type: Object }) bind: any = undefined;
  @property({ type: Boolean, reflect: true }) disabled: boolean = false;
  _handleChange(e: CustomEvent) {
    const checkbox = e.target as SlCheckbox;
    this.data = checkbox.checked ? this.bind : undefined;
  }

  connectedCallback() {
    super.connectedCallback();
    this.data = this.checked ? this.bind : undefined;
  }

  render() {
    return html`<sl-checkbox
      ?disabled=${this.disabled}
      ?checked=${this.checked ? true : false}
      @sl-change=${this._handleChange}
      >${this.label}</sl-checkbox
    >`;
  }
}
Example #6
Source File: analytics.ts    From litelement-website with MIT License 6 votes vote down vote up
@customElement('lit-analytics')
export class Analytics extends LitElement {
  static styles = css`
    .container {
      margin: 20px;
    }
  `;

  render() {
    return html`
      <div class="container">
        <h2>Analytics</h2>
        <slot></slot>
      </div>
    `;
  }
}
Example #7
Source File: context.ts    From medblocks-ui with Apache License 2.0 6 votes vote down vote up
@customElement('mb-context')
export default class MbContext extends EhrElement {
   /** @ignore */
   static styles = css`
   :host {
     display: none;
   }
 `;
  @property({ type: Object })
  data: any;

  @property({ type: Object }) bind: any = undefined;

  @event('mb-input')
  _mbInput: EventEmitter<any>;

  @property({ type: Boolean })
  autocontext: boolean = true;

  connectedCallback() {
    super.connectedCallback();
    setTimeout(() => {
      this._mbInput.emit();
    }, 50);
  }
}
Example #8
Source File: about.ts    From litelement-website with MIT License 6 votes vote down vote up
@customElement('lit-about')
export class About extends LitElement {
  render() {
    return html`
      <h2>About Me</h2>
      <p>
        Suspendisse mollis lobortis lacus, et venenatis nibh sagittis ac. Morbi
        interdum purus diam, vitae pharetra tellus auctor sagittis. Proin
        pellentesque diam a mauris euismod condimentum. Nullam eros ante,
        pretium eget euismod ut, molestie sed nunc. Nullam ut lorem tempus,
        convallis dui ac, congue dolor. Maecenas rutrum magna ac ullamcorper
        fermentum. Nunc porttitor sem at augue ornare, nec interdum ex laoreet.
        Ut vitae mattis urna. In elementum odio a diam iaculis, vel molestie
        diam gravida. Sed in urna nec nibh feugiat fermentum ac vitae dolor. Sed
        porta enim ut orci egestas, vitae gravida mauris scelerisque. Duis
        convallis tincidunt vehicula.
      </p>
    `;
  }
}
Example #9
Source File: submit.ts    From medblocks-ui with Apache License 2.0 6 votes vote down vote up
@customElement('mb-submit')


export default class MbSubmit extends LitElement {
  @event('mb-trigger-submit') submit: EventEmitter<any>;
  handleClick() {
    this.submit.emit()
  }
  connectedCallback() {
    super.connectedCallback()
    this.addEventListener('click', this.handleClick)
  }

  disconnectedCallback() {
    this.removeEventListener('click', this.handleClick)
  }
  render() {
    return html`
      <slot></slot>
    `;
  }
}
Example #10
Source File: opc-back-to-top.ts    From op-components with MIT License 6 votes vote down vote up
@customElement('opc-back-to-top')
export class BackToTop extends LitElement {
  @property({ type: Number, attribute: 'reveal-at' }) revealAt = 0;
  @property({ type: Number, reflect: true }) scrolledY = 0;
  constructor() {
    super();
    window.addEventListener("scroll", () => { this.scrolledY = window.scrollY });
  }

  static get styles() {
    return [style];
  }

  get toggleButton() {
    return this.revealAt < this.scrolledY ? 'show' : 'hide';
  }

  goToTop() {
    window.scrollTo(0, 0);
  }

  render() {
    return html`
    <slot @click="${this.goToTop}" class="${this.toggleButton}">
      <button class="rh-text" tabindex="0">
        <svg xmlns='http://www.w3.org/2000/svg' width='20px' height='20px' viewBox='0 0 512 512'>
          <title>ionicons-v5-a</title>
          <polyline points='112 244 256 100 400 244' style='fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px'/>
            <line x1='256' y1='120' x2='256' y2='412' style='fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:48px'/>
        </svg>
          Go to top
      </button>
   </slot>`;
  }
}
Example #11
Source File: count.ts    From medblocks-ui with Apache License 2.0 5 votes vote down vote up
/**
 * An input element to capture number
 * @inheritdoc
 */
@customElement('mb-count')
export default class MbCount extends EhrElement {
  @property({ type: Number }) data: number;

  @property({ type: String }) label: string = '';

  @property({ type: Boolean, reflect: true }) required: boolean = false;

  @property({ type: Boolean, reflect: true }) disabled: boolean;

  @event('mb-input')
  _mbInput: EventEmitter<number>;

  handleInput(e: CustomEvent) {
    const inputElement = e.target as SlInput;
    this.data = parseFloat(inputElement.value);
    this._mbInput.emit();
  }

  reportValidity() {
    let input;
    input = this.shadowRoot!.querySelector('sl-input') as SlInput;
    return input.reportValidity();
  }

  render() {
    return html`
      <sl-input
        .disabled=${this.disabled}
        type="number"
        ?required=${this.required}
        label=${this.label}
        @sl-input=${this.handleInput}
        value=${this.data || ''}
      ></sl-input>
    `;
  }
}
Example #12
Source File: blog.ts    From litelement-website with MIT License 5 votes vote down vote up
@customElement('lit-blog')
export class Blog extends LitElement {
  render() {
    return html` <slot></slot> `;
  }
}
Example #13
Source File: fhirForm.ts    From medblocks-ui with Apache License 2.0 5 votes vote down vote up
@customElement('mb-fhir-form')
export default class FHIRForm extends Form {
    plugin = FHIRPlugin
}
Example #14
Source File: blog-card.ts    From litelement-website with MIT License 5 votes vote down vote up
@customElement('blog-card')
export class BlogCard extends LitElement {
  static styles = css`
    .blog-card {
      margin: 20px;
      display: flex;
      flex-direction: column;
      margin-bottom: 15px;
      background: white;
      border-radius: 5px;
      overflow: hidden;
      border-radius: 10px;
    }

    .blog-description {
      padding: 20px;
      background: white;
    }

    .blog-footer {
      text-align: right;
    }

    .blog-link {
      color: #008cba;
    }

    h1 {
      margin: 0;
      font-size: 1.5rem;
    }
    h2 {
      font-size: 1rem;
      font-weight: 300;
      color: #5e5e5e;
      margin-top: 5px;
    }
  `;

  // @property({ type: String }) postTitle?: string;
  // @property({ type: String }) author?: string;
  // @property({ type: String }) description?: string;

  @property({ type: Object }) post?: Post;

  render() {
    return html`
      <div class="blog-card">
        <div class="blog-description">
          <h1>${this.post?.title}</h1>
          <h2>${this.post?.author}</h2>
          <p>${this.post?.description}</p>
          <p class="blog-footer">
            <a class="blog-link" @click="${this.handleClick}">Read More</a>
          </p>
        </div>
      </div>
    `;
  }

  public handleClick() {
    this.dispatchEvent(
      new CustomEvent('readMore', { detail: this.post, composed: true })
    );
  }
}
Example #15
Source File: element.ts    From Bundler with MIT License 5 votes vote down vote up
@customElement("my-element")
export class MyElement extends LitElement {
  static styles = unsafeCSS(styles);

  render() {
    return html`<h1>Hello from LitElement!</h1>`;
  }
}
Example #16
Source File: opc-header.ts    From op-components with MIT License 5 votes vote down vote up
@customElement('opc-header-links')
class OPCHeaderLinks extends LitElement {

  // Property Declarations
  @property({ type: Array })
  _links = []

  constructor() {
    super()
  }

  static get styles() {
    return [ style ]
  }

  get opcHeaderLinks() {
    return this._links;
  }

  set opcHeaderLinks(links) {
    if (!links.length) {
      console.warn(`${this.tagName.toLowerCase()}: Array of "links" must be provided. You can do so by using opcHeaderLinks setter function`);
    } else {
      this._links = links;
    }
  }

  render() {
    return html`
        <link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly/patternfly.css" crossorigin="anonymous">
        ${this._links.map(link =>
          html`
              <a class="pf-c-button pf-m-link" href="${link.href}">
                <span class="pf-c-button__icon pf-m-start">
                  <i class="fas ${link.icon}" aria-hidden="true"></i>
                </span>${link.name}
              </a>`)}
      `;
  }
}
Example #17
Source File: pwa-auth.ts    From pwa-auth with MIT License 4 votes vote down vote up
@customElement('pwa-auth')
export class PwaAuthImpl extends LitElement implements PwaAuth {

    @property({ type: String, reflect: true }) appearance: "button" | "list" | "none" = "button";
    @property({ type: String }) signInButtonText = "Sign in";
    @property({ type: String }) microsoftButtonText = "Sign in with Microsoft";
    @property({ type: String }) googleButtonText = "Sign in with Google";
    @property({ type: String }) facebookButtonText = "Sign in with Facebook";
    @property({ type: String }) appleButtonText = "Sign in with Apple";
    @property({ type: String }) appleRedirectUri: string | undefined | null;
    @property({ type: String }) microsoftKey: string | undefined | null;
    @property({ type: String }) googleKey: string | undefined | null;
    @property({ type: String }) facebookKey: string | undefined | null;
    @property({ type: String }) appleKey: string | undefined | null;
    @property({ type: String }) credentialMode: "none" | "silent" | "prompt" = "silent";
    @property({ type: Boolean }) menuOpened = false;
    @property({ type: String, reflect: true }) menuPlacement: "start" | "end" = "start";
    @property({ type: Boolean }) disabled = false;
    @property({ type: String }) iconLoading: "lazy" | "eager" = "lazy";
    @property({ type: Boolean }) requireNewAccessToken = false; // If true, user always goes through OAuth flow to acquire a new access token. If false, user can sign-in using a stored credential with possibly stale access token.

    readonly providers: ProviderInfo[] = [
        {
            name: "Microsoft",
            url: "https://graph.microsoft.com",
            getKey: () => this.microsoftKey,
            getButtonText: () => this.microsoftButtonText,
            getIconUrl: () => this.getMicrosoftIconUrl(),
            import: (key: string) => this.importMicrosoftProvider(key),
            btnClass: "microsoft-btn",
            buttonPartName: "microsoftButton",
            containerPartName: "microsoftContainer",
            iconPartName: "microsoftIcon",
            signIn: () => this.signIn("Microsoft")
        },
        {
            name: "Google",
            url: "https://account.google.com",
            getKey: () => this.googleKey,
            getButtonText: () => this.googleButtonText,
            getIconUrl: () => this.getGoogleIconUrl(),
            import: (key: string) => this.importGoogleProvider(key),
            btnClass: "google-btn",
            buttonPartName: "googleButton",
            containerPartName: "googleContainer",
            iconPartName: "googleIcon",
            signIn: () => this.signIn("Google")
        },
        {
            name: "Facebook",
            url: "https://www.facebook.com",
            getKey: () => this.facebookKey,
            getButtonText: () => this.facebookButtonText,
            getIconUrl: () => this.getFacebookIconUrl(),
            import: (key: string) => this.importFacebookProvider(key),
            btnClass: "facebook-btn",
            buttonPartName: "facebookButton",
            containerPartName: "facebookContainer",
            iconPartName: "facebookIcon",
            signIn: () => this.signIn("Facebook")
        },
        {
            name: "Apple",
            url: "https://appleid.apple.com",
            getKey: () => this.appleKey,
            getButtonText: () => this.appleButtonText,
            getIconUrl: () => this.getAppleIconUrl(),
            import: (key: string) => this.importAppleProvider(key),
            btnClass: "apple-btn",
            buttonPartName: "appleButton",
            containerPartName: "appleContainer",
            iconPartName: "appleIcon",
            signIn: () => this.signIn("Apple")
        },
    ];

    static readonly assetBaseUrl = "https://cdn.jsdelivr.net/npm/@pwabuilder/pwaauth@latest/assets";
    static readonly authTokenLocalStoragePrefix = "pwa-auth-token";

	static styles = css`

		:host {
			display: inline-block;
		}

        button {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
        }

        :host([appearance="list"]) .provider {
            width: 200px;
		}
		:host([appearance="list"]) .provider + .provider {
			margin-top: 10px;
		}

        :host([appearance="list"]) .provider button {
			display: block;
            width: 100%;
            padding: 10px;
            cursor: pointer;
            border-radius: 2px;
            border-width: 0;
            text-align: left;
        }

        :host([appearance="list"]) .provider button img {
            vertical-align: middle;
            margin-right: 10px;
            margin-left: 5px;
        }

        :host([appearance="list"]) .google-btn {
            background-color: white;
            border: 1px solid rgb(192, 192, 192);
        }

        :host([appearance="list"]) .google-btn:hover {
            background-color: rgb(245, 245, 246);
        }

        :host([appearance="list"]) .microsoft-btn {
            color: white;
            background-color: rgb(84, 84, 84);
        }

        :host([appearance="list"]) .microsoft-btn:hover {
            background-color: rgb(47, 51, 55);
        }

        :host([appearance="list"]) .facebook-btn {
            color: white;
            background-color: #385499;
        }

        :host([appearance="list"]) .facebook-btn:hover {
            background-color: #314a86;
        }

        :host([appearance="list"]) .apple-btn {
            background-color: black;
            color: white;
        }

        .signin-btn {
            background-color: rgb(225, 230, 234);
            border: 1px solid rgb(220, 224, 229);
            color: rgb(33, 37, 41);
            border-radius: 4px;
            padding: 12px;
            transition: all 0.15s ease-in-out;
            outline: none;
            cursor: pointer;
        }

            .signin-btn:hover:not(:disabled) {
                background-color: rgb(220, 224, 228);
                border-color: rgb(212, 218, 223);
            }

            .signin-btn:focus {
                background-color: rgb(219, 225, 230);
                border-color: rgb(212, 218, 224);
                box-shadow: rgba(216, 217, 219, 0.1) 0 0 0 3.2px;
            }

            .signin-btn:active {
                background-color: rgb(210, 214, 218);
                border-color: rgb(202, 208, 213);
            }

            .signin-btn:disabled {
                color: rgba(16, 16, 16, 0.3);
            }

        .dropdown {
            position: relative;
            display: inline-block;
        }

        .dropdown .menu {
            position: absolute;
            top: 100%;
            left: 0;
            z-index: 1000;
            display: none;
            float: left;
            min-width: 10rem;
            padding: .5rem 0;
            margin: .125rem 0 0;
            font-size: 1rem;
            background-color: white;
            background-clip: padding-box;
            border: 1px solid rgba(0,0,0,.15);
            border-radius: .25rem;
            cursor: pointer;
        }

        .dropdown .menu.open {
            display: block;
            transform: translate3d(0px, 38px, 0px);
            top: 0;
            left: 0;

            animation-name: dropdown-animation;
            animation-duration: 300ms;
        }

        .dropdown .menu.open.align-end {
            left: auto;
            right: 0;
        }

        .dropdown .menu button {
            background-color: transparent;
            white-space: nowrap;
            border: none;
            outline: none;
            padding: 8px 24px 8px 24px;
            cursor: pointer;
            width: 100%;
            text-align: left;
        }

            .dropdown .menu button:hover {
                background-color: rgb(245, 246, 247);
            }

            .dropdown .menu button:active {
                background-color: rgb(240, 241, 242);
            }

        .dropdown .menu button img {
            vertical-align: middle;
            margin-right: 10px;
        }

        .provider-error {
            background-color: rgb(220, 53, 69);
            color: white;
            padding: 20px;
        }

        @keyframes dropdown-animation {
          from {
            opacity: 0;
          }
          to {
            opacity: 1;
          }
        }

        @media(prefers-reduced-motion: reduce) {
            .dropdown .menu.open {
                animation: none;
            }
        }
    `;

    firstUpdated() {
        // If we're on Safari, we need to load dependencies up front to avoid Safari
        // blocking the first OAuth popup. See https://github.com/pwa-builder/pwa-auth/issues/3
        if (this.isWebKit()) {
            this.disabled = true;
            this.loadAllDependencies()
                .finally(() => this.disabled = false);
        }
    }

    render() {
        if (!this.hasAnyKey) {
            return this.renderNoKeysError();
        }

        if (this.appearance === "list") {
            return this.renderListButtons();
        }

        if (this.appearance === "button") {
            return this.renderLoginButton();
        }

        return super.render();
    }

    /**
     * Starts the sign-in process using the specified provider.
     * @param providerName The name provider to sign-in with. Must be "Microsoft", "Google", "Facebook", or "Apple"
     */
    public signIn(providerName: ProviderName): Promise<SignInResult> {
        const provider = this.providers.find(p => p.name === providerName);
        if (!provider) {
            const errorMessage = "Unable to sign-in because of unsupported provider";
            console.error(errorMessage, providerName);
            return Promise.reject(errorMessage + " " + providerName);
        } 
        
        return this.signInWithProvider(provider)
            .then(result => this.signInCompleted(result));
    }

    private getMicrosoftIconUrl(): string {
        if (this.appearance === "button") {
            return `${PwaAuthImpl.assetBaseUrl}/microsoft-icon-button.svg`;
        }

        return `${PwaAuthImpl.assetBaseUrl}/microsoft-icon-list.svg`;
    }

	private getGoogleIconUrl(): string {
        return `${PwaAuthImpl.assetBaseUrl}/google-icon.svg`;
    }
    
    private getFacebookIconUrl(): string {
        if (this.appearance === "button") {
            return `${PwaAuthImpl.assetBaseUrl}/facebook-icon-button.svg`;
        }

        return `${PwaAuthImpl.assetBaseUrl}/facebook-icon-list.svg`;
    }

	private getAppleIconUrl(): string {
        if (this.appearance === "button") {
            return `${PwaAuthImpl.assetBaseUrl}/apple-icon-button.svg`;
        }

        return `${PwaAuthImpl.assetBaseUrl}/apple-icon-list.svg`;
    }

    private renderLoginButton(): TemplateResult {
        return html`
            <div class="dropdown" @focusout="${this.dropdownFocusOut}">
                <button class="signin-btn" part="signInButton" ?disabled=${this.disabled} @click="${this.signInClicked}">
                    ${this.signInButtonText}
                </button>
                <div class="menu ${this.menuOpened ? "open" : ""} ${this.menuPlacement === "end" ? "align-end" : ""}" part="dropdownMenu">
					${this.renderListButtons()}
                </div>
            </div>
        `;
    }

    private renderListButtons(): TemplateResult {
        return html`
            ${this.providers
                .filter(provider => !!provider.getKey())
                .map(provider => html`
                <div class="provider" part="${provider.containerPartName}">
                    <button class="${provider.btnClass}" ?disabled=${this.disabled} part="${provider.buttonPartName}" @click="${provider.signIn}">
                        <img part="${provider.iconPartName}" loading="${this.iconLoading}" width="20px" height="20px" src="${provider.getIconUrl()}" />
                        ${provider.getButtonText()}
                    </button>
                </div>
            `)}
        `;
    }

    private renderNoKeysError(): TemplateResult {
        return html`<div class="provider-error"><strong>❌ No available sign-ins</strong><br><em>To enable sign-in, pass a Microsoft key, Google key, Facebook, or Apple key to the &lt;pwa-auth&gt; component.</em><br><pre>&lt;pwa-auth microsoftkey="..."&gt;&lt;/pwa-auth&gt;</pre></div>`;
    }

    private dropdownFocusOut(e: FocusEvent) {
        // Close the dropdown if the focus is no longer within it.
        if (this.menuOpened) {
            const dropdown = this.shadowRoot?.querySelector(".dropdown");
            const dropdownContainsFocus = dropdown?.matches(":focus-within");
            if (!dropdownContainsFocus) {
                this.menuOpened = false;
            }
        }
    }

    private get hasAnyKey(): boolean {
        return this.providers.some(p => !!p.getKey());
    }

    private async signInClicked() {
        // Are we configured to use browser credentials (the new CredentialStore API)?
        // If so, go ahead and sign in with whatever stored credential we have.
        if (this.credentialMode === "none") {
            this.toggleMenu();
        } else {
            const signedInCreds = await this.tryAutoSignIn();
            if (!signedInCreds) {
                // There was no stored credential to sign in with. Just show the menu.
                this.toggleMenu();
            }
        }
    }

    private toggleMenu() {
        this.menuOpened = !this.menuOpened;
    }

    private signInWithProvider(provider: ProviderInfo) {
        const key = provider.getKey();
        if (!key) {
            return Promise.reject("No key specified");
        }
        if (this.disabled) {
            return Promise.reject("Sign-in already in progress, rejecting new sign-in attempt");
        }

        this.disabled = true;
        this.menuOpened = false;
        return this.trySignInWithStoredCredential(provider.url)
            .then(storedCredSignInResult => {
                // Did we sign in with a stored credential? Good, we're done.
                if (storedCredSignInResult) {
                    return storedCredSignInResult;
                }

                // Couldn't sign in with stored credential.
                // Kick off the provider-specified OAuth flow.
                return provider.import(key)
                    .then(p => p.signIn())
                    .catch(error => {
                        // If the provider sends back an error, consider that a SignInResult
                        const providerError: SignInResult = {
                            error: error,
                            provider: provider.name
                        };
                        return providerError;
                    })
            })
            .finally(() => this.disabled = false);
    }

    private signInCompleted(signIn: SignInResult): SignInResult {
        this.rehydrateAccessToken(signIn);
        this.dispatchEvent(new CustomEvent("signin-completed", { detail: signIn }));
        this.tryStoreCredential(signIn);
        return signIn;
    }

    private importMicrosoftProvider(key: string): Promise<SignInProvider> {
        return import("./microsoft-provider")
            .then(module => new module.MicrosoftProvider(key));
    }

    private importGoogleProvider(key: string): Promise<SignInProvider> {
        return import("./google-provider")
            .then(module => new module.GoogleProvider(key));
    }

    private importFacebookProvider(key: string): Promise<SignInProvider> {
        return import ("./facebook-provider")
            .then(module => new module.FacebookProvider(key));
    }

    private importAppleProvider(key: string): Promise<SignInProvider> {
        return import ("./apple-provider")
            .then(module => new module.AppleProvider(key, this.appleRedirectUri));
    }

    private tryStoreCredential(signIn: SignInResult) {
        // Use the new Credential Management API to store the credential, allowing for automatic sign-in next time the user visits the page.
        // https://developers.google.com/web/fundamentals/security/credential-management/
        const federatedCredentialCtor = window["FederatedCredential"];
        if (signIn.email && federatedCredentialCtor) {
            try {
                const cred = new federatedCredentialCtor({
                    id: signIn.email,
                    provider: this.providers.find(p => p.name === signIn.provider)?.url || signIn.provider,
                    name: signIn.name || "",
                    iconURL: signIn.imageUrl || ""
                });
                navigator.credentials.store(cred);
            } catch (error) {
                console.error("Unable to store federated credential", error);
            }
        }
    }

    private async tryAutoSignIn(): Promise<FederatedCredential | null> {
        // Use the new Credential Management API to login the user automatically.
        // https://developers.google.com/web/fundamentals/security/credential-management/

        // Bail if we don't support Credential Management
        if (!window["FederatedCredential"]) {
            return null;
        }

        // Bail if we're forcing OAuth flow.
        if (this.requireNewAccessToken) {
            return null;
        }

        let credential: FederatedCredential | null = null;
        if (this.credentialMode === "prompt") {
            // Let the user choose.
            // The browser brings up the native "choose your sign in" dialog.
            credential = await this.getStoredCredential("required", this.providers.map(p => p.url));
        } else if (this.credentialMode === "silent") {
            // Go through the available providers and find one that the user has logged in with.
            for (let i = 0; i < this.providers.length; i++) {
                const provider = this.providers[i];
                credential = await this.getStoredCredential("silent", [provider.url]);
                if (credential) {
                    break;
                }
            }
        }

        if (credential) {
            const loginResult = this.credentialToSignInResult(credential);
            this.signInCompleted(loginResult);
        }

        return credential;
    }

    private trySignInWithStoredCredential(providerUrl: string): Promise<SignInResult | null> {
        return this.getStoredCredential("silent", [providerUrl])
            .catch(error => console.warn("Error attempting to sign-in with stored credential", error))
            .then(credential => credential ? this.credentialToSignInResult(credential) : null);
    }

    private getStoredCredential(mediation: string, providerUrls: string[]): Promise<FederatedCredential | null> {
        // Bail if we don't support Credential Management
        if (!window["FederatedCredential"]) {
            return Promise.resolve(null);
        }

        // Bail if we're not allowed to use stored credential.
        if (this.requireNewAccessToken) {
            return Promise.resolve(null);
        }

        const credOptions: any = {
            mediation: mediation,
            federated: {
                providers: providerUrls
            }
        };

        return navigator.credentials.get(credOptions);
    }

    private credentialToSignInResult(cred: FederatedCredential): SignInResult {
        return {
            name: cred.name,
            email: cred.id,
            providerData: null,
            imageUrl: cred.iconURL,
            error: null,
            provider: this.getProviderNameFromUrl(cred.provider!) as ProviderName
        };
    }

    private getProviderNameFromUrl(url: string): ProviderName {
        const provider = this.providers.find(p => p.url === url);
        if (!provider) {
            console.warn("Unable to find provider matching URL", url);
            return "Microsoft";
        }
        
        return provider.name;
    }

    private isWebKit(): boolean {
        // As of April 2020, Webkit-based browsers wrongfully blocks
        // the OAuth popup due to lazy-loading the auth library(s).
        const isIOS = !!navigator.userAgent.match(/ipad|iphone/i);  // everything is WebKit on iOS
        const isSafari = !!navigator.vendor && navigator.vendor.includes("Apple");
        return isIOS || isSafari;
    }

    private loadAllDependencies(): Promise<any> {
        const dependencyLoadTasks = this.providers
            .filter(p => !!p.getKey())
            .map(p => p.import(p.getKey()!).then(p => p.loadDependencies()));

        return Promise.all(dependencyLoadTasks)
            .catch(error => console.error("Error loading dependencies", error));
    }

    private tryUpdateStoredTokenInfo(signIn: SignInResult) {
        const localStorageKey = this.getAuthTokenLocalStorageKeyName(signIn.provider);
        const storedToken: StoredAccessToken = {
            token: signIn.accessToken || null,
            expiration: signIn.accessTokenExpiration || null,
            providerData: signIn.providerData
        };
        try {
            localStorage.setItem(localStorageKey, JSON.stringify(storedToken));
        } catch (error) {
            console.warn("Unable to store auth token in local storage", localStorageKey, signIn, error);
        }
    }

    private tryReadStoredTokenInfo(providerName: ProviderName): StoredAccessToken | null {
        const localStorageKey = this.getAuthTokenLocalStorageKeyName(providerName);
        try {
            const tokenJson = localStorage.getItem(localStorageKey);
            return tokenJson ? JSON.parse(tokenJson) : null;
        } catch (error) {
            console.warn("Unable to read auth token from local storage", localStorageKey, error);
            return null;
        }
    }

    private getAuthTokenLocalStorageKeyName(providerName: string): string {
        return `${PwaAuthImpl.authTokenLocalStoragePrefix}-${providerName}`;
    }

    private rehydrateAccessToken(signIn: SignInResult) {
        if (signIn.accessToken) {
            // If the user signed in with OAuth flow just now, we already have the auth token.
            // Store it for later.
            this.tryUpdateStoredTokenInfo(signIn);
        } else {
            // We don't have an access token, meaning we signed-in with a stored credential.
            // Thus, we'll fetch it from local storage.
            const tokenInfo = this.tryReadStoredTokenInfo(signIn.provider);
            if (tokenInfo) {
                signIn.accessToken = tokenInfo.token;
                signIn.accessTokenExpiration = tokenInfo.expiration;
                signIn.providerData = tokenInfo.providerData;
            }
        }
    }
}
Example #18
Source File: ha-card-weather-conditions.ts    From ha-card-weather-conditions with MIT License 4 votes vote down vote up
@customElement("ha-card-weather-conditions")
    class HaCardWeatherConditions extends LitElement {
      @property() public hass?: HomeAssistant;
      @property() private _config?: CardConfig;

      private _iconsConfig: IconsConfig = new class implements IconsConfig {
        iconType: string;
        icons_model: string ;
        iconsDay: { [p: string]: string };
        iconsNight: { [p: string]: string };
        path: string ;
      };
      private _terms: ITerms = new class implements ITerms {
        windDirections;
        words;
      };

      private invalidConfig: boolean = false ;
      private numberElements: number = 0 ;

      private _header: boolean = true ;
      private _name: string = '' ;
      private _language: string ;

      private _hasCurrent: boolean = false ;
      private _hasForecast: boolean = false ;
      private _hasMeteogram: boolean = false ;
      private _hasAirQuality: boolean = false ;
      private _hasPollen: boolean = false ;
      private _hasUv: boolean = false ;
      private _hasAlert: boolean = false ;
      private _hasSea: boolean = false ;

      private _displayTop: boolean = true ;
      private _displayCurrent: boolean = true ;
      private _displayForecast: boolean = true ;

      private _classNameSuffix: string ;

      private _showSummary: boolean = true ;
      private _showPresent: boolean = true ;
      private _showUv: boolean = true ;
      private _showAirQuality: boolean = true ;
      private _showPollen: boolean = true ;
      private _showForecast: boolean = true ;
      private _showAlert: boolean = true ;
      private _showSea: boolean = true ;

      /**
       *
       * @param {CardConfig} config
       */
      public setConfig(config: CardConfig) {
        console.log({card_config: config});

        if (!config) {
          this.invalidConfig = true;
          throw new Error("Invalid configuration");
        }

        if (config.name && config.name.length > 0) {
          this._name = config.name;
        }
        if (config.language && config.language.length > 0) {
          this._language = config.language.toLowerCase();
        } else this._language = 'en';

        let transls ;
        try {
          transls = JSON.parse(translations[cwcLocale[this._language]]);
          this._terms.windDirections = transls.cwcLocWindDirections ;
          this._terms.words = transls.cwcTerms ;
          console.info(logo + "%c card \"" + this._name + "\", locale is '" + this._language + "'.",
            optConsoleParam1, optConsoleParam2, optConsoleParam3);
        } catch(e) {
          transls = JSON.parse(translations[cwcLocale['en']]);
          this._terms.windDirections = transls.cwcLocWindDirections ;
          this._terms.words = transls.cwcTerms ;
          console.info(logo + "%c card \"" + this._name + "\" unable to use '" + this._language + "' locale, set as default 'en'.",
            optConsoleParam1, optConsoleParam2, optConsoleParam3);
        }

        numberFormat_0dec = new Intl.NumberFormat(this._language, { maximumFractionDigits: 0 }) ;
        numberFormat_1dec = new Intl.NumberFormat(this._language, { maximumFractionDigits: 1 }) ;

        if (undefined !== config.display) {
          this._displayTop = config.display.findIndex(item => 'top' === item.toLowerCase()) >= 0;
          this._displayCurrent = config.display.findIndex(item => 'current' === item.toLowerCase()) >= 0;
          this._displayForecast = config.display.findIndex(item => 'forecast' === item.toLowerCase()) >= 0;
        }

        this._hasCurrent = (!!config.weather) && (!!config.weather.current);
        this._hasForecast = (!!config.weather) && (!!config.weather.forecast);
        this._hasMeteogram = this._hasForecast && (!!config.weather.forecast.meteogram);
        this._hasAirQuality = !!config.air_quality;
        this._hasPollen = !!config.pollen && (!!config.pollen.tree || !!config.pollen.weed || !!config.pollen.grass);
        this._hasUv = !!config.uv;
        this._hasAlert = !!config.alert;
        this._hasSea = !!config.sea;

        this._iconsConfig.path = hacsImages ? hacsImagePath : manImages ? manImagePath : null;
        // this._iconsConfig.iconType = config.animation ? "animated" : "static";
        this._iconsConfig.iconType = config.animation ? "animated" : "static";
        this._iconsConfig.iconsDay = cwcClimacellDayIcons;
        this._iconsConfig.iconsNight = cwcClimacellNightIcons;
        this._iconsConfig.icons_model = "climacell";
        if ((!!config.weather) && (!!config.weather.icons_model))
          switch (config.weather.icons_model.toLowerCase()) {
            case 'darksky':
              this._iconsConfig.iconsDay = cwcDarkskyDayIcons;
              this._iconsConfig.iconsNight = cwcDarkskyNightIcons;
              this._iconsConfig.icons_model = "darksky";
              break;
            case 'openweathermap':
              this._iconsConfig.iconsDay = cwcOpenWeatherMapDayIcons;
              this._iconsConfig.iconsNight = cwcOpenWeatherMapNightIcons;
              this._iconsConfig.icons_model = "openweathermap";
              break;
            case 'buienradar':
              this._iconsConfig.iconsDay = cwcBuienradarDayIcons;
              this._iconsConfig.iconsNight = cwcBuienradarNightIcons;
              this._iconsConfig.icons_model = "buienradar";
              break;
            case 'defaulthass':
              this._iconsConfig.iconsDay = cwcDefaultHassDayIcons;
              this._iconsConfig.iconsNight = cwcDefaultHassNightIcons;
              this._iconsConfig.icons_model = "defaulthass";
              break;
          }

        this._config = config;
      }

      /**
       * get the current size of the card
       * @return {Number}
       */
      getCardSize() {
        return 1;
      }

      /**
       *
       * @returns {CSSResult}
       */
      static get styles(): CSSResult {
        return css`${style}${styleSummary}${styleForecast}${styleMeter}${styleCamera}${styleNightAndDay}${unsafeCSS(getSeaStyle(globalImagePath))}`;
      }

      /**
       * generates the card HTML
       * @return {TemplateResult}
       */
      render() {
        if (this.invalidConfig) return html`
            <ha-card class="ha-card-weather-conditions">
                <div class='banner'>
                    <div class="header">ha-card-weather-conditions</div>
                </div>
                <div class='content'>
                    Configuration ERROR!
                </div>
            </ha-card>
        `;
        else {
          return this._render();
        }
      }

      /**
       *
       * @returns {TemplateResult}
       * @private
       */
      _render() {
        let sunrise, sunriseEnd, sunsetStart, sunset, now ;
        let dynStyle, condition, habgImage ;

        let _renderedSummary, _renderedPresent, _renderedUv, _renderedAirQuality, _renderedPollen, _renderedForecast,
          _renderedAlert, _renderedSea ;
        // let _renderSummury: boolean = false ;

        let posix:number = 0 ;
        let states = this.hass.states ;

        if( this._showSummary && this._hasCurrent ) {
          let current = this._config.weather.current ;

          if((current.current_conditions && typeof states[ current.current_conditions ] !== undefined)
            || (current.temperature && typeof states[ current.temperature ] !== undefined)) {
            _renderedSummary = renderSummary(this.hass,
              this._config.weather.current, this._config.name, this._iconsConfig, this._terms) ;
            posix++ ;
          } else _renderedSummary = "" ;
        } else _renderedSummary = "" ;

        // Test if render >Present<
        if( this._showPresent && this._hasCurrent) {
          let current = this._config.weather.current ;

          if((current.sun && typeof states[ current.sun ] !== undefined)
            || (current.humidity && typeof states[ current.humidity ] !== undefined)
            || (current.pressure && typeof states[ current.pressure ] !== undefined)
            || (current.visibility && typeof states[ current.visibility ] !== undefined)
            || (current.wind_bearing && typeof states[ current.wind_bearing ] !== undefined)
            || (current.wind_speed && typeof states[ current.wind_speed ] !== undefined)) {

            _renderedPresent = renderPresent(this.hass,
              this._config.weather.current, this._config.weather.forecast, this._language, this._terms, posix > 0) ;
            posix++ ;
          } else {
            if(current.forecast && this._hasForecast) {
              let forecast = this._config.weather.forecast ;

              if((forecast.temperature_low && forecast.temperature_low.day_1 && typeof states[ forecast.temperature_low.day_1 ] !== undefined)
                || (forecast.temperature_high && forecast.temperature_high.day_1 && typeof states[ forecast.temperature_high.day_1 ] !== undefined)
                || (forecast.precipitation_intensity && forecast.precipitation_intensity.day_1 && typeof states[ forecast.precipitation_intensity.day_1 ] !== undefined)
                || (forecast.precipitation_probability && forecast.precipitation_probability.day_1 && typeof states[ forecast.precipitation_probability.day_1 ] !== undefined)) {

                _renderedPresent = renderPresent(this.hass,
                  this._config.weather.current, this._config.weather.forecast, this._language, this._terms, posix > 0) ;
                posix++ ;
              } else _renderedPresent = "" ;
            } else _renderedPresent = "" ;
          }
        } else _renderedPresent = "" ;

        // Test AirQuality
        if(this._showAirQuality && this._hasAirQuality ) {
          let airQuality = this._config.air_quality ;

          if((airQuality.co && typeof states[ airQuality.co ] !== undefined)
            || (airQuality.epa_aqi && typeof states[ airQuality.epa_aqi ] !== undefined)
            || (airQuality.epa_health_concern && typeof states[ airQuality.epa_health_concern ] !== undefined)
            || (airQuality.no2 && typeof states[ airQuality.no2 ] !== undefined)
            || (airQuality.o3 && typeof states[ airQuality.o3 ] !== undefined)
            || (airQuality.pm10 && typeof states[ airQuality.pm10 ] !== undefined)
            || (airQuality.pm25 && typeof states[ airQuality.pm25 ] !== undefined)
            || (airQuality.so2 && typeof states[ airQuality.so2 ] !== undefined)) {

            _renderedAirQuality = renderAirQualities(this.hass, this._config.air_quality, posix > 0) ;
            posix++ ;
          } else _renderedAirQuality = "" ;
        } else _renderedAirQuality = "" ;

        // Test uv
        if(this._showUv && this._hasUv ) {
          let uv = this._config.uv ;

          if((uv.protection_window && typeof states[ uv.protection_window ] !== undefined)
            || (uv.ozone_level && typeof states[ uv.ozone_level ] !== undefined)
            || (uv.uv_index && typeof states[ uv.uv_index ] !== undefined)
            || (uv.uv_level && typeof states[ uv.uv_level ] !== undefined)
            || (uv.max_uv_index && typeof states[ uv.max_uv_index ] !== undefined)) {

            _renderedUv = renderUv(this.hass, this._config.uv, posix > 0) ;
            posix++ ;
          } else _renderedUv = "" ;
        } else _renderedUv = "" ;

        if(this._showPollen && this._hasPollen ) {
          let pollen = this._config.pollen ;

          if((pollen.grass && pollen.grass.entity &&  typeof states[ pollen.grass.entity ] !== undefined)
            || (pollen.tree && pollen.tree.entity &&  typeof states[ pollen.tree.entity ] !== undefined)
            || (pollen.weed && pollen.weed.entity &&  typeof states[ pollen.weed.entity ] !== undefined)) {

            _renderedPollen = renderPollens(this.hass, this._config.pollen, posix > 0) ;
            posix++ ;
          } else _renderedPollen = "" ;
        } else _renderedPollen = "" ;

        if( this._showForecast && this._hasForecast ) {
          let forecast = this._config.weather.forecast ;

          _renderedForecast = renderForecasts(this.hass,
            this._config.weather.current, forecast, this._iconsConfig, this._language, posix > 0) ;
          posix++ ;
        } else _renderedForecast = "" ;

        // Test Alert
        if( this._showAlert && this._hasAlert ) {
          let alert = this._config.alert ;

          _renderedAlert = renderAlert(this.hass, alert, posix > 0) ;
          posix++ ;
        } else _renderedAlert = "" ;

        // Test Sea
        if( this._showSea && this._hasSea ) {
          let sea = this._config.sea ;
          _renderedSea = renderSeaForecast(this.hass, sea, this._iconsConfig, this._language, posix > 0) ;
          posix++ ;
        } else _renderedSea = "" ;

        return html`
      ${dynStyle ? html`
      <style>${dynStyle}</style>` : "" }
      
      <ha-card class="ha-card-weather-conditions ">
        <div class="nd-container ${habgImage ? habgImage : ''}">
        ${this._header ? html`
            ${_renderedSummary}
            ${_renderedAlert}
            ${_renderedPresent}
            ${_renderedUv}
            ${_renderedAirQuality}
            ${_renderedPollen}
            ${_renderedForecast}
            ${_renderedSea}
            ${this._hasMeteogram ? this.renderCamera(this.hass, this._config.weather.forecast.meteogram) : ""}
            ${this._config.camera ? this.renderCamera(this.hass, this._config.camera) : ""}
        ` : html``}
        </div>
      </ha-card>
    `;
      }

      /**
       *
       * @param hass
       * @param camId
       */
      renderCamera(hass: HomeAssistant, camId: string) {
        let camera = hass.states[camId];
        let entity_picture: string = camera ? camera.attributes.entity_picture : undefined ;

        return entity_picture ? html`
        <div @click=${e => this.handlePopup(e, camId)} class="camera-container">
          <div class="camera-image">
            <img src="${entity_picture}" alt="${camera.attributes.friendly_name}"/>
          </div>
        </div>
      ` : html``;
      }

      /**
       *
       * @param e
       * @param entityId
       */
      handlePopup(e, entityId: string) {
        e.stopPropagation();

        let ne = new Event('hass-more-info', {composed: true});
        // @ts-ignore
        ne.detail = {entityId};
        this.dispatchEvent(ne);
      }

    }
Example #19
Source File: hui-grid-card-options.ts    From Custom-Grid-View with MIT License 4 votes vote down vote up
@customElement('hui-grid-card-options')
export class HuiGridCardOptions extends LitElement {
  @property({ attribute: false }) public hass?: HomeAssistant;

  @property({ attribute: false }) public lovelace?;

  @property({ type: Array }) public path?: [number, number];

  @queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;

  public getCardSize(): number | Promise<number> {
    return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1;
  }

  protected render(): TemplateResult {
    return html`
      <slot></slot>
      <div class="parent-card-actions">
        <div class="overlay"></div>
        <div class="card-actions">
          <mwc-icon-button
            .title=${this.hass!.localize('ui.panel.lovelace.editor.edit_card.edit')}
            @click=${this._editCard}
          >
            <ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
          </mwc-icon-button>
          <mwc-icon-button
            .title=${this.hass!.localize('ui.panel.lovelace.editor.edit_card.delete')}
            @click=${this._deleteCard}
          >
            <ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
          </mwc-icon-button>
        </div>
      </div>
    `;
  }

  private _editCard(): void {
    fireEvent(this, 'll-edit-card' as any, { path: this.path });
  }

  private _deleteCard(): void {
    fireEvent(this, 'll-delete-card' as any, { path: this.path });
  }

  static get styles(): CSSResult {
    return css`
      slot {
        pointer-events: none;
        z-index: 0;
      }

      .overlay {
        transition: all 0.25s;
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 1;
        opacity: 0;
        cursor: move;
      }

      .parent-card-actions:hover .overlay {
        outline: 2px solid var(--primary-color);
        background: rgba(0, 0, 0, 0.3);
        /* background-color: grey; */
        opacity: 1;
      }

      .parent-card-actions {
        transition: all 0.25s;
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        opacity: 0;
      }

      .parent-card-actions:hover {
        opacity: 1;
      }

      .card-actions {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        align-items: center;
        z-index: 2;
        position: absolute;
        left: 0;
        right: 0;
        bottom: 24px;
        color: white;
      }

      .card-actions > * {
        margin: 0 4px;
        border-radius: 24px;
        background: rgba(0, 0, 0, 0.7);
      }

      mwc-list-item {
        cursor: pointer;
        white-space: nowrap;
      }

      mwc-list-item.delete-item {
        color: var(--error-color);
      }

      .drag-handle {
        cursor: move;
      }
    `;
  }
}
Example #20
Source File: ha-card-weather-conditions.ts    From ha-card-weather-conditions with MIT License 4 votes vote down vote up
Promise.all(findImagePath).then((testResults) => {
  let hacsImages: boolean, manImages: boolean ;

  hacsImages = hacsImagePathExist = testResults[0] ;
  manImages = manImagePathExist = testResults[1] ;

  globalImagePath = (hacsImages ? hacsImagePath : manImages ? manImagePath : null) ;
  let translPath = globalImagePath + '/../transl/' ;
  let findTranslation = [
    loadJSON(translPath + 'en.json'),
    loadJSON(translPath + 'it.json'),
    loadJSON(translPath + 'nl.json'),
    loadJSON(translPath + 'es.json'),
    loadJSON(translPath + 'de.json'),
    loadJSON(translPath + 'fr.json'),
    loadJSON(translPath + 'sr-latn.json'),
    loadJSON(translPath + 'pt.json'),
    loadJSON(translPath + 'da.json'),
    loadJSON(translPath + 'no-NO.json')
  ] ;

  if( hacsImages ) console.info(logo + "%c use HACS path to retrieve icons.", optConsoleParam1, optConsoleParam2, optConsoleParam3);
  else if ( manImages ) console.info(logo + "%c use www root path to retrieve icons.", optConsoleParam1, optConsoleParam2, optConsoleParam3);
  else console.info(logo + "%c error setting right icons path.", optConsoleParam1, optConsoleParam2, optConsoleParam3);

  Promise.all(findTranslation).then((translations) => {

    @customElement("ha-card-weather-conditions")
    class HaCardWeatherConditions extends LitElement {
      @property() public hass?: HomeAssistant;
      @property() private _config?: CardConfig;

      private _iconsConfig: IconsConfig = new class implements IconsConfig {
        iconType: string;
        icons_model: string ;
        iconsDay: { [p: string]: string };
        iconsNight: { [p: string]: string };
        path: string ;
      };
      private _terms: ITerms = new class implements ITerms {
        windDirections;
        words;
      };

      private invalidConfig: boolean = false ;
      private numberElements: number = 0 ;

      private _header: boolean = true ;
      private _name: string = '' ;
      private _language: string ;

      private _hasCurrent: boolean = false ;
      private _hasForecast: boolean = false ;
      private _hasMeteogram: boolean = false ;
      private _hasAirQuality: boolean = false ;
      private _hasPollen: boolean = false ;
      private _hasUv: boolean = false ;
      private _hasAlert: boolean = false ;
      private _hasSea: boolean = false ;

      private _displayTop: boolean = true ;
      private _displayCurrent: boolean = true ;
      private _displayForecast: boolean = true ;

      private _classNameSuffix: string ;

      private _showSummary: boolean = true ;
      private _showPresent: boolean = true ;
      private _showUv: boolean = true ;
      private _showAirQuality: boolean = true ;
      private _showPollen: boolean = true ;
      private _showForecast: boolean = true ;
      private _showAlert: boolean = true ;
      private _showSea: boolean = true ;

      /**
       *
       * @param {CardConfig} config
       */
      public setConfig(config: CardConfig) {
        console.log({card_config: config});

        if (!config) {
          this.invalidConfig = true;
          throw new Error("Invalid configuration");
        }

        if (config.name && config.name.length > 0) {
          this._name = config.name;
        }
        if (config.language && config.language.length > 0) {
          this._language = config.language.toLowerCase();
        } else this._language = 'en';

        let transls ;
        try {
          transls = JSON.parse(translations[cwcLocale[this._language]]);
          this._terms.windDirections = transls.cwcLocWindDirections ;
          this._terms.words = transls.cwcTerms ;
          console.info(logo + "%c card \"" + this._name + "\", locale is '" + this._language + "'.",
            optConsoleParam1, optConsoleParam2, optConsoleParam3);
        } catch(e) {
          transls = JSON.parse(translations[cwcLocale['en']]);
          this._terms.windDirections = transls.cwcLocWindDirections ;
          this._terms.words = transls.cwcTerms ;
          console.info(logo + "%c card \"" + this._name + "\" unable to use '" + this._language + "' locale, set as default 'en'.",
            optConsoleParam1, optConsoleParam2, optConsoleParam3);
        }

        numberFormat_0dec = new Intl.NumberFormat(this._language, { maximumFractionDigits: 0 }) ;
        numberFormat_1dec = new Intl.NumberFormat(this._language, { maximumFractionDigits: 1 }) ;

        if (undefined !== config.display) {
          this._displayTop = config.display.findIndex(item => 'top' === item.toLowerCase()) >= 0;
          this._displayCurrent = config.display.findIndex(item => 'current' === item.toLowerCase()) >= 0;
          this._displayForecast = config.display.findIndex(item => 'forecast' === item.toLowerCase()) >= 0;
        }

        this._hasCurrent = (!!config.weather) && (!!config.weather.current);
        this._hasForecast = (!!config.weather) && (!!config.weather.forecast);
        this._hasMeteogram = this._hasForecast && (!!config.weather.forecast.meteogram);
        this._hasAirQuality = !!config.air_quality;
        this._hasPollen = !!config.pollen && (!!config.pollen.tree || !!config.pollen.weed || !!config.pollen.grass);
        this._hasUv = !!config.uv;
        this._hasAlert = !!config.alert;
        this._hasSea = !!config.sea;

        this._iconsConfig.path = hacsImages ? hacsImagePath : manImages ? manImagePath : null;
        // this._iconsConfig.iconType = config.animation ? "animated" : "static";
        this._iconsConfig.iconType = config.animation ? "animated" : "static";
        this._iconsConfig.iconsDay = cwcClimacellDayIcons;
        this._iconsConfig.iconsNight = cwcClimacellNightIcons;
        this._iconsConfig.icons_model = "climacell";
        if ((!!config.weather) && (!!config.weather.icons_model))
          switch (config.weather.icons_model.toLowerCase()) {
            case 'darksky':
              this._iconsConfig.iconsDay = cwcDarkskyDayIcons;
              this._iconsConfig.iconsNight = cwcDarkskyNightIcons;
              this._iconsConfig.icons_model = "darksky";
              break;
            case 'openweathermap':
              this._iconsConfig.iconsDay = cwcOpenWeatherMapDayIcons;
              this._iconsConfig.iconsNight = cwcOpenWeatherMapNightIcons;
              this._iconsConfig.icons_model = "openweathermap";
              break;
            case 'buienradar':
              this._iconsConfig.iconsDay = cwcBuienradarDayIcons;
              this._iconsConfig.iconsNight = cwcBuienradarNightIcons;
              this._iconsConfig.icons_model = "buienradar";
              break;
            case 'defaulthass':
              this._iconsConfig.iconsDay = cwcDefaultHassDayIcons;
              this._iconsConfig.iconsNight = cwcDefaultHassNightIcons;
              this._iconsConfig.icons_model = "defaulthass";
              break;
          }

        this._config = config;
      }

      /**
       * get the current size of the card
       * @return {Number}
       */
      getCardSize() {
        return 1;
      }

      /**
       *
       * @returns {CSSResult}
       */
      static get styles(): CSSResult {
        return css`${style}${styleSummary}${styleForecast}${styleMeter}${styleCamera}${styleNightAndDay}${unsafeCSS(getSeaStyle(globalImagePath))}`;
      }

      /**
       * generates the card HTML
       * @return {TemplateResult}
       */
      render() {
        if (this.invalidConfig) return html`
            <ha-card class="ha-card-weather-conditions">
                <div class='banner'>
                    <div class="header">ha-card-weather-conditions</div>
                </div>
                <div class='content'>
                    Configuration ERROR!
                </div>
            </ha-card>
        `;
        else {
          return this._render();
        }
      }

      /**
       *
       * @returns {TemplateResult}
       * @private
       */
      _render() {
        let sunrise, sunriseEnd, sunsetStart, sunset, now ;
        let dynStyle, condition, habgImage ;

        let _renderedSummary, _renderedPresent, _renderedUv, _renderedAirQuality, _renderedPollen, _renderedForecast,
          _renderedAlert, _renderedSea ;
        // let _renderSummury: boolean = false ;

        let posix:number = 0 ;
        let states = this.hass.states ;

        if( this._showSummary && this._hasCurrent ) {
          let current = this._config.weather.current ;

          if((current.current_conditions && typeof states[ current.current_conditions ] !== undefined)
            || (current.temperature && typeof states[ current.temperature ] !== undefined)) {
            _renderedSummary = renderSummary(this.hass,
              this._config.weather.current, this._config.name, this._iconsConfig, this._terms) ;
            posix++ ;
          } else _renderedSummary = "" ;
        } else _renderedSummary = "" ;

        // Test if render >Present<
        if( this._showPresent && this._hasCurrent) {
          let current = this._config.weather.current ;

          if((current.sun && typeof states[ current.sun ] !== undefined)
            || (current.humidity && typeof states[ current.humidity ] !== undefined)
            || (current.pressure && typeof states[ current.pressure ] !== undefined)
            || (current.visibility && typeof states[ current.visibility ] !== undefined)
            || (current.wind_bearing && typeof states[ current.wind_bearing ] !== undefined)
            || (current.wind_speed && typeof states[ current.wind_speed ] !== undefined)) {

            _renderedPresent = renderPresent(this.hass,
              this._config.weather.current, this._config.weather.forecast, this._language, this._terms, posix > 0) ;
            posix++ ;
          } else {
            if(current.forecast && this._hasForecast) {
              let forecast = this._config.weather.forecast ;

              if((forecast.temperature_low && forecast.temperature_low.day_1 && typeof states[ forecast.temperature_low.day_1 ] !== undefined)
                || (forecast.temperature_high && forecast.temperature_high.day_1 && typeof states[ forecast.temperature_high.day_1 ] !== undefined)
                || (forecast.precipitation_intensity && forecast.precipitation_intensity.day_1 && typeof states[ forecast.precipitation_intensity.day_1 ] !== undefined)
                || (forecast.precipitation_probability && forecast.precipitation_probability.day_1 && typeof states[ forecast.precipitation_probability.day_1 ] !== undefined)) {

                _renderedPresent = renderPresent(this.hass,
                  this._config.weather.current, this._config.weather.forecast, this._language, this._terms, posix > 0) ;
                posix++ ;
              } else _renderedPresent = "" ;
            } else _renderedPresent = "" ;
          }
        } else _renderedPresent = "" ;

        // Test AirQuality
        if(this._showAirQuality && this._hasAirQuality ) {
          let airQuality = this._config.air_quality ;

          if((airQuality.co && typeof states[ airQuality.co ] !== undefined)
            || (airQuality.epa_aqi && typeof states[ airQuality.epa_aqi ] !== undefined)
            || (airQuality.epa_health_concern && typeof states[ airQuality.epa_health_concern ] !== undefined)
            || (airQuality.no2 && typeof states[ airQuality.no2 ] !== undefined)
            || (airQuality.o3 && typeof states[ airQuality.o3 ] !== undefined)
            || (airQuality.pm10 && typeof states[ airQuality.pm10 ] !== undefined)
            || (airQuality.pm25 && typeof states[ airQuality.pm25 ] !== undefined)
            || (airQuality.so2 && typeof states[ airQuality.so2 ] !== undefined)) {

            _renderedAirQuality = renderAirQualities(this.hass, this._config.air_quality, posix > 0) ;
            posix++ ;
          } else _renderedAirQuality = "" ;
        } else _renderedAirQuality = "" ;

        // Test uv
        if(this._showUv && this._hasUv ) {
          let uv = this._config.uv ;

          if((uv.protection_window && typeof states[ uv.protection_window ] !== undefined)
            || (uv.ozone_level && typeof states[ uv.ozone_level ] !== undefined)
            || (uv.uv_index && typeof states[ uv.uv_index ] !== undefined)
            || (uv.uv_level && typeof states[ uv.uv_level ] !== undefined)
            || (uv.max_uv_index && typeof states[ uv.max_uv_index ] !== undefined)) {

            _renderedUv = renderUv(this.hass, this._config.uv, posix > 0) ;
            posix++ ;
          } else _renderedUv = "" ;
        } else _renderedUv = "" ;

        if(this._showPollen && this._hasPollen ) {
          let pollen = this._config.pollen ;

          if((pollen.grass && pollen.grass.entity &&  typeof states[ pollen.grass.entity ] !== undefined)
            || (pollen.tree && pollen.tree.entity &&  typeof states[ pollen.tree.entity ] !== undefined)
            || (pollen.weed && pollen.weed.entity &&  typeof states[ pollen.weed.entity ] !== undefined)) {

            _renderedPollen = renderPollens(this.hass, this._config.pollen, posix > 0) ;
            posix++ ;
          } else _renderedPollen = "" ;
        } else _renderedPollen = "" ;

        if( this._showForecast && this._hasForecast ) {
          let forecast = this._config.weather.forecast ;

          _renderedForecast = renderForecasts(this.hass,
            this._config.weather.current, forecast, this._iconsConfig, this._language, posix > 0) ;
          posix++ ;
        } else _renderedForecast = "" ;

        // Test Alert
        if( this._showAlert && this._hasAlert ) {
          let alert = this._config.alert ;

          _renderedAlert = renderAlert(this.hass, alert, posix > 0) ;
          posix++ ;
        } else _renderedAlert = "" ;

        // Test Sea
        if( this._showSea && this._hasSea ) {
          let sea = this._config.sea ;
          _renderedSea = renderSeaForecast(this.hass, sea, this._iconsConfig, this._language, posix > 0) ;
          posix++ ;
        } else _renderedSea = "" ;

        return html`
      ${dynStyle ? html`
      <style>${dynStyle}</style>` : "" }
      
      <ha-card class="ha-card-weather-conditions ">
        <div class="nd-container ${habgImage ? habgImage : ''}">
        ${this._header ? html`
            ${_renderedSummary}
            ${_renderedAlert}
            ${_renderedPresent}
            ${_renderedUv}
            ${_renderedAirQuality}
            ${_renderedPollen}
            ${_renderedForecast}
            ${_renderedSea}
            ${this._hasMeteogram ? this.renderCamera(this.hass, this._config.weather.forecast.meteogram) : ""}
            ${this._config.camera ? this.renderCamera(this.hass, this._config.camera) : ""}
        ` : html``}
        </div>
      </ha-card>
    `;
      }

      /**
       *
       * @param hass
       * @param camId
       */
      renderCamera(hass: HomeAssistant, camId: string) {
        let camera = hass.states[camId];
        let entity_picture: string = camera ? camera.attributes.entity_picture : undefined ;

        return entity_picture ? html`
        <div @click=${e => this.handlePopup(e, camId)} class="camera-container">
          <div class="camera-image">
            <img src="${entity_picture}" alt="${camera.attributes.friendly_name}"/>
          </div>
        </div>
      ` : html``;
      }

      /**
       *
       * @param e
       * @param entityId
       */
      handlePopup(e, entityId: string) {
        e.stopPropagation();

        let ne = new Event('hass-more-info', {composed: true});
        // @ts-ignore
        ne.detail = {entityId};
        this.dispatchEvent(ne);
      }

    }
  }) ;
}) ;
Example #21
Source File: editor.ts    From harmony-card with MIT License 4 votes vote down vote up
@customElement('harmony-card-editor')
export class HarmonyCardEditor extends LitElement implements LovelaceCardEditor {
    @property() public hass?: HomeAssistant;
    @property() private _config?: HarmonyCardConfig;
    @property() private _toggle?: boolean;

    public setConfig(config: HarmonyCardConfig): void {
        this._config = config;
    }

    get _name(): string {
        if (this._config) {
            return this._config.name || '';
        }

        return '';
    }

    get _entity(): string {
        if (this._config) {
            return this._config.entity || '';
        }

        return '';
    }

    get _volume_entity(): string {
        if (this._config) {
            return this._config.volume_entity || '';
        }

        return '';
    }

    get _show_warning(): boolean {
        if (this._config) {
            return this._config.show_warning || false;
        }

        return false;
    }

    get _show_error(): boolean {
        if (this._config) {
            return this._config.show_error || false;
        }

        return false;
    }

    get _tap_action(): ActionConfig {
        if (this._config) {
            return this._config.tap_action || { action: 'more-info' };
        }

        return { action: 'more-info' };
    }

    get _hold_action(): ActionConfig {
        if (this._config) {
            return this._config.hold_action || { action: 'none' };
        }

        return { action: 'none' };
    }

    get _double_tap_action(): ActionConfig {
        if (this._config) {
            return this._config.double_tap_action || { action: 'none' };
        }

        return { action: 'none' };
    }

    protected render(): TemplateResult | void {
        if (!this.hass) {
            return html``;
        }

        // You can restrict on domain type
        const entities = Object.keys(this.hass.states).filter(eid => eid.substr(0, eid.indexOf('.')) === 'remote');

        const volume_entities = Object.keys(this.hass.states).filter(eid => eid.substr(0, eid.indexOf('.')) === 'media_player');

        return html`
      <div class="card-config">
        <div class="option" @click=${this._toggleOption} .option=${'required'}>
          <div class="row">
            <ha-icon .icon=${`mdi:${options.required.icon}`}></ha-icon>
            <div class="title">${options.required.name}</div>
          </div>
          <div class="secondary">${options.required.secondary}</div>
        </div>
        ${options.required.show
                ? html`
              <div class="values">
                <paper-dropdown-menu
                  label="Harmony Entity (Required)"
                  @value-changed=${this._valueChanged}
                  .configValue=${'entity'}
                >
                  <paper-listbox slot="dropdown-content" .selected=${entities.indexOf(this._entity)}>
                    ${entities.map(entity => {
                    return html`
                        <paper-item>${entity}</paper-item>
                      `;
                })}
                  </paper-listbox>
                </paper-dropdown-menu>
              </div>
              <div class="values">
                <paper-dropdown-menu
                  label="Volume Entity"
                  @value-changed=${this._valueChanged}
                  .configValue=${'volume_entity'}
                >
                  <paper-listbox slot="dropdown-content" .selected=${volume_entities.indexOf(this._volume_entity)}>
                    ${volume_entities.map(entity => {
                    return html`
                        <paper-item>${entity}</paper-item>
                      `;
                })}
                  </paper-listbox>
                </paper-dropdown-menu>
              </div>
            `
                : ''}
        <div class="option" @click=${this._toggleOption} .option=${'actions'}>
          <div class="row">
            <ha-icon .icon=${`mdi:${options.actions.icon}`}></ha-icon>
            <div class="title">${options.actions.name}</div>
          </div>
          <div class="secondary">${options.actions.secondary}</div>
        </div>
        ${options.actions.show
                ? html`
              <div class="values">
                <div class="option" @click=${this._toggleAction} .option=${'tap'}>
                  <div class="row">
                    <ha-icon .icon=${`mdi:${options.actions.options.tap.icon}`}></ha-icon>
                    <div class="title">${options.actions.options.tap.name}</div>
                  </div>
                  <div class="secondary">${options.actions.options.tap.secondary}</div>
                </div>
                ${options.actions.options.tap.show
                        ? html`
                      <div class="values">
                        <paper-item>Action Editors Coming Soon</paper-item>
                      </div>
                    `
                        : ''}
                <div class="option" @click=${this._toggleAction} .option=${'hold'}>
                  <div class="row">
                    <ha-icon .icon=${`mdi:${options.actions.options.hold.icon}`}></ha-icon>
                    <div class="title">${options.actions.options.hold.name}</div>
                  </div>
                  <div class="secondary">${options.actions.options.hold.secondary}</div>
                </div>
                ${options.actions.options.hold.show
                        ? html`
                      <div class="values">
                        <paper-item>Action Editors Coming Soon</paper-item>
                      </div>
                    `
                        : ''}
                <div class="option" @click=${this._toggleAction} .option=${'double_tap'}>
                  <div class="row">
                    <ha-icon .icon=${`mdi:${options.actions.options.double_tap.icon}`}></ha-icon>
                    <div class="title">${options.actions.options.double_tap.name}</div>
                  </div>
                  <div class="secondary">${options.actions.options.double_tap.secondary}</div>
                </div>
                ${options.actions.options.double_tap.show
                        ? html`
                      <div class="values">
                        <paper-item>Action Editors Coming Soon</paper-item>
                      </div>
                    `
                        : ''}
              </div>
            `
                : ''}
        <div class="option" @click=${this._toggleOption} .option=${'appearance'}>
          <div class="row">
            <ha-icon .icon=${`mdi:${options.appearance.icon}`}></ha-icon>
            <div class="title">${options.appearance.name}</div>
          </div>
          <div class="secondary">${options.appearance.secondary}</div>
        </div>
        ${options.appearance.show
                ? html`
              <div class="values">
                <paper-input
                  label="Name (Optional)"
                  .value=${this._name}
                  .configValue=${'name'}
                  @value-changed=${this._valueChanged}
                ></paper-input>
                <br />
                <ha-switch
                  aria-label=${`Toggle warning ${this._show_warning ? 'off' : 'on'}`}
                  .checked=${this._show_warning !== false}
                  .configValue=${'show_warning'}
                  @change=${this._valueChanged}
                  >Show Warning?</ha-switch
                >
                <ha-switch
                  aria-label=${`Toggle error ${this._show_error ? 'off' : 'on'}`}
                  .checked=${this._show_error !== false}
                  .configValue=${'show_error'}
                  @change=${this._valueChanged}
                  >Show Error?</ha-switch
                >
              </div>
            `
                : ''}
      </div>
    `;
    }

    private _toggleAction(ev): void {
        this._toggleThing(ev, options.actions.options);
    }

    private _toggleOption(ev): void {
        this._toggleThing(ev, options);
    }

    private _toggleThing(ev, optionList): void {
        const show = !optionList[ev.target.option].show;
        for (const [key] of Object.entries(optionList)) {
            optionList[key].show = false;
        }
        optionList[ev.target.option].show = show;
        this._toggle = !this._toggle;
    }

    private _valueChanged(ev): void {
        if (!this._config || !this.hass) {
            return;
        }
        const target = ev.target;
        if (this[`_${target.configValue}`] === target.value) {
            return;
        }
        if (target.configValue) {
            if (target.value === '') {
                delete this._config[target.configValue];
            } else {
                this._config = {
                    ...this._config,
                    [target.configValue]: target.checked !== undefined ? target.checked : target.value,
                };
            }
        }
        fireEvent(this, 'config-changed', { config: this._config });
    }

    static get styles(): CSSResult {
        return css`
      .option {
        padding: 4px 0px;
        cursor: pointer;
      }
      .row {
        display: flex;
        margin-bottom: -14px;
        pointer-events: none;
      }
      .title {
        padding-left: 16px;
        margin-top: -6px;
        pointer-events: none;
      }
      .secondary {
        padding-left: 40px;
        color: var(--secondary-text-color);
        pointer-events: none;
      }
      .values {
        padding-left: 16px;
        background: var(--secondary-background-color);
      }
      ha-switch {
        padding-bottom: 8px;
      }
    `;
    }
}
Example #22
Source File: input-multiple.ts    From medblocks-ui with Apache License 2.0 4 votes vote down vote up
@customElement('mb-input-multiple')
export default class MbInputMultiple extends EhrElement {
  /** @ignore */
  static styles = css`
    :host {
      display: flex;
      flex-direction: column;
    }
    sl-tag {
      margin: var(--sl-spacing-x-small) var(--sl-spacing-xx-small) 0 0;
    }
    sl-icon {
      font-size: var(--sl-font-size-large);
      cursor: pointer;
    }
  `;
  @property({ type: Array }) data: string[] = [];

  @property({ type: Boolean }) multiple: boolean = true;

  @property({ type: String, reflect: true }) placeholder: string = '';

  @property({ type: Boolean, reflect: true }) required: boolean = false;

  @state() value: string = '';

  handleClear(tagIndex: number) {
    this.data = this.data.filter((_, i) => i !== tagIndex);
    this._mbInput.emit();
  }

  handleInput(e: CustomEvent) {
    const target = e.target as SlInput;
    this.value = target.value;
  }

  addValue() {
    if (this.value !== '') {
      this.data = [...this.data, this.value];
      this.value = '';
      this._mbInput.emit();
    }
  }

  connectedCallback() {
    super.connectedCallback();
    this.addEventListener('keypress', event => {
      if (event.key === 'Enter') {
        this.addValue();
      }
    });
  }

  reportValidity() {
    const input = this.shadowRoot!.querySelector('sl-input') as SlInput;
    if(this.data.length>0){
      return true
    }
    return input.reportValidity();
  }

  render() {
    return html`
      <sl-input
        ?required=${this.required }
        help-text=${`Press enter to add ${this.placeholder}`}
        @sl-input=${this.handleInput}
        label=${this.label || ''}
        .value=${this.value}
        @sl-blur=${() => this.addValue()}
      >
        ${this.value &&
        html`<sl-icon @click=${this.addValue} library="medblocks" name="arrow-right-circle" slot="suffix"></sl-icon>
                </sl-icon>`}
      </sl-input>
      <div>
        ${this.data.map(
          (s, i) =>
            html`<sl-tag
              size="medium"
              @sl-clear=${() => this.handleClear(i)}
              clearable
              >${s}</sl-tag
            >`
        )}
      </div>
    `;
  }
}
Example #23
Source File: grid-view.ts    From Custom-Grid-View with MIT License 4 votes vote down vote up
@customElement('grid-dnd')
export class GridView extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @property({ attribute: false }) public lovelace?: any;

  @property({ type: Number }) public index?: number;

  @property({ attribute: false }) public cards: Array<LovelaceGridCard> = [];

  @property({ attribute: false }) public badges: any[] = [];

  @internalProperty() private _columns?: number;

  @internalProperty() private _layout?: Array<{
    width: number;
    height: number;
    posX: number;
    posY: number;
    key: string;
  }>;

  @internalProperty() public _cards: {
    [key: string]: LovelaceCard | any;
  } = {};

  private _config?: LovelaceViewConfig;

  private _layoutEdit?: Array<{
    width: number;
    height: number;
    posX: number;
    posY: number;
    key: string;
  }>;

  private _createColumnsIteration = 0;

  private _mqls?: MediaQueryList[];

  public constructor() {
    super();
    this.addEventListener('iron-resize', (ev: Event) => ev.stopPropagation());
  }

  public setConfig(config: LovelaceViewConfig): void {
    this._config = config;
  }

  protected render(): TemplateResult {
    return html`
      ${this.lovelace.editMode
        ? html`
            <div class="toolbar">
              <mwc-button @click=${this._saveView} raised>Save Layout</mwc-button>
            </div>
          `
        : ''}
      <div id="badges" style=${this.badges.length > 0 ? 'display: block' : 'display: none'}>
        ${this.badges.map(
          badge =>
            html`
              ${badge}
            `,
        )}
      </div>
      <lit-grid-layout
        rowHeight="40"
        .containerPadding=${[8, 8]}
        .margin=${[8, 8]}
        .resizeHandle=${RESIZE_HANDLE}
        .itemRenderer=${this._itemRenderer}
        .layout=${this._layout}
        .columns=${this._columns}
        .dragHandle=${'.overlay'}
        .dragDisabled=${!this.lovelace?.editMode}
        .resizeDisabled=${!this.lovelace?.editMode}
        @item-changed=${this._saveLayout}
      ></lit-grid-layout>
      ${this.lovelace?.editMode
        ? html`
            <mwc-fab
              class=${classMap({
                rtl: computeRTL(this.hass!),
              })}
              .title=${this.hass!.localize('ui.panel.lovelace.editor.edit_card.add')}
              @click=${this._addCard}
            >
              <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
            </mwc-fab>
          `
        : ''}
    `;
  }

  protected firstUpdated(): void {
    this._updateColumns = this._updateColumns.bind(this);
    this._mqls = [300, 600, 900, 1200].map(width => {
      const mql = matchMedia(`(min-width: ${width}px)`);
      mql.addEventListener('change', this._updateColumns);
      return mql;
    });
    this._updateCardsWithID();
    this._updateColumns();
  }

  protected updated(changedProperties: PropertyValues): void {
    super.updated(changedProperties);

    if (changedProperties.has('hass')) {
      const oldHass = changedProperties.get('hass') as HomeAssistant;

      if ((oldHass && this.hass!.dockedSidebar !== oldHass.dockedSidebar) || (!oldHass && this.hass)) {
        this._updateColumns();
      }

      if (changedProperties.size === 1) {
        return;
      }
    }

    const oldLovelace = changedProperties.get('lovelace') as any | undefined;

    if (
      (changedProperties.has('lovelace') &&
        (oldLovelace?.config !== this.lovelace?.config || oldLovelace?.editMode !== this.lovelace?.editMode)) ||
      changedProperties.has('_columns')
    ) {
      if (!this._layout?.length) {
        this._createLayout();
        return;
      }

      this._createCards();
    }

    if (changedProperties.has('lovelace') && this.lovelace.editMode && !oldLovelace.editMode) {
      this._layoutEdit = this._layout;
    }

    if (changedProperties.has('lovelace') && !this.lovelace.editMode && oldLovelace.editMode) {
      this._layout = (this._config as any).layout;
    }
  }

  private _updateCardsWithID(): void {
    if (!this._config) {
      return;
    }

    if (this._config.cards!.filter(card => !card.layout?.key).length === 0) {
      return;
    }

    const cards = this._config.cards!.map(card => {
      if (card.layout?.key) {
        return card;
      }
      card = { ...card, layout: { key: card.layout?.key || uuidv4() } };
      return card;
    });

    const newConfig = { ...this._config, cards };

    this.lovelace.saveConfig(replaceView(this.lovelace!.config, this.index!, newConfig));
  }

  private async _createLayout(): Promise<void> {
    this._createColumnsIteration++;
    const iteration = this._createColumnsIteration;

    if (this._layout?.length) {
      return;
    }

    const newLayout: Array<{
      width: number;
      height: number;
      posX: number;
      posY: number;
      key: string;
      minHeight: number;
    }> = [];

    let tillNextRender: Promise<unknown> | undefined;
    let start: Date | undefined;

    // Calculate the size of every card and determine in what column it should go
    for (const [index, card] of this.cards.entries()) {
      const cardConfig = this._config!.cards![index];

      const currentLayout = (this._config as any).layout?.find(item => item.key === cardConfig.layout?.key);

      if (currentLayout) {
        newLayout.push(currentLayout);
        continue;
      }

      console.log('not in current layout: ', cardConfig);

      if (tillNextRender === undefined) {
        // eslint-disable-next-line no-loop-func
        tillNextRender = nextRender().then(() => {
          tillNextRender = undefined;
          start = undefined;
        });
      }

      let waitProm: Promise<unknown> | undefined;

      // We should work for max 16ms (60fps) before allowing a frame to render
      if (start === undefined) {
        // Save the time we start for this frame, no need to wait yet
        start = new Date();
      } else if (new Date().getTime() - start.getTime() > 16) {
        // We are working too long, we will prevent a render, wait to allow for a render
        waitProm = tillNextRender;
      }

      const cardSizeProm = computeCardSize(card);
      // @ts-ignore
      // eslint-disable-next-line no-await-in-loop
      const [cardSize] = await Promise.all([cardSizeProm, waitProm]);

      if (iteration !== this._createColumnsIteration) {
        // An other create columns is started, abort this one
        return;
      }

      const computedLayout = {
        width: 3,
        height: cardSize,
        key: cardConfig.layout?.key,
      };

      newLayout.push({
        ...computedLayout,
        ...currentLayout,
      });
    }

    this._layout = newLayout;
    this._createCards();
  }

  private _createCards(): void {
    const elements = {};

    this.cards.forEach((card: LovelaceGridCard, index) => {
      const cardLayout = this._layout![index];

      if (!cardLayout) {
        return;
      }

      card.editMode = this.lovelace?.editMode;
      let element = card;

      if (this.lovelace?.editMode) {
        const wrapper = document.createElement('hui-grid-card-options') as LovelaceGridCard;
        wrapper.hass = this.hass;
        wrapper.lovelace = this.lovelace;
        wrapper.path = [this.index!, index];
        wrapper.appendChild(card);
        element = wrapper;
      }

      elements[cardLayout.key] = element;
    });

    this._cards = elements;
  }

  private _saveLayout(ev: CustomEvent): void {
    this._layoutEdit = ev.detail.layout;
  }

  private async _saveView(): Promise<void> {
    const viewConf: any = {
      ...this._config,
      layout: this._layoutEdit,
    };

    await this.lovelace?.saveConfig(replaceView(this.lovelace!.config, this.index!, viewConf));
  }

  private _itemRenderer = (key: string): TemplateResult => {
    if (!this._cards) {
      return html``;
    }

    return html`
      ${this._cards[key]}
    `;
  };

  private _addCard(): void {
    fireEvent(this, 'll-create-card' as any);
  }

  private _updateColumns(): void {
    if (!this._mqls) {
      return;
    }
    const matchColumns = this._mqls!.reduce((cols, mql) => cols + Number(mql.matches), 0);
    // Do -1 column if the menu is docked and open
    this._columns = Math.max(1, mediaQueryColumns[matchColumns - 1]);
  }

  static get styles(): CSSResult {
    return css`
      :host {
        display: block;
        box-sizing: border-box;
        padding: 4px 4px env(safe-area-inset-bottom);
        transform: translateZ(0);
        position: relative;
        color: var(--primary-text-color);
        background: var(--lovelace-background, var(--primary-background-color));
      }

      lit-grid-layout {
        --placeholder-background-color: var(--accent-color);
        --resize-handle-size: 32px;
      }

      #badges {
        margin: 8px 16px;
        font-size: 85%;
        text-align: center;
      }

      mwc-fab {
        position: sticky;
        float: right;
        right: calc(16px + env(safe-area-inset-right));
        bottom: calc(16px + env(safe-area-inset-bottom));
        z-index: 5;
      }

      mwc-fab.rtl {
        float: left;
        right: auto;
        left: calc(16px + env(safe-area-inset-left));
      }

      .toolbar {
        background-color: var(--divider-color);
        border-bottom-left-radius: var(--ha-card-border-radius, 4px);
        border-bottom-right-radius: var(--ha-card-border-radius, 4px);
        padding: 8px;
      }
    `;
  }
}
Example #24
Source File: quantity.ts    From medblocks-ui with Apache License 2.0 4 votes vote down vote up
/**
 * @inheritdoc
 * Quantity element with an input and select for units.
 */
@customElement('mb-quantity')
export default class MbQuantity extends QuantityElement {
  /** @ignore */
  static styles = css`
    :host {
      display: flex;
      flex: 1;
      align-items: flex-end;
    }

    sl-input {
      width: 0;
      flex: 3 1 auto;
      
    }

    .margin-xs {
      margin-right: var(--sl-spacing-x-small);
    }

    sl-select {
      flex: 2 1 auto;
      width: 0;
    }
  `;

  /**The default unit to choose. Must be the `value` of a child mb-option element */
  @property({ type: String, reflect: true }) default: string;

  /** Required form validation */
  @property({ type: Boolean, reflect: true }) required: boolean = false;

  @property({ type: Number, reflect: true }) max: number | string | null;

  @property({ type: Number, reflect: true }) min: number | string | null;
  /** Hides the units. Make sure to set a default unit, or set it programatically. */
  @property({ type: Boolean, reflect: true }) hideunit: boolean = false;

  @property({ type: Boolean, reflect: true }) disabled: boolean;

  @property({ type: Number, reflect: true }) step: number;

  /** Automatically disables the unit if only a single unit is present */
  @property({type: Boolean, reflect: true}) enablesingleunit: boolean = false
  @state()
  units: MbUnit[] = [];

  handleChildChange() {
    this.units = [...(this.querySelectorAll('mb-unit') as NodeListOf<MbUnit>)];
  }

  reportValidity() {
    const input = this.shadowRoot!.querySelector('sl-input') as SlInput;
    return input.reportValidity();
  }
  connectedCallback() {
    super.connectedCallback();
    const observer = new MutationObserver(() => {
      this.handleChildChange();
    });
    observer.observe(this, { attributes: true, childList: true });
  }

  handleInput(e: CustomEvent) {
    const input = e.target as SlInput;
    if (input.value === '') {
      this.data = undefined;
    } else {
      this.data = {
        unit: this.data?.unit || this.default,
        magnitude: parseFloat(input.value),
      };
    }
  }

  handleSelect(e: CustomEvent) {
    const select = e.target as SlSelect;
    if (select.value) {
      this.data = {
        ...this.data,
        unit: select.value as string,
      };
    } else {
      if (this.data?.magnitude) {
        this.data = {
          ...this.data,
          unit: undefined,
        };
      } else {
        this.data = undefined;
      }
    }

    let Unit = this.units.filter(unit => unit.unit === select.value)[0];

    this.max = Unit ? Unit.max : null;
    this.min = Unit ? Unit.min : null;
  }

  get displayUnit(){
    if (this.data?.unit){
      return this.data?.unit 
    }
    if (this.default){
      return this.default
    }
    return ''
    
  }

  render() {
    return html`
      <sl-input
        class=${this.hideunit ? '' : 'margin-xs'}
        .disabled=${this.disabled}
        .step=${this.step ?? 'any'}
        .required=${this.required}
        .max=${this.max}
        .min=${this.min}
        label=${ifDefined(this.label)}
        type="number"
        @sl-input=${this.handleInput}
        .value=${this.data?.magnitude?.toString() || ''}
      ></sl-input>
      <sl-select
        .disabled=${this.disabled || (this.enablesingleunit ? false : this.units.length === 1)}
        style="${this.hideunit ? 'display: none' : ''}"
        placeholder="Select units"
        .value=${this.displayUnit}
        @sl-change=${this.handleSelect}
      >
        ${this.units.map(
          unit =>
            html`<sl-menu-item
              value=${unit.unit}
              max=${unit.max}
              min=${unit.min}
              >${unit.label}</sl-menu-item
            >`
        )}
      </sl-select>
      <slot style="display: none" @slotchange=${this.handleChildChange}></slot>
    `;
  }
}
Example #25
Source File: select-list-card.ts    From select-list-card with MIT License 4 votes vote down vote up
@customElement('select-list-card')
export class SelectListCard extends LitElement implements LovelaceCard {
  public static async getConfigElement(): Promise<LovelaceCardEditor> {
    return document.createElement('select-list-card-editor') as LovelaceCardEditor;
  }

  public static getStubConfig(hass, entities): object {
    const entity = entities.find(item => item.startsWith('input_select'));
    const dummy = hass;
    return {
      entity: entity || '',
      icon: '',
      truncate: true,
      show_toggle: false,
      scroll_to_selected: true,
      max_options: 5,
    };
  }

  public setConfig(config: SelectListCardConfig): void {
    if (!config || !config.entity || !config.entity.startsWith('input_select')) {
      throw new Error(localize('error.invalid_configuration'));
    }

    if (config.test_gui) {
      getLovelace().setEditMode(true);
    }

    this.config = {
      title: '',
      icon: '',
      show_toggle: false,
      truncate: true,
      scroll_to_selected: true,
      max_options: 5,
      ...config,
    };
    this.open = this.open || this.config.open;
  }

  public async getCardSize(): Promise<number> {
    let size = 0;
    if (!this.config) {
      return size;
    }
    size += this.config.title ? 1 : 0;
    size += this.config.max_options ? this.config.max_options : this.options.length;
    return size;
  }

  @property() public hass!: HomeAssistant;
  @property() private config!: SelectListCardConfig;
  @property() private open = true;
  @query('#list') private listEl;
  private options: string[] = [];

  protected shouldUpdate(changedProps: PropertyValues): boolean {
    return hasConfigOrEntityChanged(this, changedProps, false);
  }

  protected render(): TemplateResult | void {
    if (!this.config.entity) {
      return this.showError();
    }
    const stateObj = this.stateObj;
    const selected = stateObj.state;
    this.options = stateObj.attributes.options;
    const style = this.config.max_options === 0 ? '' : `max-height: ${(this.config.max_options || 5) * 48 + 16}px`;
    return html`
      <ha-card aria-label=${`Select list card: ${this.config.entity}`}>
        ${this.config.title && this.config.title.length
          ? html`
              <div
                class="card-header ${this.config.show_toggle ? 'pointer' : ''}"
                @click=${this.toggle}
                ?open=${this.open}
              >
                <div class="name">
                  ${this.config.icon && this.config.icon.length
                    ? html`
                        <ha-icon class="icon" .icon="${this.config.icon}"></ha-icon>
                      `
                    : ''}
                  ${this.config.title}
                </div>
                ${this.config.show_toggle
                  ? html`
                      <ha-icon class="pointer" .icon="${this.open ? 'mdi:chevron-up' : 'mdi:chevron-down'}"></ha-icon>
                    `
                  : ''}
              </div>
            `
          : ''}
        <paper-listbox
          id="list"
          @iron-select=${this.selectedOptionChanged}
          .selected=${this.options.indexOf(selected)}
          style="${style}"
          ?open=${this.open}
        >
          ${this.options.map(option => {
            if (this.config.truncate) {
              return html`
                <paper-item title="${option}"><div class="truncate-item">${option}</div></paper-item>
              `;
            }
            return html`
              <paper-item>${option}</paper-item>
            `;
          })}
        </paper-listbox>
      </ha-card>
    `;
  }

  private get stateObj(): HassEntity {
    return this.hass.states[this.config.entity] as HassEntity;
  }

  private toggle(): void {
    if (!this.config.show_toggle) {
      return;
    }
    this.open = !this.open;
    if (this.open) {
      const selected = this.listEl.querySelector('.iron-selected') as HTMLElement;
      if (selected) {
        setTimeout(() => {
          this.setScrollTop(selected.offsetTop);
        }, 100);
      }
    }
  }

  private setScrollTop(offsetTop: number): void {
    if (!this.config.scroll_to_selected) {
      return;
    }
    this.listEl.scrollTop = offsetTop - (this.listEl.offsetTop + 8);
  }

  private async selectedOptionChanged(ev: any): Promise<any> {
    const option = ev.detail.item.innerText.trim();
    const selected = this.stateObj.state;
    if (ev.detail && ev.detail.item) {
      this.setScrollTop(ev.detail.item.offsetTop);
    }
    if (option === selected) {
      return;
    }
    await SelectListCard.setInputSelectOption(this.hass, this.config.entity, option);
  }

  private static setInputSelectOption(hass: HomeAssistant, entity: string, option: string): Promise<any> {
    return hass.callService('input_select', 'select_option', {
      option,
      entity_id: entity,
    });
  }

  private showError(): TemplateResult {
    return html`
        <ha-card>
          <div class="preview not-available">
            <div class="metadata">
              <div class="not-available">
                ${localize('error.not_available')}
              </div>
            <div>
          </div>
        </ha-card>
      `;
  }

  static get styles(): CSSResult {
    return css`
      select-list-card:focus {
        outline: none;
      }
      .card-header {
        display: flex;
        justify-content: space-between;
        user-select: none;
      }
      .pointer {
        cursor: pointer;
      }
      paper-listbox {
        display: none;
        box-sizing: border-box;
        overflow-y: auto;
        overflow-x: hidden;
        scrollbar-color: var(--scrollbar-thumb-color) transparent;
        scrollbar-width: thin;
        background: var(--paper-card-background-color);
      }
      paper-listbox[open] {
        display: block;
      }
      paper-listbox::-webkit-scrollbar {
        width: 0.4rem;
        height: 0.4rem;
      }
      paper-listbox::-webkit-scrollbar-thumb {
        -webkit-border-radius: 4px;
        border-radius: 4px;
        background: var(--scrollbar-thumb-color);
      }
      paper-item {
        cursor: pointer;
      }
      paper-item:hover::before,
      .iron-selected:before {
        position: var(--layout-fit_-_position);
        top: var(--layout-fit_-_top);
        right: var(--layout-fit_-_right);
        bottom: var(--layout-fit_-_bottom);
        left: var(--layout-fit_-_left);
        background: currentColor;
        content: '';
        opacity: var(--dark-divider-opacity);
        pointer-events: none;
      }
      .truncate-item {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
    `;
  }
}
Example #26
Source File: admin.ts    From litelement-website with MIT License 4 votes vote down vote up
@customElement('lit-admin')
export class Admin extends LitElement {
  @property({ type: String }) username!: string;

  render() {
    return html`
      <h2>Admin</h2>
      <p>Welcome ${this.username}</p>
      <p>Only for authorized users</p>
      <p>Go to <a href="${router.urlForPath('/about')}">About</a></p>
    `;
  }

  public onBeforeEnter(
    location: RouterLocation,
    commands: PreventAndRedirectCommands,
    router: Router
  ): Promise<unknown> | RedirectResult | undefined {
    console.log('onBeforeEnter');
    if (!this.isAuthorized()) {
      // sync operation
      // return commands.redirect('/');

      // async operation
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // console.log('Not authorized, redirect to home page');
          resolve(commands.redirect('/'));
        }, 2000);
      });
    }

    // console.log('You can see this page');
  }

  public onAfterEnter(
    location: RouterLocation,
    commands: PreventAndRedirectCommands,
    router: Router
  ): void {
    console.log('onAfterEnter');
    // Read params from URL
    const section = location.params.section; // path: 'admin/:section'
    const username = new URLSearchParams(location.search).get('username');
    console.log('section', section);
    console.log('username', username);

    // Assign the username value from the URL
    this.username = username || 'user';

    // No need to return a result.
  }

  public onBeforeLeave(
    location: RouterLocation,
    commands: PreventAndRedirectCommands,
    router: Router
  ): PreventResult | Promise<unknown> | undefined {
    console.log('onBeforeLeave');

    const leave = window.confirm('Are you sure to leave this page?');
    if (!leave) {
      // sync operation
      // return commands.prevent();

      // async operation
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // console.log('Not authorized, redirect to home page');
          console.log('resolved');
          resolve(commands.prevent());
        }, 2000);
      });
    }
  }

  public onAfterLeave(
    location: RouterLocation,
    commands: PreventAndRedirectCommands,
    router: Router
  ): void {
    console.log('onAfterLeave');
    alert('Just wanted to say goodbye!');
  }

  private isAuthorized() {
    // Logic to determine if the current user can see this page
    return true;
  }
}
Example #27
Source File: linak-desk-card.ts    From linak-desk-card with MIT License 4 votes vote down vote up
@customElement('linak-desk-card')
export class LinakDeskCard extends LitElement {
  public static async getConfigElement(): Promise<LovelaceCardEditor> {
    return document.createElement('linak-desk-card-editor');
  }

  public static getStubConfig(_: HomeAssistant, entities: string[]): Partial<LinakDeskCardConfig> {
      const [desk] = entities.filter((eid) => eid.substr(0, eid.indexOf('.')) === 'cover' && eid.includes('desk'));
      const [height_sensor] = entities.filter((eid) => eid.substr(0, eid.indexOf('.')) === 'sensor' && eid.includes('desk_height'));
      const [moving_sensor] = entities.filter((eid) => eid.substr(0, eid.indexOf('.')) === 'binary_sensor' && eid.includes('desk_moving'));
      const [connection_sensor] = entities.filter((eid) => eid.substr(0, eid.indexOf('.')) === 'binary_sensor' && eid.includes('desk_connection'));
    return {
      desk,
      height_sensor,
      moving_sensor,
      connection_sensor,
      min_height: 62,
      max_height: 127,
      presets: []
    };
  }

  @property({ attribute: false }) public hass!: HomeAssistant;
  @internalProperty() private config!: LinakDeskCardConfig;

  public setConfig(config: LinakDeskCardConfig): void {
    if (!config.desk || !config.height_sensor) {
      throw new Error(localize('common.desk_and_height_required'));
    }

    if (!config.min_height || !config.max_height) {
      throw new Error(localize('common.min_and_max_height_required'));
    }

    this.config = { ...config };
  }

  get desk(): HassEntity {
    return this.hass.states[this.config.desk];
  }

  get height(): number {
    return this.relativeHeight + this.config.min_height;
  }

  get relativeHeight(): number {
    return parseInt(this.hass.states[this.config.height_sensor]?.state, 10) || 0;
  }

  get connected(): boolean {
    return this.hass.states[this.config.connection_sensor]?.state === 'on';
  }

  get moving(): boolean {
    return this.hass.states[this.config.moving_sensor]?.state === 'on';
  }
  get alpha(): number {
    return (this.relativeHeight) / (this.config.max_height - this.config.min_height)
  }

  protected shouldUpdate(changedProps: PropertyValues): boolean {
    if (!this.config) {
      return false;
    }

    if (changedProps.has('config')) {
      return true;
    }

    const newHass = changedProps.get('hass') as HomeAssistant | undefined;
    if (newHass) {
      return (
        newHass.states[this.config?.desk] !== this.hass?.states[this.config?.desk]
        || newHass.states[this.config?.connection_sensor]?.state !== this.hass?.states[this.config?.connection_sensor]?.state
        || newHass.states[this.config?.height_sensor]?.state !== this.hass?.states[this.config?.height_sensor]?.state
        || newHass.states[this.config?.moving_sensor]?.state !== this.hass?.states[this.config?.moving_sensor]?.state
      );
    }
    return true;
  }

  protected render(): TemplateResult | void {
    return html`
      <ha-card .header=${this.config.name}>
        ${this.config.connection_sensor ? html`<div class="connection">
          ${localize(this.connected ? 'status.connected' : 'status.disconnected')}
          <div class="indicator ${this.connected ? 'connected' : 'disconnected'}" ></div>
        </div>` : html``}
        <div class="preview">
          <img src="${tableTopImg}" style="transform: translateY(${this.calculateOffset(90)}px);" />
          <img src="${tableMiddleImg}" style="transform: translateY(${this.calculateOffset(60)}px);" />
          <img src="${tableBottomImg}" />
          <div class="height" style="transform: translateY(${this.calculateOffset(90)}px);">
            ${this.height}
            <span>cm</span>
          </div>
          <div class="knob">
            <div class="knob-button" 
                  @touchstart='${this.goUp}' 
                  @mousedown='${this.goUp}' 
                  @touchend='${this.stop}'
                  @mouseup='${this.stop}'>
              <ha-icon icon="mdi:chevron-up"></ha-icon>
            </div>
            <div class="knob-button" 
                  @touchstart=${this.goDown} 
                  @mousedown=${this.goDown} 
                  @touchend=${this.stop}
                  @mouseup=${this.stop}>
              <ha-icon icon="mdi:chevron-down"></ha-icon>
            </div>
          </div>
          ${this.renderPresets()}
        </div>
      </ha-card>
    `;
  }

  calculateOffset(maxValue: number): number {
    return Math.round(maxValue * (1.0 - this.alpha))
  }

  renderPresets(): TemplateResult {
    const presets = this.config.presets || [];

    return html`
        <div class="presets">
          ${presets.map(item => html`
            <paper-button @click="${() => this.handlePreset(item.target)}">
              ${item.label} - ${item.target} cm
            </paper-button>`)} 
        </div>
      `;
  }

  handlePreset(target: number): void {
    if (target > this.config.max_height) {
      return;
    }

    const travelDist = this.config.max_height - this.config.min_height;
    const positionInPercent = Math.round(((target - this.config.min_height) / travelDist) * 100);

    if (Number.isInteger(positionInPercent)) {
      this.callService('set_cover_position', { position: positionInPercent });
    }
  }

  private goUp(): void {
    this.callService('open_cover');
  }

  private goDown(): void {
    this.callService('close_cover');
  }

  private stop(): void {
    this.callService('stop_cover');
  }

  private callService(service, options = {}): void {
    this.hass.callService('cover', service, {
      entity_id: this.config.desk,
      ...options
    });
  }

  static get styles(): CSSResult {
    return css`
      :host {
        display: flex;
        flex: 1;
        flex-direction: column;
      }
      ha-card {
        flex-direction: column;
        flex: 1;
        position: relative;
        padding: 0px;
        border-radius: 4px;
        overflow: hidden;
      }
      .preview {
        background: linear-gradient(to bottom, var(--primary-color), var(--dark-primary-color));
        overflow: hidden;
        position: relative;
        min-height: 365px;
      }
      .preview img {
        position: absolute;
        bottom: 0px;
        transition: all 0.2s linear;
      }
      .preview .knob {
        background: #fff;
        position: absolute;
        display: flex;
        flex-direction: column;
        left: 20px;
        bottom: 12px;
        border-radius: 35px;
        width: 50px;
        overflow: hidden;
        height: 120px;
        box-shadow: 0px 0px 36px darkslategrey;
      }
      .preview .knob .knob-button {
        display: flex;
        justify-content: center;
        align-items: center;
        flex: 1;
      }
      .preview .knob .knob-button ha-icon {
        color: #030303;
        cursor: pointer;
      }
      .preview .knob .knob-button:active {
        background: rgba(0, 0, 0, 0.06);
      }
      .height {
        position: absolute;
        left: 30px;
        top: 60px;
        font-size: 32px;
        font-weight: bold;
        transition: all 0.2s linear;
      }
      .height span {
        opacity: 0.6;
      }
      .presets {
        position: absolute;
        display: flex;
        flex-direction: column;
        justify-content: space-around;
        width: 36%;
        min-width: 120px;
        height: 80%;
        right: 5%;
        top: 10%;
      }

      .presets > paper-button {
        height: 40px;
        margin-bottom: 5px;
        background-color: white;
        border-radius: 20px;
        box-shadow: darkslategrey 0px 0px 36px;
        display: flex;
        justify-content: center;
        align-items: center;
        cursor: pointer;
        color: rgb(3, 3, 3);
        font-size: 18px;
        font-weight: 500;
      }

      .connection {
        position: absolute;
        display: flex;
        align-items: center;
        right: 12px;
        top: 10px;
        color: var(--text-primary-color);
        z-index: 1;
      }

      .connection .indicator {
        margin-left: 10px;
        height: 10px;
        width: 10px;
        border-radius: 50%;
      } 

      .indicator.connected {
        background-color: green;
      }
      .indicator.disconnected {
        background-color: red;
      }
    `;
  }
}
Example #28
Source File: mini-thermostat.ts    From lovelace-mini-thermostat with MIT License 4 votes vote down vote up
@customElement('mini-thermostat')
export class MiniThermostatCard extends LitElement {
  public static getStubConfig(): object {
    return {};
  }

  static get properties(): PropertyDeclarations {
    return {
      _values: { type: Object },
      unit: { type: String },
      _updating: { type: Boolean },
      entity: { type: Object },
      _hass: { type: Object },
      config: { type: Object },
    };
  }

  private _hass: HomeAssistant | null;
  private config: CardConfig | null;
  private entity: HassEntity | null;
  private _updating = false;
  private _values: Values = {};
  private unit: string;

  private _debounceTemperature = debounce(
    (values: Values) => {
      if (!this.config!.entity || !this.hass) return;

      const details = {
        entity_id: this.config!.entity,
        ...values,
      };
      this.hass.callService('climate', 'set_temperature', details);
    },
    {
      wait: 2000,
    },
  );

  constructor() {
    super();

    this.config = null;
    this._updating = false;
    this._values = {};
    this.entity = null;
    this.unit = '';
    this._hass = null;
  }

  public setConfig(config: CardConfig): void {
    this._validateConfig(config);
    this.config = config;
  }

  set hass(hass: HomeAssistant) {
    this._hass = hass;

    if (!this.config) return;
    const entity = hass.states[this.config.entity];
    if (!entity) return;
    this.entity = entity;

    const values: Values = {
      temperature: entity.attributes.temperature,
    };
    if (this._updating && values.temperature == this._values.temperature) {
      this._updating = false;
    } else {
      this._values = values;
    }

    this.unit = hass.config.unit_system.temperature;
  }

  get hass() {
    return this._hass!;
  }

  private _validateConfig(config: CardConfig): void {
    const throwError = (error: string) => {
      throw new Error(error);
    };
    if (!config || !config.entity) {
      throwError(localize('common.config.entity'));
    }
    if (config.layout) {
      if (config.layout.dropdown) {
        if (config.layout.dropdown !== 'hvac_modes' && config.layout.dropdown !== 'preset_modes') {
          throwError(localize('common.config.dropdown'));
        }
      }
      if (config.layout.preset_buttons) {
        if (!Array.isArray(config.layout.preset_buttons)) {
          if (config.layout.preset_buttons !== 'hvac_modes' && config.layout.preset_buttons !== 'preset_modes')
            throwError('Invalid configuration for preset_buttons');
        } else {
          config.layout.preset_buttons.forEach(button => {
            if (!button.data) {
              throwError('Missing option: data');
            }
            if (button.type === 'temperature' && typeof button.data.temperature !== 'number') {
              throwError('Temperature should be a number');
            }
            if (button.type === 'hvac_mode' && !button.data.hvac_mode) {
              throwError('Missing option: data.hvac_mode');
            }
            if (button.type === 'preset_mode' && !button.data.preset_mode) {
              throwError('Missing option: data.preset_mode');
            }
            if ((button.type === 'script' || button.type === 'service') && !button.entity) {
              throwError('Missing option: entity');
            }
          });
        }
      }
    }
  }

  private _getIcon(name: string) {
    if (this.config?.icons && this.config.icons[name]) {
      return this.config.icons[name];
    }
    return ICONS[name] || name;
  }

  private _getLabel(name: string, fallback?: string) {
    if (this.config?.labels && this.config.labels[name]) {
      return this.config.labels[name];
    }
    return this.hass.localize(name) || fallback;
  }

  private shouldRenderUpDown() {
    return this.config!.layout?.up_down !== false;
  }

  protected shouldUpdate(changedProps: PropertyValues): boolean {
    return UPDATE_PROPS.some(prop => changedProps.has(prop));
  }

  protected render(): TemplateResult | void {
    if (!this.config || !this.hass) {
      return html``;
    }

    if (!this.entity) {
      return html`
        <ha-card>
          <div class="warning">${localize('common.config.not-available')}</div>
        </ha-card>
      `;
    }

    return html`
      <ha-card
        class="${this._computeClasses()}"
        tabindex="0"
        aria-label="${`MiniThermostat: ${this.config.entity}`}"
        .header="${this.config!.name}"
      >
        <div class="flex-box">
          <div class="state">
            ${this._renderState()}
          </div>
          <div class="container-middle">
            ${this._renderMiddle()}
          </div>
          ${this._renderEnd()}
        </div>
      </ha-card>
    `;
  }

  private _renderState() {
    const relativeState = this._getRelativeState(this.entity);
    const stateIcon = relativeState === 'under' ? 'heat' : relativeState === 'above' ? 'cool' : 'default';
    const currentTemperature = this.entity!.attributes.current_temperature;
    return html`
      <mwc-button
        dense
        class="state-${relativeState}"
        @action=${this._handleAction}
        .actionHandler=${actionHandler({
          hasHold: hasAction(this.config!.hold_action),
          hasDoubleTap: hasAction(this.config!.double_tap_action),
          repeat: this.config!.hold_action ? this.config!.hold_action.repeat : undefined,
        })}
      >
        <ha-icon icon="${this._getIcon(stateIcon)}"></ha-icon>
        ${this._toDisplayTemperature(currentTemperature)}
      </mwc-button>
    `;
  }

  private _renderMiddle() {
    if (!this.config?.layout) return '';

    const middle: any[] = [];
    if (this.config!.layout.name) {
      middle.push(this._renderName(this.config!.layout.name));
    }
    if (this.config!.layout.dropdown) {
      middle.push(this._renderDropdown(this.config!.layout.dropdown));
    }
    if (this.config!.layout.preset_buttons) {
      middle.push(this._renderPresetButtons());
    }

    return html`
      ${middle.map(
        item =>
          html`
            ${item}
          `,
      )}
    `;
  }

  private _renderDropdown(dropdown: string) {
    if (dropdown === 'hvac_modes') {
      return this._renderHvacModesDropdown();
    } else if (dropdown === 'preset_modes') {
      return this._renderPresetModesDropdown();
    }
    return '';
  }

  private _renderName(name: string) {
    return html`
      <mwc-button dense @click=${() => this._showEntityMoreInfo()}>
        ${name}
      </mwc-button>
    `;
  }

  private _renderHvacModesDropdown() {
    if (!this.entity!.attributes.hvac_modes) return '';

    const modes = this.entity!.attributes.hvac_modes;
    const currentMode = this.entity!.state;
    const localizationKey = 'state.climate';

    return this._renderListbox(modes, currentMode, localizationKey, 'hvac_mode');
  }

  private _renderPresetModesDropdown() {
    if (!this.entity!.attributes.preset_mode) return '';

    if (!this.entity!.attributes.preset_modes) return this.entity!.attributes.preset_mode;

    const modes = this.entity!.attributes.preset_modes;
    const currentMode = this.entity!.attributes.preset_mode;
    const localizationKey = 'state_attributes.climate.preset_mode';

    return this._renderListbox(modes, currentMode, localizationKey, 'preset_mode');
  }

  private _renderListbox(options: string[], current: string, localizationKey: string, service: string) {
    return html`
      <ha-paper-dropdown-menu no-label-float dynamic-align>
        <paper-listbox
          slot="dropdown-content"
          attr-for-selected="item-name"
          .selected="${current}"
          @selected-changed="${ev => this._handleModeChanged(ev, service, current)}"
        >
          ${options.map(
            option => html`
              <paper-item item-name=${option}>
                ${this._getLabel(`${localizationKey}.${option}`, option)}
              </paper-item>
            `,
          )}
        </paper-listbox>
      </ha-paper-dropdown-menu>
    `;
  }

  private _renderPresetButtons({ config, entity } = this) {
    if (!config?.layout?.preset_buttons || !entity) return '';

    if (Array.isArray(config.layout.preset_buttons)) {
      return html`
        ${config.layout.preset_buttons.map(button => this._renderPresetButton(button))}
      `;
    } else {
      if (config.layout.preset_buttons === 'preset_modes') {
        return html`
          ${entity.attributes.preset_modes.map(mode => this._renderSetPresetModeButton(mode))}
        `;
      } else if (config.layout.preset_buttons === 'hvac_modes') {
        return html`
          ${entity.attributes.hvac_modes.map(mode => this._renderSetHvacModeButton(mode))}
        `;
      } else {
        return '';
      }
    }
  }

  private _renderPresetButton(button: PresetButtonConfig) {
    switch (button.type) {
      case 'temperature':
        return this._renderSetTemperatureButton(button.data.temperature!, button.icon, button.label);
      case 'hvac_mode':
        return this._renderSetHvacModeButton(button.data.hvac_mode!, button.icon, button.label);
      case 'preset_mode':
        return this._renderSetPresetModeButton(button.data.preset_mode!, button.icon, button.label);
      case 'script':
        return this._renderScriptButton(button.entity!, button.data, button.icon, button.label);
      case 'service':
        return this._renderServiceButton(button.entity!, button.data, button.icon, button.label);
      default:
        return '';
    }
  }

  private _renderSetTemperatureButton(temperature: number, icon?: string, label?: string) {
    const isCurrentTargetTemperature = this.entity?.attributes.temperature === temperature;
    const action = () => this._setTemperature(temperature);
    if (!icon && !label) {
      label = this._toDisplayTemperature(temperature);
    }
    return this._renderActionButton(action, isCurrentTargetTemperature, icon, label);
  }

  private _renderSetHvacModeButton(mode: string, icon?: string, label?: string) {
    const isCurrentHvacMode = this.entity!.state === mode;
    const action = () => this._setMode('hvac_mode', mode);
    if (!icon && !label) {
      label = this._getLabel(`state.climate.${mode}`, mode);
    }
    return this._renderActionButton(action, isCurrentHvacMode, icon, label);
  }

  private _renderSetPresetModeButton(mode: string, icon?: string, label?: string) {
    const isCurrentPresetMode = this.entity!.attributes.preset_mode === mode;
    const action = () => this._setMode('preset_mode', mode);
    if (!icon && !label) {
      label = this._getLabel(`state_attributes.climate.preset_mode.${mode}`, mode);
    }
    return this._renderActionButton(action, isCurrentPresetMode, icon, label);
  }

  private _renderScriptButton(entity: string, data: any, icon?: string, label?: string) {
    const action = () => this._callScript(entity, data);
    return this._renderActionButton(action, false, icon, label);
  }

  private _renderServiceButton(entity: string, data: any, icon?: string, label?: string) {
    const action = () => this._callService(entity, data);
    return this._renderActionButton(action, false, icon, label);
  }

  private _renderActionButton(action: () => any, active: boolean, icon?: string, label?: string) {
    return html`
      <mwc-button dense .raised="${active}" .outlined="${!active}" @click="${action}">
        ${icon
          ? html`
              <ha-icon icon="${this._getIcon(icon)}"></ha-icon>
            `
          : label}
      </mwc-button>
    `;
  }

  private _renderTemperatureChangeButton(change: number) {
    const direction = change >= 0 ? 'up' : 'down';
    return html`
      <ha-icon-button
        title="Temperature ${direction}"
        class="change-arrow"
        icon="${this._getIcon(`${direction}`)}"
        @click="${() => this._changeTemperature(change)}"
        class="action"
      >
      </ha-icon-button>
    `;
  }

  private _renderEnd({ temperature: targetTemperature } = this._values) {
    const upDown = this.shouldRenderUpDown();
    const step_size = this.config!.step_size || DEFAULT_STEP_SIZE;
    return upDown
      ? html`
          <div class="actions flex-box">
            ${this._renderTemperatureChangeButton(+step_size)}
            <mwc-button dense @click="${() => this._showEntityMoreInfo()}">
              <span class="${this._updating ? 'updating' : ''}">
                ${this._toDisplayTemperature(targetTemperature)}
              </span>
            </mwc-button>
            ${this._renderTemperatureChangeButton(-step_size)}
          </div>
        `
      : '';
  }

  private _toDisplayTemperature(temperature?: number, fallback = 'N/A'): string {
    return temperature ? `${Number(temperature).toFixed(1)} ${this.unit}` : fallback;
  }

  private _changeTemperature(change): void {
    if (!this.hass || !this.config) {
      return;
    }
    const currentTarget = this._values.temperature;
    this._setTemperature(currentTarget + change);
  }

  private _setTemperature(temperature): void {
    if (!this.hass || !this.config) {
      return;
    }
    this._updating = true;
    this._values = {
      ...this._values,
      temperature,
    };
    this._debounceTemperature({ ...this._values });
  }

  private _callScript(entity: string, data: any): void {
    const split = entity.split('.')?.pop();
    if (!split || !split.length) {
      return;
    }
    this.hass.callService('script', split, ...data);
  }

  private _callService(entity: string, data: any): void {
    const [domain, service] = entity.split('.');
    this.hass.callService(domain, service, ...data);
  }

  private _handleModeChanged(ev: CustomEvent, modeType: string, current: string): void {
    const newVal = ev.detail.value;
    // prevent heating while in idle by checking for current
    if (newVal && newVal !== current) {
      this._setMode(modeType, newVal);
    }
  }

  private _setMode(modeType: string, value: string): void {
    const serviceData = {
      entity_id: this.config!.entity,
      [modeType]: value,
    };
    this.hass.callService('climate', `set_${modeType}`, serviceData);
  }

  private _getRelativeState(stateObj): string {
    const targetTemperature = stateObj.attributes.temperature;
    if (
      stateObj.state === CURRENT_HVAC_OFF ||
      stateObj.state === CURRENT_HVAC_IDLE ||
      targetTemperature == null ||
      stateObj.attributes.hvac_action === CURRENT_HVAC_OFF ||
      stateObj.attributes.hvac_action === CURRENT_HVAC_IDLE
    ) {
      return 'inactive';
    }
    const currentTemperature = stateObj.attributes.current_temperature;
    if (currentTemperature < targetTemperature) {
      return 'under';
    }
    if (currentTemperature === targetTemperature) {
      return 'equal';
    }
    if (currentTemperature > targetTemperature) {
      return 'above';
    }
    return 'neutral';
  }

  private _handleAction(ev: ActionHandlerEvent): void {
    if (this.hass && this.config && ev.detail.action) {
      handleAction(this, this.hass, this.config, ev.detail.action);
    }
  }

  private _showEntityMoreInfo({ entity } = this.config!): void {
    const event = new Event('hass-more-info', { bubbles: true, cancelable: false, composed: true });
    (event as any).detail = { entityId: entity };
    this.dispatchEvent(event);
  }

  private _computeClasses({ config } = this) {
    if (!config) return '';
    const hasHeader = !!config.name;
    return classMap({
      grouped: config.layout?.grouped || false,
      'with-header': hasHeader,
      'no-header': !hasHeader,
      tiny: config.layout?.tiny || false,
    });
  }

  static get styles(): CSSResult {
    return styles;
  }

  getCardSize(): number {
    return 1;
  }
}
Example #29
Source File: opc-timeline.ts    From op-components with MIT License 4 votes vote down vote up
@customElement('opc-timeline')
export class Timeline extends LitElement {
  @property({ type: Array, attribute: 'steps'}) steps = [];
  @property({ type: Number, attribute: 'current-step-index' }) currentStepIndex = 0;
  @property({ type: String, attribute: 'variant'}) variant = 'default';
  @internalProperty() stepWidth = 0;
  @query('li') listElement;
  @query('ul') listElementContainer;
  static get styles() {
    return [ style ];
  }

  _scrollHandler(direction) {
    this.shadowRoot.querySelector('#timeline-steps').scrollBy({
      left: direction === 'left' ?  -this.listElement.offsetWidth : this.listElement.offsetWidth,
      behavior: 'smooth'
    });
  }

  _getDirectionArrow() {
    if (this.variant === 'compact') {
      return {
        left: html`
          <span class="timeline__arrow timeline__arrow--left" @click="${() => {this._scrollHandler('left')}}">
            <div class="timeline__arrow--shape">
            </div>
          </span>`,
          right: html`
          <span class="timeline__arrow timeline__arrow--right" @click="${() => this._scrollHandler('right')}">
            <div class="timeline__arrow--shape">
            </div>
          </span>`,
      }
    }
    return {
      left: '',
      right: ''
    };
  }

  _eventEmitter(stepIndex: number, stepItem: string|null) {
    this.dispatchEvent(
      new CustomEvent('opc-timeline-step:click', {
        detail: {
          message: 'opc-timeline-step clicked',
          data: {
            index: stepIndex,
            item: stepItem,
          },
          bubbles: true,
          composed: true,
        }
      })
    );
  }

  updateTimelineWidth() {
    const liWidth = this.listElement.offsetWidth * this.steps.length;
    const ulWidth = this.listElementContainer.offsetWidth;
    this.stepWidth = ulWidth > liWidth ? ulWidth : liWidth;
  }

  updated() {
    this.updateTimelineWidth();
    window.addEventListener('resize', () => {
      this.updateTimelineWidth();
    });
  }

  render() {
    if (this.currentStepIndex < 0) {
      console.warn(`OPC-TIMELINE: The current-step-index attribute is set to ${this.currentStepIndex}, the active state will not be visible`);
    }
    return html`
    <style>
      .compact .timeline-steps {
        overflow: hidden;
      }
      .compact .timeline-steps::before {
        width: ${this.stepWidth}px;
      }
      .compact .timeline-steps .timeline-steps__step {
        flex: 0 0 180px;
      }
    </style>
    <div class="timeline ${this.variant === 'compact' ? 'compact' : ''}">
        ${this._getDirectionArrow().left}
        <ul id="timeline-steps" class="timeline-steps">
          ${this.steps.map((step, index) => {
            if (step) {
              return html`
              <li
                class="timeline-steps__step
                ${this.currentStepIndex === index ? 'active' : ''}"
                @click="${() => {this._eventEmitter(index, step)}}">
                  ${step}
              </li>`;
            } else {
              return html`
                <li 
                  class="timeline-steps__step 
                  ${this.currentStepIndex === index ? 'active' : ''}"
                  @click="${() => {this._eventEmitter(index, null)}}">
                </li>`;
            }
          })}
        </ul>
        ${this._getDirectionArrow().right}
      </div>
      <div class="timeline-label">
        <slot name="start-label"></slot>
        <slot name="end-label"></slot>
      </div>
      <div>
        <slot name="timeline-details"></slot>
      </div>
    `;
  }
}