Skip to content

Commit

Permalink
Feat: Add search function to get coordinates. (#41)
Browse files Browse the repository at this point in the history
* feat: add search function to get coordinates

* refactor: Improve MoonHorizon component with resize observer for card width measurement
  • Loading branch information
ngocjohn authored Nov 14, 2024
1 parent 72aecfa commit 3129769
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 38 deletions.
26 changes: 19 additions & 7 deletions src/components/moon-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,31 @@ import { Moon } from '../utils/moon';
export class LunarBaseData extends LitElement {
@state() moon!: Moon;
@state() swiper: Swiper | null = null;
@state() moonData?: MoonData;

static get styles(): CSSResultGroup {
return [mainStyles, unsafeCSS(swiperStyleCss)];
}

protected firstUpdated(changedProps: PropertyValues): void {
protected async firstUpdated(changedProps: PropertyValues): Promise<void> {
super.firstUpdated(changedProps);
console.time('moon');
if (this.moon) {
this.initSwiper();
this._validateMoonData();
}
this.initSwiper();
}

private _validateMoonData() {
const replacer = (key: string, value: any) => {
if (['direction', 'position'].includes(key)) {
return undefined;
}
return value;
};
const moonData = JSON.parse(JSON.stringify(this.moon.moonData, replacer));
this.moonData = moonData;
console.timeEnd('moon');
}

protected shouldUpdate(_changedProperties: PropertyValues): boolean {
Expand Down Expand Up @@ -55,11 +70,8 @@ export class LunarBaseData extends LitElement {

protected render(): TemplateResult {
// const newMoonData = this.baseMoonData;
const baseMoonData = this.moon.moonData;
const newMoonData: MoonData = { ...baseMoonData };
delete newMoonData.direction;
delete newMoonData.position;
const chunkedData = this._chunkObject(newMoonData, 5);
const baseMoonData = (this.moonData as MoonData) || {};
const chunkedData = this._chunkObject(baseMoonData, 5);
const dataContainer = Object.keys(chunkedData).map((key) => {
return html`
<div class="swiper-slide">
Expand Down
141 changes: 141 additions & 0 deletions src/components/moon-editor-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { mdiClose, mdiMagnify } from '@mdi/js';
import { LitElement, TemplateResult, CSSResultGroup, html, nothing, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

import { SearchResults } from '../types';
import { getCoordinates } from '../utils/helpers';

// styles
import editorStyles from '../css/editor.css';
import { LunarPhaseCardEditor } from '../editor';

const ALERT_DURATION = 5 * 1000; // 5 seconds

@customElement('moon-editor-search')
export class MoonEditorSearch extends LitElement {
@property({ attribute: false }) _editor!: LunarPhaseCardEditor;
@state() _searchValue: string = '';
@state() _searchResults: SearchResults[] = [];
@state() _searchResultsVisible = false;
@state() _toastDissmissed = false;

static get styles(): CSSResultGroup {
return [editorStyles];
}

render(): TemplateResult {
const searchInput = html`
<ha-textfield
.autofocus=${this.autofocus}
.aria-label=${'Search'}
.placeholder=${'Enter a location'}
.value=${this._searchValue || ''}
@input=${(ev: Event) => this._handleSearchInput(ev)}
@blur=${() => this._searchLocation()}
>
</ha-textfield>
${this._searchResultsVisible
? html`<ha-icon-button .path=${mdiClose} @click=${this._clearSearch}></ha-icon-button>`
: html`<ha-icon-button .path=${mdiMagnify} @click=${this._searchLocation}></ha-icon-button>`}
`;

const infoSuccess = html` <ha-alert
alert-type="success"
id="success-alert"
style="display: none;"
title="Location change"
>
Search results found
</ha-alert>`;

return html` <div class="card-config">
<div class="search-form">${searchInput}</div>
${infoSuccess} ${this._renderSearchResults()}
</div>`;
}

private _handleAlertDismiss(ev: Event): void {
const alert = ev.target as HTMLElement;
alert.style.display = 'none';
this._toastDissmissed = true;
}

private _renderSearchResults(): TemplateResult | typeof nothing {
if (!this._searchResultsVisible || this._searchResults.length === 0) {
return html`${!this._toastDissmissed
? html` <ha-alert
alert-type="info"
dismissable
@alert-dismissed-clicked=${(ev: Event) => this._handleAlertDismiss(ev)}
>
You can get the latitude and longitude from the search with query like "London, UK".</ha-alert
>`
: nothing}`;
}
const results = this._searchResults.map((result) => {
return html`<li class="search-item" @click=${() => this._handleSearchResult(result)}>
${result.display_name}
</li> `;
});

return html` <ul class="search-results">
${results}
</ul>`;
}

private _handleSearchResult(result: SearchResults): void {
console.log('search result', result);
const { lat, lon, display_name } = result;
const event = new CustomEvent('location-update', {
detail: {
latitude: lat,
longitude: lon,
},
bubbles: true,
composed: true,
});

this.dispatchEvent(event);
this._clearSearch();
const message = `${display_name} [${lat}, ${lon}]`;
this._handleSettingsSuccess(message);
}

private _handleSettingsSuccess(message: string): void {
const alert = this.shadowRoot?.getElementById('success-alert') as HTMLElement;
if (alert) {
alert.innerHTML = message;
alert.style.display = 'block';
setTimeout(() => {
alert.style.display = 'none';
}, ALERT_DURATION);
}
}

private _handleSearchInput(ev: Event): void {
ev.stopPropagation();
const target = ev.target as HTMLInputElement;
this._searchValue = target.value;
}

private _clearSearch(): void {
console.log('clear search');
this._searchValue = '';
this._searchResults = [];
this._searchResultsVisible = false;
}

private async _searchLocation(): Promise<void> {
console.log('search location', this._searchValue);
const searchValue = this._searchValue;
if (!searchValue || searchValue === '') {
return;
}
this._toastDissmissed = true;
const results = await getCoordinates(searchValue);
if (results) {
this._searchResults = results;
this._searchResultsVisible = true;
}
}
}
48 changes: 48 additions & 0 deletions src/css/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
font-size: 12px;
}

.search-button {
display: flex;
align-items: center;
justify-content: flex-end;
}

.switches {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -97,6 +103,48 @@ ha-select {
cursor: pointer;
}

.search-form {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
}

ul.search-results {
list-style-type: none;
padding-left: 0;
padding-right: 0;
margin-top: 0;
margin-bottom: 0;
max-height: 500px;
overflow-y: auto;
background-color: var(--divider-color);
}

li.search-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
cursor: pointer;
border-bottom: 1px solid var(--divider-color);
min-height: 40px;
}

li.search-item:nth-child(odd) {
background-color: var(--secondary-background-color);
}

li.search-item:hover {
background-color: var(--divider-color);
}

li.search-item:last-child {
border-bottom: none;
}



.location-item {
display: flex;
flex-direction: column;
Expand Down
81 changes: 57 additions & 24 deletions src/editor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* @typescript-eslint/no-explicit-any */
import { LitElement, html, TemplateResult, css, CSSResultGroup, PropertyValues } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';

// Custom card helpers
import { fireEvent, LovelaceCardEditor } from 'custom-card-helpers';
Expand All @@ -21,15 +20,20 @@ import { generateConfig } from './utils/ha-helper';
import { compareConfig, getAddressFromOpenStreet } from './utils/helpers';
import { loadHaComponents, stickyPreview, _saveConfig } from './utils/loader';

// Components
import './components/moon-editor-search';
import { mdiMagnify } from '@mdi/js';

@customElement('lunar-phase-card-editor')
export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass!: HomeAssistant;

@state() private _config!: LunarPhaseCardConfig;
@state() _config!: LunarPhaseCardConfig;
@state() private _activeTabIndex?: number;
@state() _activeGraphEditor = false;

@state() private _location!: LocationAddress;
@state() private _searchLocation: boolean = false;

public async setConfig(config: LunarPhaseCardConfig): Promise<void> {
this._config = { ...config };
Expand All @@ -52,6 +56,7 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit

protected async firstUpdated(changedProps: PropertyValues): Promise<void> {
super.firstUpdated(changedProps);
console.log('First updated');
await new Promise((resolve) => setTimeout(resolve, 0));
this._handleFirstConfig(this._config);
this.getLocation();
Expand Down Expand Up @@ -80,6 +85,7 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit
this._config = newConfig;
fireEvent(this, 'config-changed', { config: this._config });
await _saveConfig(cardId, this._config);
console.log('Config is valid');
}
}
private get selectedLanguage(): string {
Expand All @@ -95,12 +101,11 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit
}

private getLocation = () => {
this.updateComplete.then(() => {
this.updateComplete.then(async () => {
const { latitude, longitude } = this._config;
if (latitude && longitude) {
getAddressFromOpenStreet(latitude, longitude).then((location) => {
this._location = location;
});
const location = await getAddressFromOpenStreet(latitude, longitude);
this._location = location;
}
});
};
Expand Down Expand Up @@ -172,31 +177,52 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit
></ha-checkbox>
</ha-formfield>`;

const contentWrapp = html`
<div class="comboboxes">${radios} ${southern}</div>
${this._renderLocation()}
<div>
${this._config?.use_default
? this._renderUseDefault()
: this._config?.use_custom
? this._renderCustomLatLong()
: this._config?.use_entity
? this._renderEntityPicker()
: ''}
</div>
`;
const searchWrapper = html` <moon-editor-search
._editor=${this}
@location-update=${(ev: CustomEvent) => this._handleLocationChange(ev)}
></moon-editor-search>`;

const contentWrapp = html`${this._renderLocation()}
${this._searchLocation
? html`<div class="sub-config-wrapper">${searchWrapper}</div>`
: html`
<div>
${this._config?.use_default
? this._renderUseDefault()
: this._config?.use_custom
? this._renderCustomLatLong()
: this._config?.use_entity
? this._renderEntityPicker()
: ''}
</div>
<div class="comboboxes">${radios} ${southern}</div>
`}`;

return this.contentTemplate('baseConfig', 'baseConfig', 'mdi:cog', contentWrapp);
}

private _renderLocation(): TemplateResult {
const location = this._location || { country: '', city: '' };

const markerStyle = `color: var(--secondary-text-color); margin-right: 0.5rem;`;
const headerStyle = `border: none; min-height: auto;`;

const locationHeader = html` <ha-icon icon="mdi:map-marker" style=${markerStyle}></ha-icon>
<div class="header-title">
<div>${location.city}</div>
<span class="secondary">${location.country}</span>
</div>
<ha-icon-button
.path=${mdiMagnify}
@click=${() => (this._searchLocation = !this._searchLocation)}
></ha-icon-button>`;

return html`
<div class="header-container">
<div class="header-title">
<div>${location.country} ${unsafeHTML(`&#8226;`)} ${location.city}</div>
</div>
<ha-icon icon=${'mdi:map-marker'}></ha-icon>
<div class="header-container" style=${headerStyle}>
${this._searchLocation
? html`<ha-button @click=${() => (this._searchLocation = !this._searchLocation)}>Back</ha-button> `
: locationHeader}
</div>
`;
}
Expand Down Expand Up @@ -646,6 +672,13 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit

/* ----------------------------- HANDLER METHODS ---------------------------- */

private _handleLocationChange(ev: CustomEvent): void {
const { latitude, longitude } = ev.detail;
this._config = { ...this._config, latitude, longitude };
fireEvent(this, 'config-changed', { config: this._config });
this.getLocation();
}

private _handleValueChange(event: any): void {
event.stopPropagation();
const ev = event as CustomEvent;
Expand Down
Loading

0 comments on commit 3129769

Please sign in to comment.