mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
fix(touch): refresh device hits and defer pinch zoom save (#613)
Issue: #613 User-Visible: yes
This commit is contained in:
@@ -21,6 +21,76 @@ export interface DeviceHitCandidate {
|
||||
floorRadius: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every target whose own scroll can move a card in client coordinates.
|
||||
*
|
||||
* `scroll` is neither bubbling nor composed. A listener on `document` cannot
|
||||
* see a Home Assistant dashboard scroller behind a shadow boundary, so walk
|
||||
* the rendered (slot/shadow-host) ancestry and listen to each target directly.
|
||||
*/
|
||||
export function deviceHitScrollSources(host: Node): EventTarget[] {
|
||||
const sources: EventTarget[] = [];
|
||||
const seen = new Set<EventTarget>();
|
||||
const add = (source: EventTarget | null | undefined): void => {
|
||||
if (!source || seen.has(source)) return;
|
||||
seen.add(source);
|
||||
sources.push(source);
|
||||
};
|
||||
|
||||
let current: Node | null = host;
|
||||
while (current) {
|
||||
if (current.nodeType === 1 || current.nodeType === 9) add(current);
|
||||
const assignedSlot = current.nodeType === 1
|
||||
? (current as Element).assignedSlot : null;
|
||||
if (assignedSlot) {
|
||||
current = assignedSlot;
|
||||
continue;
|
||||
}
|
||||
if (current.parentElement) {
|
||||
current = current.parentElement;
|
||||
continue;
|
||||
}
|
||||
const root = current.getRootNode();
|
||||
const shadowHost = root.nodeType === 11 && 'host' in root
|
||||
? (root as ShadowRoot).host : null;
|
||||
if (shadowHost) {
|
||||
current = shadowHost;
|
||||
continue;
|
||||
}
|
||||
const document = current.ownerDocument;
|
||||
current = document && current !== document ? document : null;
|
||||
}
|
||||
|
||||
const document = host.nodeType === 9 ? host as Document : host.ownerDocument;
|
||||
add(document?.defaultView);
|
||||
return sources;
|
||||
}
|
||||
|
||||
/** Subscribe to client-coordinate shifts and return an idempotent teardown. */
|
||||
export function observeDeviceHitGeometryScroll(
|
||||
host: Node,
|
||||
invalidate: () => void,
|
||||
): () => void {
|
||||
const sources = deviceHitScrollSources(host);
|
||||
const document = host.nodeType === 9 ? host as Document : host.ownerDocument;
|
||||
const viewport = document?.defaultView?.visualViewport;
|
||||
const handler = (): void => invalidate();
|
||||
for (const source of sources) {
|
||||
source.addEventListener('scroll', handler, { passive: true });
|
||||
}
|
||||
viewport?.addEventListener('scroll', handler, { passive: true });
|
||||
viewport?.addEventListener('resize', handler, { passive: true });
|
||||
|
||||
let active = true;
|
||||
return (): void => {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
for (const source of sources) source.removeEventListener('scroll', handler);
|
||||
viewport?.removeEventListener('scroll', handler);
|
||||
viewport?.removeEventListener('resize', handler);
|
||||
};
|
||||
}
|
||||
|
||||
const EPSILON = 1e-7;
|
||||
|
||||
const distanceSquared = (a: DeviceHitPoint, b: DeviceHitPoint): number => {
|
||||
|
||||
+23
-13
@@ -108,7 +108,7 @@ import {
|
||||
type FixedFloorSelection, type InitialSpaceSelection,
|
||||
} from './initial-load';
|
||||
import { TouchGestureClickGuard } from './touch-gesture-click-guard';
|
||||
import { DeviceHitController } from './device-hit-owner';
|
||||
import { DeviceHitController, observeDeviceHitGeometryScroll } from './device-hit-owner';
|
||||
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
|
||||
import {
|
||||
createEmptySpaceConfig, initialSpaceDisplayDraft, roomTempRangeFromDraft, switchSpacePlanSource, touchSpaceDisplay,
|
||||
@@ -2048,7 +2048,7 @@ export class HouseplanCard extends LitElement {
|
||||
private _roomFocus: { spaceId: string; roomId: string } | null = null;
|
||||
/** Pointer owner captured from the actually painted event path. */
|
||||
private _roomPointer: RoomFitGestureCandidate | null = null; private readonly _doubleFit = new DoubleFitGestureRecognizer();
|
||||
private readonly _deviceHits = new DeviceHitController();
|
||||
private readonly _deviceHits = new DeviceHitController(); private _deviceHitScrollUnsub?: () => void;
|
||||
private _pointers = new Map<number, { x: number; y: number }>();
|
||||
private _panStart: { sx: number; sy: number; vx: number; vy: number } | null = null;
|
||||
/**
|
||||
@@ -2061,7 +2061,7 @@ export class HouseplanCard extends LitElement {
|
||||
private _pinchStart: { dist: number; zoom: number } | null = null;
|
||||
private _safeDayCycleOutline = false;
|
||||
private _suppressClick = false;
|
||||
private _viewportGestureDirty = false;
|
||||
private _viewportGestureDirty = false; private _pinchZoomDirty = false;
|
||||
private _activateSafeDayCycleOutline(): void { this._safeDayCycleOutline = true; this._stageEl?.classList.add('hp-safe-daycycle-outline'); }
|
||||
/**
|
||||
* Pointer events from an interactive child may stop before the stage sees
|
||||
@@ -2695,6 +2695,8 @@ export class HouseplanCard extends LitElement {
|
||||
this._summarySlot.connect();
|
||||
void this._ensureLiveRuntime().catch(() => this.requestUpdate());
|
||||
this._pointerModality.connect(this.ownerDocument.defaultView);
|
||||
this._deviceHitScrollUnsub?.();
|
||||
this._deviceHitScrollUnsub = observeDeviceHitGeometryScroll(this, () => this._invalidateDeviceHitGeometry());
|
||||
const PointerHoverObserver = this.ownerDocument.defaultView?.MutationObserver;
|
||||
if (PointerHoverObserver) {
|
||||
this._pointerHoverObserver = new PointerHoverObserver((records) => {
|
||||
@@ -2803,6 +2805,7 @@ export class HouseplanCard extends LitElement {
|
||||
this._languageFailureUnsub = undefined;
|
||||
this._motionMedia?.removeEventListener?.('change', this._onMotionChange);
|
||||
this._motionMedia = undefined;
|
||||
this._deviceHitScrollUnsub?.(); this._deviceHitScrollUnsub = undefined; this._pinchZoomDirty = false;
|
||||
if (this._vacRaf) { cancelAnimationFrame(this._vacRaf); this._vacRaf = 0; }
|
||||
if (this._refitRaf) { cancelAnimationFrame(this._refitRaf); this._refitRaf = 0; }
|
||||
this._warmModeRequest = 0;
|
||||
@@ -6789,6 +6792,17 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _markPinchZoomDirty(): void { this._pinchZoomDirty = true; }
|
||||
|
||||
/** Both capture and stage see terminal events; clearing first makes this idempotent. */
|
||||
private _finishViewportGesture(): void {
|
||||
const saveZoom = this._pinchZoomDirty; this._pinchZoomDirty = false;
|
||||
if (saveZoom) this._saveZoom();
|
||||
if (!this._viewportGestureDirty) return;
|
||||
this._viewportGestureDirty = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** Restore the saved space zoom and center the plan. */
|
||||
private _restoreZoom(): void {
|
||||
this._clearRoomFocus(true);
|
||||
@@ -6964,8 +6978,8 @@ export class HouseplanCard extends LitElement {
|
||||
const cy = (pts[0].y + pts[1].y) / 2 - r.top;
|
||||
this._zoomAt(cx, cy, this._pinchStart.zoom * scale);
|
||||
this._viewportGestureDirty = true;
|
||||
this._markPinchZoomDirty();
|
||||
this._suppressClick = true;
|
||||
this._saveZoom();
|
||||
} else if (this._panStart) {
|
||||
const ddx = ev.clientX - this._panStart.sx;
|
||||
const ddy = ev.clientY - this._panStart.sy;
|
||||
@@ -7104,10 +7118,7 @@ export class HouseplanCard extends LitElement {
|
||||
// reset click suppression on the next tick (so that a click right after a pan does not fire)
|
||||
if (this._suppressClick) setTimeout(() => (this._suppressClick = false), 0);
|
||||
}
|
||||
if (this._viewportGestureDirty && this._pointers.size === 0 && !acceptedRoom) {
|
||||
this._viewportGestureDirty = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
if (this._pointers.size === 0 && !acceptedRoom) this._finishViewportGesture();
|
||||
if (acceptedRoom) this._fitRoom(acceptedRoom);
|
||||
}
|
||||
|
||||
@@ -7385,7 +7396,8 @@ export class HouseplanCard extends LitElement {
|
||||
const rect = this._stageEl.getBoundingClientRect();
|
||||
this._zoomAt((a.x + b.x) / 2 - rect.left, (a.y + b.y) / 2 - rect.top,
|
||||
this._pinchStart.zoom * scale);
|
||||
this._saveZoom();
|
||||
this._viewportGestureDirty = true;
|
||||
this._markPinchZoomDirty();
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -7408,6 +7420,7 @@ export class HouseplanCard extends LitElement {
|
||||
if (this._pointers.size === 0) {
|
||||
this._panStart = null;
|
||||
this._panLock = null;
|
||||
this._finishViewportGesture();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7974,10 +7987,7 @@ export class HouseplanCard extends LitElement {
|
||||
this._panLock = null;
|
||||
this._swipeStart = null;
|
||||
}
|
||||
if (this._viewportGestureDirty && this._pointers.size === 0) {
|
||||
this._viewportGestureDirty = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
if (this._pointers.size === 0) this._finishViewportGesture();
|
||||
}
|
||||
|
||||
private _applyGeometryState(
|
||||
|
||||
Reference in New Issue
Block a user