import TemplateControl from "../../core/TemplateControl.js"; import { XNode } from "../../core/XNode.js"; const findLargestDistance = (touches: TouchList) => { let pair = null as { t1: Touch, t2: Touch, distance: number}; for (let i = 0; i < touches.length; i++) { const t1 = touches[i]; for (let j = i + 1; j < touches.length; j++) { const t2 = touches[j]; const distance = Math.hypot(t1.pageX - t2.pageX, t1.pageY - t2.pageY); if (pair === null) { pair = { t1, t2, distance }; continue; } if (pair.distance < distance) { pair = { t1, t2, distance }; } } } return pair; }; const center = (ev: TouchEvent) => { const touch = ev.touches[0]; if (touch) { return { x: touch.clientX, y: touch.clientY } } return { x: 0, y: 0 }; } export interface IZoom { anchorX: number; anchorY: number; x: number; y: number; scale: number; } export default class FullScreenImageGallery extends TemplateControl { container: HTMLElement; last: HTMLElement; selected: HTMLElement; async prepare() { const slot = this.attachShadow({ mode: "open", delegatesFocus: true, slotAssignment: "manual" }); XNode.append(slot,
this.move(-1)}/>
this.move(1)}/>
this.closeSelf()}/>
); this.container = slot.querySelector(`[part="content"]`); let mouseMoveDisposable: Disposable; let mouseUpDisposable: Disposable; const scrollView = this.container; scrollView.addEventListener("dragstart", (ev: DragEvent) => { ev.preventDefault(); ev.stopImmediatePropagation(); }); scrollView.addEventListener("wheel", (e: WheelEvent) => { const slotted = this.getSlottedElement(e.target as HTMLElement); if (!slotted) { return; } e.preventDefault(); e.stopImmediatePropagation(); const diff = e.deltaY < 0 ? 50 : -50; this.updateZoom(slotted, diff, e.clientX, e.clientY); }); scrollView.addEventListener("scroll", () => { if (this.last) { this.clearScale(this.last); } } , { passive: true } ); let previous: {x: number, y: number}; let touchMoveDisposable; let touchEndDisposable; let scale = 0; let previousDistance = undefined; scrollView.addEventListener("touchstart", (evs: TouchEvent) => { const slottedElement = this.getSlottedElement(evs.target as HTMLElement); if (!slottedElement) { return; } if (evs.touches.length < 2) { return; } previous = center(evs); evs.preventDefault(); evs.stopImmediatePropagation(); touchMoveDisposable ??= this.bindEvent(scrollView, "touchmove", (ev: TouchEvent) => { if (ev.touches.length < 2) { return; } ev.preventDefault(); ev.stopImmediatePropagation(); const ld = findLargestDistance(ev.touches); const first = ld.t1; const second = ld.t2; const newScale = ld.distance; if (previousDistance === void 0) { previousDistance = newScale; return; } if (previousDistance === newScale) { return; } const anchorX = ((first.clientX + second.clientX) / 2); // - rect.left; const anchorY = ((first.clientY + second.clientY) / 2); // - rect.top; scale = newScale - previousDistance; previousDistance = newScale; this.updateZoom(slottedElement, scale, anchorX, anchorY); }); touchEndDisposable ??= this.bindEvent(scrollView, "touchend", (ev: TouchEvent) => { if (ev.touches.length <2) { return; } ev.preventDefault(); ev.stopImmediatePropagation?.(); touchMoveDisposable?.dispose(); touchEndDisposable?.dispose(); touchMoveDisposable = undefined; touchEndDisposable = undefined; previousDistance = undefined; }); }); scrollView.addEventListener("mousedown", (de: MouseEvent) => { const slottedElement = this.getSlottedElement(de.target as HTMLElement); if (!slottedElement) { return; } slottedElement.setAttribute("data-state", "grabbing"); previous = { x: de.clientX, y: de.clientY }; mouseMoveDisposable ??= this.bindEvent(scrollView, "mousemove", (e: MouseEvent) => { e.preventDefault(); e.stopImmediatePropagation(); const cp = { x: e.clientX, y: e.clientY }; const diffX = previous.x - cp.x; const diffY = previous.y - cp.y; previous = cp; slottedElement.assignedSlot.scrollBy(diffX, diffY); }); mouseUpDisposable ??= this.bindEvent(scrollView, "mouseup", (e: MouseEvent) => { e.preventDefault(); e.stopImmediatePropagation(); slottedElement.removeAttribute("data-state"); mouseMoveDisposable[Symbol.dispose](); mouseUpDisposable[Symbol.dispose](); mouseMoveDisposable = null; mouseUpDisposable = null; }); }); } getSlottedElement(target: HTMLElement): HTMLElement { while(target && !target.assignedSlot) { target = target.parentElement; } if (target.assignedSlot) { return target; } return null; } closeSelf() { const { last } = this; if (last) { this.clearScale(last); return; } this.remove(); } move(n: number) { const { last, container } = this; if (last) { this.clearScale(last); } container.scrollBy({ left: n * container.offsetWidth, behavior: "smooth" }); } async onFirstPrepare() { if (!this.selected) { return; } this.selected.scrollIntoView({ block: "nearest", inline: "center", behavior: "instant" }) this.selected = null; } onChildAdded(child: HTMLElement) { const slot = XNode.append(this.container, ) as HTMLSlotElement; slot.assign(child); // child.onwheel = (e: WheelEvent) => { // const target = e.target as HTMLElement; // const diff = e.deltaY < 0 ? 50 : -50; // e.preventDefault(); // e.stopImmediatePropagation(); // this.updateZoom(target, diff, e.offsetX, e.offsetY ) // }; } onChildRemoved(child: Node) { } private updateZoom(target: HTMLElement, scaleDiff, anchorX, anchorY ) { this.container.style.scrollSnapType = "none"; this.last = target; const { clientWidth } = this; let scale = parseInt(target.getAttribute("last-scale") || "0", 10) + scaleDiff; if (scale <= 0) { scale = 0; } target.setAttribute("last-scale", scale); if (scale === 0) { this.clearScale(target); return; } const { assignedSlot } = target; // target.assignedSlot.style.position = "absolute"; assignedSlot.style.overflow = "auto"; assignedSlot.style.scrollSnapAlign = "unset"; assignedSlot.style.scrollSnapStop = "unset"; assignedSlot.style.scrollSnapType = "unset"; this.container.style.scrollSnapType = "unset"; // this.container.style.gridColumn = "1 / span 3"; target.style.scrollSnapAlign = "unset"; target.style.scrollSnapStop = "unset"; target.style.scrollSnapType = "unset"; // target.assignedSlot.style.zIndex = "1000"; const scaleFactor = (clientWidth + scale) / clientWidth; // /** old one working in desktop */ // const e = target.getBoundingClientRect(); // target.style.width = scaleFactor * 100 + "%"; // target.style.height = scaleFactor * 100 + "%"; // const moved = Rectangle.from({ // x: 0, // y: 0, // width: e.width, // height: e.height // }) // .move(assignedSlot.scrollLeft,assignedSlot.scrollTop) // .scale(scaleFactor, anchorX, anchorY); // assignedSlot.scrollLeft -= moved.left; // assignedSlot.scrollTop -= moved.top; target.style.transformOrigin = `${anchorX}px ${anchorY}px`; target.style.transform = `scale(${scaleFactor})`; const e = target.getBoundingClientRect(); // console.log([e.left, e.top]); const left = -e.left; const top = -e.top; target.style.transformOrigin = "0 0"; const de = target.getBoundingClientRect(); // console.log([de.left, de.top]); assignedSlot.scrollTo({ left: left + de.left, top: top + de.top , behavior: "instant" }); } private clearScale(target: HTMLElement) { const { assignedSlot: { style: slotStyle } } = target; this.container.style.removeProperty("scroll-snap-type"); // this.container.style.removeProperty("grid-column"); slotStyle.removeProperty("overflow"); slotStyle.removeProperty("scroll-snap-align"); slotStyle.removeProperty("scroll-snap-stop"); slotStyle.removeProperty("scroll-snap-type"); target.removeAttribute("last-scale"); target.style.removeProperty("scroll-snap-align"); target.style.removeProperty("scroll-snap-stop"); target.style.removeProperty("scroll-snap-type"); target.style.removeProperty("width"); target.style.removeProperty("height"); // target.assignedSlot.style.removeProperty("position"); // target.assignedSlot.style.removeProperty("z-index"); target.style.transform = ""; this.last = null; } } customElements.define("full-screen-image-gallery", FullScreenImageGallery);