373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
const smPopup = document.createElement('template')
|
|
smPopup.innerHTML = `
|
|
<style>
|
|
*{
|
|
padding: 0;
|
|
margin: 0;
|
|
-webkit-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
}
|
|
:host{
|
|
position: fixed;
|
|
display: -ms-grid;
|
|
display: grid;
|
|
z-index: 10;
|
|
--accent-color: #4d2588;
|
|
--text-color: 17, 17, 17;
|
|
--background-color: 255, 255, 255;
|
|
--width: 100%;
|
|
--height: auto;
|
|
--min-width: auto;
|
|
--min-height: auto;
|
|
--body-padding: 1.5rem;
|
|
--backdrop-background: rgba(0, 0, 0, 0.6);
|
|
--border-radius: 0.8rem 0.8rem 0 0;
|
|
}
|
|
.popup-container{
|
|
display: -ms-grid;
|
|
display: grid;
|
|
position: fixed;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
place-items: center;
|
|
background: var(--backdrop-background);
|
|
-webkit-transition: opacity 0.3s;
|
|
-o-transition: opacity 0.3s;
|
|
transition: opacity 0.3s;
|
|
z-index: 10;
|
|
touch-action: none;
|
|
}
|
|
:host(.stacked) .popup{
|
|
-webkit-transform: scale(0.9) translateY(-2rem) !important;
|
|
transform: scale(0.9) translateY(-2rem) !important;
|
|
}
|
|
.popup{
|
|
display: -webkit-box;
|
|
display: -ms-flexbox;
|
|
display: flex;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
flex-direction: column;
|
|
position: relative;
|
|
-ms-flex-item-align: end;
|
|
align-self: flex-end;
|
|
-webkit-box-align: start;
|
|
-ms-flex-align: start;
|
|
align-items: flex-start;
|
|
width: var(--width);
|
|
min-width: var(--min-width);
|
|
height: var(--height);
|
|
min-height: var(--min-height);
|
|
max-height: 90vh;
|
|
border-radius: var(--border-radius);
|
|
-webkit-transform: scale(1) translateY(100%);
|
|
transform: scale(1) translateY(100%);
|
|
-webkit-transition: -webkit-transform 0.3s;
|
|
transition: -webkit-transform 0.3s;
|
|
-o-transition: transform 0.3s;
|
|
transition: transform 0.3s, -webkit-transform 0.3s;
|
|
transition: transform 0.3s;
|
|
background: rgba(var(--background-color), 1);
|
|
-webkit-box-shadow: 0 -1rem 2rem #00000020;
|
|
box-shadow: 0 -1rem 2rem #00000020;
|
|
content-visibility: auto;
|
|
}
|
|
.container-header{
|
|
display: -webkit-box;
|
|
display: flex;
|
|
width: 100%;
|
|
touch-action: none;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
}
|
|
.popup-top{
|
|
display: -webkit-box;
|
|
display: flex;
|
|
width: 100%;
|
|
}
|
|
.popup-body{
|
|
display: -webkit-box;
|
|
display: flex;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
-ms-flex-direction: column;
|
|
flex-direction: column;
|
|
-webkit-box-flex: 1;
|
|
-ms-flex: 1;
|
|
flex: 1;
|
|
width: 100%;
|
|
padding: var(--body-padding);
|
|
overflow-y: auto;
|
|
}
|
|
.hide{
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
visiblity: none;
|
|
}
|
|
@media screen and (min-width: 640px){
|
|
:host{
|
|
--border-radius: 0.4rem;
|
|
}
|
|
.popup{
|
|
-ms-flex-item-align: center;
|
|
-ms-grid-row-align: center;
|
|
align-self: center;
|
|
border-radius: var(--border-radius);
|
|
height: var(--height);
|
|
-webkit-transform: scale(1) translateY(3rem);
|
|
transform: scale(1) translateY(3rem);
|
|
-webkit-box-shadow: 0 3rem 2rem -0.5rem #00000040;
|
|
box-shadow: 0 3rem 2rem -0.5rem #00000040;
|
|
}
|
|
}
|
|
@media screen and (max-width: 640px){
|
|
.popup-top{
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
flex-direction: column;
|
|
-webkit-box-align: center;
|
|
align-items: center;
|
|
}
|
|
.handle{
|
|
height: 0.3rem;
|
|
width: 2rem;
|
|
background: rgba(var(--text-color), .4);
|
|
border-radius: 1rem;
|
|
margin: 0.5rem 0;
|
|
}
|
|
}
|
|
@media (any-hover: hover){
|
|
::-webkit-scrollbar{
|
|
width: 0.5rem;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb{
|
|
background: rgba(var(--text-color), 0.3);
|
|
border-radius: 1rem;
|
|
&:hover{
|
|
background: rgba(var(--text-color), 0.5);
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
<div part="background" class="popup-container hide" role="dialog">
|
|
<div part="popup" class="popup">
|
|
<div part="popup-header" class="popup-top">
|
|
<div class="handle"></div>
|
|
<slot name="header"></slot>
|
|
</div>
|
|
<div part="popup-body" class="popup-body">
|
|
<slot></slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
customElements.define('sm-popup', class extends HTMLElement {
|
|
constructor() {
|
|
super()
|
|
this.attachShadow({
|
|
mode: 'open'
|
|
}).append(smPopup.content.cloneNode(true))
|
|
|
|
this.allowClosing = false
|
|
this.isOpen = false
|
|
this.pinned = false
|
|
this.popupStack
|
|
this.offset
|
|
this.touchStartY = 0
|
|
this.touchEndY = 0
|
|
this.touchStartTime = 0
|
|
this.touchEndTime = 0
|
|
this.touchEndAnimataion
|
|
|
|
this.popupContainer = this.shadowRoot.querySelector('.popup-container')
|
|
this.popup = this.shadowRoot.querySelector('.popup')
|
|
this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot')
|
|
this.popupHeader = this.shadowRoot.querySelector('.popup-top')
|
|
|
|
this.resumeScrolling = this.resumeScrolling.bind(this)
|
|
this.show = this.show.bind(this)
|
|
this.hide = this.hide.bind(this)
|
|
this.handleTouchStart = this.handleTouchStart.bind(this)
|
|
this.handleTouchMove = this.handleTouchMove.bind(this)
|
|
this.handleTouchEnd = this.handleTouchEnd.bind(this)
|
|
this.movePopup = this.movePopup.bind(this)
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['open'];
|
|
}
|
|
|
|
get open() {
|
|
return this.isOpen
|
|
}
|
|
|
|
resumeScrolling() {
|
|
const scrollY = document.body.style.top;
|
|
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
|
setTimeout(() => {
|
|
document.body.style.overflow = 'auto';
|
|
document.body.style.top = 'initial'
|
|
}, 300);
|
|
}
|
|
|
|
show(options = {}) {
|
|
const {pinned = false, popupStack = undefined} = options
|
|
if (popupStack)
|
|
this.popupStack = popupStack
|
|
if (this.popupStack && !this.hasAttribute('open')) {
|
|
this.popupStack.push({
|
|
popup: this,
|
|
permission: pinned
|
|
})
|
|
if (this.popupStack.items.length > 1) {
|
|
this.popupStack.items[this.popupStack.items.length - 2].popup.classList.add('stacked')
|
|
}
|
|
this.dispatchEvent(
|
|
new CustomEvent("popupopened", {
|
|
bubbles: true,
|
|
detail: {
|
|
popup: this,
|
|
popupStack: this.popupStack
|
|
}
|
|
})
|
|
)
|
|
this.setAttribute('open', '')
|
|
this.pinned = pinned
|
|
this.isOpen = true
|
|
}
|
|
this.popupContainer.classList.remove('hide')
|
|
this.popup.style.transform = 'none';
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.top = `-${window.scrollY}px`
|
|
return this.popupStack
|
|
}
|
|
hide() {
|
|
if (window.innerWidth < 640)
|
|
this.popup.style.transform = 'translateY(100%)';
|
|
else
|
|
this.popup.style.transform = 'translateY(3rem)';
|
|
this.popupContainer.classList.add('hide')
|
|
this.removeAttribute('open')
|
|
if (typeof this.popupStack !== 'undefined') {
|
|
this.popupStack.pop()
|
|
if (this.popupStack.items.length) {
|
|
this.popupStack.items[this.popupStack.items.length - 1].popup.classList.remove('stacked')
|
|
} else {
|
|
this.resumeScrolling()
|
|
}
|
|
} else {
|
|
this.resumeScrolling()
|
|
}
|
|
|
|
if (this.forms.length) {
|
|
setTimeout(() => {
|
|
this.forms.forEach(form => form.reset())
|
|
}, 300);
|
|
}
|
|
setTimeout(() => {
|
|
this.dispatchEvent(
|
|
new CustomEvent("popupclosed", {
|
|
bubbles: true,
|
|
detail: {
|
|
popup: this,
|
|
popupStack: this.popupStack
|
|
}
|
|
})
|
|
)
|
|
this.isOpen = false
|
|
}, 300);
|
|
}
|
|
|
|
handleTouchStart(e) {
|
|
this.touchStartY = e.changedTouches[0].clientY
|
|
this.popup.style.transition = 'transform 0.1s'
|
|
this.touchStartTime = e.timeStamp
|
|
}
|
|
|
|
handleTouchMove(e) {
|
|
if (this.touchStartY < e.changedTouches[0].clientY) {
|
|
this.offset = e.changedTouches[0].clientY - this.touchStartY;
|
|
this.touchEndAnimataion = window.requestAnimationFrame(() => this.movePopup())
|
|
}
|
|
}
|
|
|
|
handleTouchEnd(e) {
|
|
this.touchEndTime = e.timeStamp
|
|
cancelAnimationFrame(this.touchEndAnimataion)
|
|
this.touchEndY = e.changedTouches[0].clientY
|
|
this.popup.style.transition = 'transform 0.3s'
|
|
this.threshold = this.popup.getBoundingClientRect().height * 0.3
|
|
if (this.touchEndTime - this.touchStartTime > 200) {
|
|
if (this.touchEndY - this.touchStartY > this.threshold) {
|
|
if (this.pinned) {
|
|
this.show()
|
|
return
|
|
} else
|
|
this.hide()
|
|
} else {
|
|
this.show()
|
|
}
|
|
} else {
|
|
if (this.touchEndY > this.touchStartY)
|
|
if (this.pinned) {
|
|
this.show()
|
|
return
|
|
}
|
|
else
|
|
this.hide()
|
|
}
|
|
}
|
|
|
|
movePopup() {
|
|
this.popup.style.transform = `translateY(${this.offset}px)`
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.popupBodySlot.addEventListener('slotchange', () => {
|
|
this.forms = this.querySelectorAll('sm-form')
|
|
})
|
|
this.popupContainer.addEventListener('mousedown', e => {
|
|
if (e.target === this.popupContainer && !this.pinned) {
|
|
if (this.pinned) {
|
|
this.show()
|
|
} else
|
|
this.hide()
|
|
}
|
|
})
|
|
|
|
const resizeObserver = new ResizeObserver(entries => {
|
|
for (let entry of entries) {
|
|
if (entry.contentBoxSize) {
|
|
// Firefox implements `contentBoxSize` as a single content rect, rather than an array
|
|
const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize;
|
|
this.threshold = contentBoxSize.blockSize.height * 0.3
|
|
} else {
|
|
this.threshold = entry.contentRect.height * 0.3
|
|
}
|
|
}
|
|
});
|
|
resizeObserver.observe(this)
|
|
|
|
|
|
this.popupHeader.addEventListener('touchstart', (e) => { this.handleTouchStart(e) }, { passive: true })
|
|
this.popupHeader.addEventListener('touchmove', (e) => { this.handleTouchMove(e) }, { passive: true })
|
|
this.popupHeader.addEventListener('touchend', (e) => { this.handleTouchEnd(e) }, { passive: true })
|
|
}
|
|
disconnectedCallback() {
|
|
this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true })
|
|
this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true })
|
|
this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true })
|
|
resizeObserver.unobserve()
|
|
}
|
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
if (name === 'open') {
|
|
if (this.hasAttribute('open')) {
|
|
this.show()
|
|
}
|
|
}
|
|
}
|
|
}) |