UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

430 lines (396 loc) 12.6 kB
import { LitElement, html, css, nothing } from "lit"; import { fa } from "../../utils/widgets"; import { getUserAccount } from "../../utils/utils"; import { faSvg, titles } from "../styles"; import { faImage } from "@fortawesome/free-solid-svg-icons/faImage"; import { faCalendar } from "@fortawesome/free-solid-svg-icons/faCalendar"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight"; import { faMedal } from "@fortawesome/free-solid-svg-icons/faMedal"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle"; import { faUser } from "@fortawesome/free-solid-svg-icons/faUser"; import { onceParentAvailable } from "../../utils/widgets"; /** * Map Filters menu allows user to select map data they want displayed. * @class Panoramax.components.menus.MapFilters * @element pnx-map-filters-menu * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/) * @example * ```html * <pnx-map-filters-menu user-search="" _parent=${viewer} /> * ``` */ export default class MapFilters extends LitElement { /** @private */ static styles = [ faSvg, titles, css` .pnx-input-group { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 5px; } /* Filter block */ .pnx-filter-block { position: relative; padding: 10px 15px; border-bottom: 2px solid var(--widget-border-div); } .pnx-filter-block:first-child { padding-top: 15px; } .pnx-filter-block:last-child { border-bottom: none; padding-bottom: 15px; } .pnx-filter-zoomin { z-index: 131; background-color: rgba(255,255,255,0.8); text-align: center; font-weight: 800; position: absolute; top: 0; right: 0; left: 0; bottom: 0; display: flex; justify-content: center; align-items: center; border-radius: 25px; } .pnx-filter-zoomin.hidden { display: none; } /* Input styles */ .pnx-filter-active, pnx-search-bar.pnx-filter-active::part(container), pnx-search-bar.pnx-filter-active::part(input) { background-color: var(--widget-bg-active) !important; border-color: var(--widget-bg-active) !important; color: var(--widget-font-active) !important; } input[type=date] { min-width: 0; flex-grow: 2; padding: 2px 0; text-align: center; background-color: var(--widget-bg); color: var(--widget-font); border: 1px solid var(--widget-border-div); border-radius: 20px; font-family: var(--font-family); } /* Input shortcuts */ .pnx-input-shortcuts { margin-top: -10px; margin-bottom: 5px; } .pnx-input-shortcuts button { border: none; font-size: 0.75em; padding: 2px 6px; vertical-align: middle; background-color: var(--grey-pale); color: var(--black); border-radius: 10px; cursor: pointer; font-family: var(--font-family); } .pnx-input-shortcuts button:hover { background-color: #d9dcd9; } /* Checkbox looking like buttons */ .pnx-input-group.pnx-checkbox-btns { gap: 0; align-items: stretch; } .pnx-checkbox-btns label { display: inline-block; padding: 2px 7px; background: none; border: 1px solid var(--widget-border-btn); color: var(--widget-font-direct); cursor: pointer; font-size: 16px; text-decoration: none; border-left-width: 0px; } .pnx-checkbox-btns label:hover { background-color: var(--widget-bg-hover); } .pnx-checkbox-btns label:first-of-type { border-top-left-radius: 8px; border-bottom-left-radius: 8px; border-left-width: 1px; } .pnx-checkbox-btns label:last-of-type { border-top-right-radius: 8px; border-bottom-right-radius: 8px; } .pnx-checkbox-btns input[type="radio"] { display: none; } .pnx-checkbox-btns input[type="radio"]:checked + label { background-color: var(--widget-bg-active); color: var(--widget-font-active); } .pnx-checkbox-btns input[type="radio"]:checked + label:first-of-type { border-right-color: white; } /* Force user search width */ #pnx-filter-search-user::part(container) { width: 100%; } ` ]; /** * Component properties. * @memberof Panoramax.components.menus.MapFilters# * @type {Object} * @property {boolean} [user-search=false] Should user search filter show up ? * @property {boolean} [quality-score=false] Should quality score filter show up ? * @property {boolean} [no-date=false] Should date filters be hidden ? * @property {boolean} [no-picture-type=false] Should picture type filter be hidden ? */ static properties = { "quality-score": {type: Boolean}, "user-search": {type: Boolean}, "no-date": {type: Boolean}, "no-picture-type": {type: Boolean}, showZoomIn: {state: true}, minDate: {state: true}, maxDate: {state: true}, type: {state: true}, score: {state: true}, user: {state: true}, }; constructor() { super(); this._formDelay = null; this.showZoomIn = true; } /** @private */ connectedCallback() { super.connectedCallback(); // Input changes for(let i of this.shadowRoot.querySelectorAll("input")) { i.addEventListener("change", this._onFormChange.bind(this)); i.addEventListener("keypress", this._onFormChange.bind(this)); i.addEventListener("paste", this._onFormChange.bind(this)); i.addEventListener("input", this._onFormChange.bind(this)); } // Map zoom onceParentAvailable(this).then(() => this._parent.onceMapReady?.().then(async () => { this._parent.map.on("zoomend", this._onMapZoom.bind(this)); this._parent.map.on("filters-changed", this._onParentFilterChange.bind(this)); this._onMapZoom(); this._onParentFilterChange(this._parent.map._mapFilters); // Load default users filter const vu = this._parent.map.getVisibleUsers(); if(vu?.length > 0 && vu[0] != "geovisio") { this.user = vu[0]; const username = await this._parent.api.getUserName(vu[0]); if(username) { this.user = username; } } })); } /** * Map zoom event handler: show/hide "zoom in" labels * @private */ _onMapZoom() { this.showZoomIn = this._parent.map.getZoom() < 7; } /** * Filter changes on parent: update input fields * @private */ _onParentFilterChange(e) { this.minDate = e?.minDate || null; this.maxDate = e?.maxDate || null; // Sanity check for date if(this.minDate && this.maxDate && this.minDate > this.maxDate) { const prevMin = this.minDate; this.minDate = this.maxDate; this.maxDate = prevMin; this._onFormChange(); } this.score = e?.qualityscore?.length < 5 ? e.qualityscore.join(",") : ""; this.type = ""; if(e?.pic_type && e.pic_type != "") { this.type = e.pic_type == "flat" ? "flat" : "equirectangular"; } } /** @private */ _onSubmit(e) { e.preventDefault(); this._onFormChange(); return false; } /** @private */ _onFormChange() { if(this._formDelay) { clearTimeout(this._formDelay); } this._formDelay = setTimeout(() => this._parent?._onMapFiltersChange(), 250); } /** @private */ _userSearch(value) { return this?._parent.api.searchUsers(value) .then(data => ((data || []) .map(f => ({ title: f.label, data: f })) )); } /** @private */ _onUserSearchResult(e) { if(e.detail) { e.target.classList.add("pnx-filter-active"); } else { e.target.classList.remove("pnx-filter-active"); } return this._parent?.map?.setVisibleUsers(e.detail?.data ? [e.detail.data.id] : ["geovisio"]); } /** @private */ _onReset() { this.shadowRoot.querySelector("#pnx-filter-qualityscore")?.setAttribute("grade", ""); this.shadowRoot.querySelector("#pnx-filter-search-user")?.reset(); this.minDate = null; this.maxDate = null; this.type = ""; this.score = null; this.user = null; this._onFormChange(); } /** @private */ _onDateShortcut(date) { const dateFromField = this.shadowRoot.getElementById("pnx-filter-date-from"); const dateToField = this.shadowRoot.getElementById("pnx-filter-date-end"); if(dateFromField) { if(dateFromField.value !== date) { dateFromField.value = date; } else { dateFromField.value = ""; } } if(dateToField) { dateToField.value = ""; } } /** @private */ _onMeUserSearch() { const userAccount = getUserAccount(); if(!userAccount) { return; } const userField = this.shadowRoot.getElementById("pnx-filter-search-user"); if(!this._parent?.map?.getVisibleUsers().includes(userAccount.id)) { userField?._onResultClick({title: userAccount.name, data: {id: userAccount.id }}); } else { userField?._onResultClick(); } } /** @private */ render() { const userAccount = getUserAccount(); return html`<form @reset=${this._onReset} @change=${this._onFormChange} @submit=${this._onSubmit} > ${this["no-date"] ? "" : html`<div class="pnx-filter-block"> <div class="pnx-filter-zoomin ${this.showZoomIn ? "" : "hidden"}">${this._parent?._t.pnx.filter_zoom_in}</div> <h4>${fa(faCalendar)} ${this._parent?._t.pnx.filter_date}</h4> <div class="pnx-input-shortcuts"> <button @click=${() => this._onDateShortcut(new Date(new Date().setMonth(new Date().getMonth() - 1)).toISOString().split("T")[0])} >${this._parent?._t.pnx.filter_date_1month}</button> <button @click=${() => this._onDateShortcut(new Date(new Date().setMonth(new Date().getMonth() - 6)).toISOString().split("T")[0])} >${this._parent?._t.pnx.filter_date_6months}</button> <button @click=${() => this._onDateShortcut(new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString().split("T")[0])} >${this._parent?._t.pnx.filter_date_1year}</button> </div> <div class="pnx-input-group"> <input type="date" id="pnx-filter-date-from" .value=${this.minDate} class=${this.minDate && this.minDate != "" ? "pnx-filter-active" : ""} /> ${fa(faArrowRight)} <input type="date" id="pnx-filter-date-end" .value=${this.maxDate} class=${this.maxDate && this.maxDate != "" ? "pnx-filter-active" : ""} /> </div> </div>`} ${this["no-picture-type"] ? "" : html`<div class="pnx-filter-block"> <h4>${fa(faImage)} ${this._parent?._t.pnx.filter_picture}</h4> <div class="pnx-input-group pnx-checkbox-btns" style="justify-content: center;"> <input type="radio" id="pnx-filter-type-all" name="pnx-filter-type" value="" .checked=${!this.type || this.type === ""} /> <label for="pnx-filter-type-all">${this._parent?._t.pnx.picture_all}</label> <input type="radio" id="pnx-filter-type-flat" name="pnx-filter-type" value="flat" .checked=${this.type === "flat"} /> <label for="pnx-filter-type-flat">${this._parent?._t.pnx.picture_flat}</label> <input type="radio" id="pnx-filter-type-360" name="pnx-filter-type" value="equirectangular" .checked=${this.type === "equirectangular"} /> <label for="pnx-filter-type-360">${this._parent?._t.pnx.picture_360}</label> </div> </div>`} ${this["quality-score"] ? html` <div class="pnx-filter-block"> <div class="pnx-filter-zoomin ${this.showZoomIn ? "" : "hidden"}">${this._parent?._t.pnx.filter_zoom_in}</div> <h4 style="margin-bottom: 3px"> ${fa(faMedal)} ${this._parent?._t.pnx.filter_qualityscore} <pnx-button title="${this._parent?._t.pnx.metadata_quality_help}" kind="superinline" @click=${() => this._parent?._showQualityScoreDoc()} > ${fa(faInfoCircle)} </pnx-button> </h4> <div class="pnx-input-group"> <pnx-quality-score id="pnx-filter-qualityscore" _t=${this._parent?._t} input="pnx-filter-qualityscore" grade=${this.score} @change=${this._onFormChange} > </pnx-quality-score> </div> </div> ` : nothing} ${this["user-search"] ? html` <div class="pnx-filter-block"> <h4>${fa(faUser)} ${this._parent?._t.pnx.filter_user}</h4> ${userAccount ? html` <div class="pnx-input-shortcuts"> <button @click=${this._onMeUserSearch}> ${this._parent?._t.pnx.filter_user_mypics} </button> </div> ` : nothing} <pnx-search-bar id="pnx-filter-search-user" placeholder=${this._parent?._t.pnx.search_user} class=${this.user ? "pnx-filter-active" : ""} value=${this.user} @result=${this._onUserSearchResult} .searcher=${this._userSearch.bind(this)} ._parent=${this._parent} no-menu-closure > </pnx-search-bar> </div> ` : nothing} </form>`; } } customElements.define("pnx-map-filters-menu", MapFilters);