209 lines
5.1 KiB
JavaScript
209 lines
5.1 KiB
JavaScript
/**
|
|
@license
|
|
Copyright (c) 2022 trading_peter
|
|
This program is available under Apache License Version 2.0
|
|
*/
|
|
|
|
import { LitElement, html, css } from 'lit';
|
|
import { DomQuery } from '@tp/helpers/dom-query.js';
|
|
import { Position } from '@tp/helpers/position.js';
|
|
import { EventHelpers } from '@tp/helpers/event-helpers.js';
|
|
import { closest } from '@tp/helpers/closest.js';
|
|
|
|
class TpPopup extends EventHelpers(Position(DomQuery(LitElement))) {
|
|
static get styles() {
|
|
return [
|
|
css`
|
|
:host {
|
|
display: inline-block;
|
|
position: relative;
|
|
outline: 0;
|
|
}
|
|
|
|
.toggle {
|
|
cursor: pointer;
|
|
}
|
|
|
|
#content {
|
|
pointer-events: none;
|
|
position: fixed;
|
|
z-index: 1;
|
|
margin-top: 5px;
|
|
transition: opacity 180ms;
|
|
opacity: 0;
|
|
border-radius: 2px;
|
|
box-shadow: var(--tp-popup-shadow, none);
|
|
}
|
|
|
|
#content[open] {
|
|
pointer-events: all;
|
|
opacity: 1;
|
|
background: var(--tp-popup-background, transparent);
|
|
}
|
|
|
|
#content .content-wrap {
|
|
padding: var(--tp-popup-content-padding, 0px);
|
|
}
|
|
|
|
@media all and (min-width: 0) and (max-width: 480px) {
|
|
:host(:not([not-responsive])) #content {
|
|
bottom: 0 !important;
|
|
top: auto !important;
|
|
right: 0 !important;
|
|
left: 0 !important;
|
|
margin-top: 0;
|
|
transition: transform 0.3s, opacity 0.3s;
|
|
transform: translateY(100%);
|
|
max-height: 80%;
|
|
overflow: hidden;
|
|
overflow-y: auto;
|
|
box-shadow: var(--tp-popup-shadow-responsive, none);
|
|
}
|
|
|
|
:host(:not([not-responsive])) #content[open] {
|
|
transform: translateY(0%);
|
|
}
|
|
}
|
|
`
|
|
];
|
|
}
|
|
|
|
render() {
|
|
const { isOpen } = this;
|
|
|
|
return html`
|
|
<div class="toggle" part="toggle">
|
|
<slot id="toggleContent" name="toggle"></slot>
|
|
</div>
|
|
<div id="content" part="content" ?open=${isOpen}>
|
|
<div class="content-wrap" part="content-wrap">
|
|
<slot id="contentSlot" name="content"></slot>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
static get properties() {
|
|
return {
|
|
isOpen: { type: Boolean, reflect: true },
|
|
alwaysToggle: { type: Boolean, reflect: true },
|
|
halign: { type: String },
|
|
valign: { type: String },
|
|
scrollTarget: { type: Object }
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.isOpen = false;
|
|
this.alwaysToggle = false;
|
|
this.halign = 'middle';
|
|
this.valign = 'bottom';
|
|
this.scrollTarget = document;
|
|
this.fit = this.fit.bind(this);
|
|
this._onDocClickHandler = () => this.close();
|
|
}
|
|
|
|
get toggleEl() {
|
|
if (!this._toggleEl) {
|
|
this._toggleEl = this.querySelector('[slot="toggle"]');
|
|
}
|
|
return this._toggleEl;
|
|
}
|
|
|
|
firstUpdated() {
|
|
super.firstUpdated();
|
|
|
|
this.listen(this, 'click', '_onClick');
|
|
}
|
|
|
|
updated(changes) {
|
|
// Fire event only if isOpen wasn't undefined before. Otherwise an event is fired at the first render.
|
|
if (changes.has('isOpen') && changes.get('isOpen') !== undefined) {
|
|
this.dispatchEvent(new CustomEvent('is-open-changed', { detail: this.isOpen, bubbles: true, composed: true }));
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
this._cleanupEvents();
|
|
}
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
this._cleanupEvents();
|
|
}
|
|
|
|
open() {
|
|
this.updateComplete.then(() => {
|
|
this.fit();
|
|
});
|
|
|
|
this.isOpen = true;
|
|
|
|
this._registerEvents();
|
|
|
|
const autofocusEl = this.querySelector('[autofocus]');
|
|
if (autofocusEl) {
|
|
autofocusEl.focus();
|
|
}
|
|
}
|
|
|
|
toggle() {
|
|
if (!this.isOpen) {
|
|
this.open();
|
|
} else {
|
|
this.isOpen = false;
|
|
this._cleanupEvents();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set correct position on the popup to fit into the window.
|
|
*/
|
|
fit() {
|
|
this._posFixed(this.shadowRoot.querySelector('.toggle'), this.$.content, {
|
|
valign: this.valign || 'bottom',
|
|
halign: this.halign || 'middle',
|
|
spacing: 5
|
|
});
|
|
}
|
|
|
|
_docClick(e) {
|
|
const isThisPopup = e.composedPath().indexOf(this) > -1;
|
|
if (!isThisPopup && this.isOpen) {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
_onClick(e) {
|
|
const toggle = closest(e.composedPath()[0], '[slot="toggle"]', true) === this.toggleEl;
|
|
const closePopup = closest(e.composedPath()[0], '[close-popup]', true);
|
|
|
|
if (toggle && this.toggleEl.hasAttribute('disabled')) return;
|
|
|
|
if (toggle || this.alwaysToggle && this.isOpen) {
|
|
this.toggle();
|
|
}
|
|
|
|
if (closePopup) {
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
_registerEvents() {
|
|
this._cleanupEvents();
|
|
this.listen(document, 'mousedown', '_docClick');
|
|
this.scrollTarget.addEventListener('scroll', this.fit, { passive: true });
|
|
this.scrollTarget.addEventListener('layout', this.fit, { passive: true });
|
|
}
|
|
|
|
_cleanupEvents() {
|
|
this.unlisten(document, 'mousedown', '_docClick');
|
|
this.scrollTarget.removeEventListener('scroll', this.fit, { passive: true });
|
|
this.scrollTarget.removeEventListener('layout', this.fit, { passive: true });
|
|
}
|
|
}
|
|
|
|
window.customElements.define('tp-popup', TpPopup);
|