From dc681a764e9f60f28e81188cdef5a56146dc3579 Mon Sep 17 00:00:00 2001 From: mistic100 Date: Sat, 7 Sep 2024 16:03:06 +0200 Subject: [PATCH] Fix #1412 compatibility with WebComponents --- packages/core/src/Viewer.ts | 2 ++ packages/core/src/buttons/MenuButton.ts | 2 +- packages/core/src/components/Panel.ts | 6 ++--- packages/core/src/model.ts | 6 ++++- packages/core/src/services/EventsHandler.ts | 25 ++++++++++--------- packages/core/src/utils/browser.ts | 25 ++++++++++++++++++- packages/core/src/utils/psv.ts | 13 ++++++++++ .../gallery-plugin/src/GalleryComponent.ts | 2 +- .../map-plugin/src/components/MapComponent.ts | 6 ++--- .../src/components/MapZoomToolbar.ts | 2 +- packages/markers-plugin/src/MarkersPlugin.ts | 22 ++++++++++------ .../overlays-plugin/src/OverlaysPlugin.ts | 3 +++ .../src/components/PlanComponent.ts | 2 +- .../settings-plugin/src/SettingsComponent.ts | 8 +++--- .../virtual-tour-plugin/src/ArrowsRenderer.ts | 13 ++++++---- 15 files changed, 96 insertions(+), 41 deletions(-) diff --git a/packages/core/src/Viewer.ts b/packages/core/src/Viewer.ts index 63f76a604..8347716ca 100644 --- a/packages/core/src/Viewer.ts +++ b/packages/core/src/Viewer.ts @@ -47,6 +47,7 @@ import { ViewerDynamics } from './services/ViewerDynamics'; import { ViewerState } from './services/ViewerState'; import { Animation, + checkClosedShadowDom, checkStylesheet, exitFullscreen, getAbortError, @@ -118,6 +119,7 @@ export class Viewer extends TypedEventTarget { this.container.classList.add('psv-container'); this.parent.appendChild(this.container); + checkClosedShadowDom(this.parent); checkStylesheet(this.container, 'core'); this.state = new ViewerState(); diff --git a/packages/core/src/buttons/MenuButton.ts b/packages/core/src/buttons/MenuButton.ts index 819566715..9642f1a51 100644 --- a/packages/core/src/buttons/MenuButton.ts +++ b/packages/core/src/buttons/MenuButton.ts @@ -84,7 +84,7 @@ export class MenuButton extends AbstractButton { content: MENU_TEMPLATE(this.viewer.navbar.collapsed, this.viewer.config.lang.menu), noMargin: true, clickHandler: (target) => { - const li = target ? getClosest(target as HTMLElement, 'li') : undefined; + const li = target ? getClosest(target as HTMLElement, '.psv-panel-menu-item') : undefined; const buttonId = li ? li.dataset[BUTTON_DATA] : undefined; if (buttonId) { diff --git a/packages/core/src/components/Panel.ts b/packages/core/src/components/Panel.ts index dfc58f659..971d17513 100644 --- a/packages/core/src/components/Panel.ts +++ b/packages/core/src/components/Panel.ts @@ -1,6 +1,6 @@ import { CAPTURE_EVENTS_CLASS, ICONS, KEY_CODES } from '../data/constants'; import { PSVError } from '../PSVError'; -import { toggleClass } from '../utils'; +import { getEventTarget, toggleClass } from '../utils'; import type { Viewer } from '../Viewer'; import { HidePanelEvent, KeypressEvent, ShowPanelEvent } from '../events'; import { AbstractComponent } from './AbstractComponent'; @@ -174,11 +174,11 @@ export class Panel extends AbstractComponent { if (config.clickHandler) { this.state.clickHandler = (e) => { - (config as PanelConfig).clickHandler(e.target as HTMLElement); + (config as PanelConfig).clickHandler(getEventTarget(e)); }; this.state.keyHandler = (e) => { if (e.key === KEY_CODES.Enter) { - (config as PanelConfig).clickHandler(e.target as HTMLElement); + (config as PanelConfig).clickHandler(getEventTarget(e)); } }; this.content.addEventListener('click', this.state.clickHandler); diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 8f448031c..639a86131 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -233,7 +233,11 @@ export type ClickData = { /** * Original element which received the click */ - target: HTMLElement; + target?: HTMLElement; + /** + * Original event which triggered the click + */ + originalEvent?: Event; /** * List of THREE scenes objects under the mouse */ diff --git a/packages/core/src/services/EventsHandler.ts b/packages/core/src/services/EventsHandler.ts index 1d547926a..4c4f79513 100644 --- a/packages/core/src/services/EventsHandler.ts +++ b/packages/core/src/services/EventsHandler.ts @@ -30,12 +30,12 @@ import { Animation, clone, distance, - getClosest, + getEventTarget, + getMatchingTarget, getPosition, getTouchData, - hasParent, isEmpty, - throttle, + throttle } from '../utils'; import { PressHandler } from '../utils/PressHandler'; import type { Viewer } from '../Viewer'; @@ -156,7 +156,7 @@ export class EventsHandler extends AbstractService { case 'fullscreenchange': this.__onFullscreenChange(); break; } - if (!getClosest(evt.target as HTMLElement, '.' + CAPTURE_EVENTS_CLASS)) { + if (!getMatchingTarget(evt, '.' + CAPTURE_EVENTS_CLASS)) { // prettier-ignore switch (evt.type) { case 'mousedown': this.__onMouseDown(evt as MouseEvent); break; @@ -243,7 +243,7 @@ export class EventsHandler extends AbstractService { */ private __onMouseUp(evt: MouseEvent) { if (this.step.is(Step.CLICK, Step.MOVING)) { - this.__stopMove(evt.clientX, evt.clientY, evt.target, evt.button === 2); + this.__stopMove(evt.clientX, evt.clientY, evt, evt.button === 2); } } @@ -271,7 +271,7 @@ export class EventsHandler extends AbstractService { if (!this.data.longtouchTimeout) { this.data.longtouchTimeout = setTimeout(() => { const touch = evt.touches[0]; - this.__stopMove(touch.clientX, touch.clientY, touch.target, true); + this.__stopMove(touch.clientX, touch.clientY, evt, true); this.data.longtouchTimeout = null; }, LONGTOUCH_DELAY); } @@ -301,7 +301,7 @@ export class EventsHandler extends AbstractService { this.__stopMove(this.data.mouseX, this.data.mouseY); } else if (evt.touches.length === 0) { const touch = evt.changedTouches[0]; - this.__stopMove(touch.clientX, touch.clientY, touch.target); + this.__stopMove(touch.clientX, touch.clientY, evt); } } } @@ -442,7 +442,7 @@ export class EventsHandler extends AbstractService { * Stops the movement * @description If the move threshold was not reached a click event is triggered, otherwise an animation is launched to simulate inertia */ - private __stopMove(clientX: number, clientY: number, target?: EventTarget, rightclick = false) { + private __stopMove(clientX: number, clientY: number, event?: Event, rightclick = false) { if (this.step.is(Step.MOVING)) { if (this.config.moveInertia) { this.__logMouseMove(clientX, clientY); @@ -453,7 +453,7 @@ export class EventsHandler extends AbstractService { } } else { if (this.step.is(Step.CLICK) && !this.__moveThresholdReached(clientX, clientY)) { - this.__doClick(clientX, clientY, target, rightclick); + this.__doClick(clientX, clientY, event, rightclick); } this.step.remove(Step.CLICK); if (!this.step.is(Step.INERTIA)) { @@ -518,7 +518,7 @@ export class EventsHandler extends AbstractService { /** * Triggers an event with all coordinates when a simple click is performed */ - private __doClick(clientX: number, clientY: number, target: EventTarget, rightclick = false) { + private __doClick(clientX: number, clientY: number, event?: Event, rightclick = false) { const boundingRect = this.viewer.container.getBoundingClientRect(); const viewerX = clientX - boundingRect.left; @@ -532,7 +532,8 @@ export class EventsHandler extends AbstractService { const data: ClickData = { rightclick: rightclick, - target: target as HTMLElement, + originalEvent: event, + target: getEventTarget(event), clientX, clientY, viewerX, @@ -576,7 +577,7 @@ export class EventsHandler extends AbstractService { * Trigger events for observed THREE objects */ private __handleObjectsEvents(evt: MouseEvent) { - if (!isEmpty(this.state.objectsObservers) && hasParent(evt.target as HTMLElement, this.viewer.container)) { + if (!isEmpty(this.state.objectsObservers) && evt.composedPath().includes(this.viewer.container)) { const viewerPos = getPosition(this.viewer.container); const viewerPoint: Point = { diff --git a/packages/core/src/utils/browser.ts b/packages/core/src/utils/browser.ts index c264ff26e..388d9d571 100644 --- a/packages/core/src/utils/browser.ts +++ b/packages/core/src/utils/browser.ts @@ -56,7 +56,7 @@ export function hasParent(el: HTMLElement, parent: Element): boolean { } /** - * Gets the closest parent (can by itself) + * Gets the closest parent matching the selector (can by itself) */ export function getClosest(el: HTMLElement, selector: string): HTMLElement | null { // When el is document or window, the matches does not exist @@ -76,6 +76,29 @@ export function getClosest(el: HTMLElement, selector: string): HTMLElement | nul return null; } +/** + * Returns the first element of the event' composedPath + */ +export function getEventTarget(e: Event): HTMLElement | null { + return e?.composedPath()[0] as HTMLElement || null; +} + +/** + * Returns the first element of the event's composedPath matching the selector + */ +export function getMatchingTarget(e: Event, selector: string): HTMLElement | null { + if (!e) { + return null; + } + return e.composedPath().find((el) => { + if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) { + return false; + } + + return el.matches(selector); + }) as HTMLElement; +} + /** * Gets the position of an element in the viewport without reflow * Will gives the same result as getBoundingClientRect() as soon as there are no CSS transforms diff --git a/packages/core/src/utils/psv.ts b/packages/core/src/utils/psv.ts index 3b95e6037..8d8688949 100644 --- a/packages/core/src/utils/psv.ts +++ b/packages/core/src/utils/psv.ts @@ -446,3 +446,16 @@ export function checkVersion(name: string, version: string, coreVersion: string) console.error(`PhotoSphereViewer: @photo-sphere-viewer/${name} is in version ${version} but @photo-sphere-viewer/core is in version ${coreVersion}`); } } + +/** + * Checks if the viewer is not used insude a closed shadow DOM + */ +export function checkClosedShadowDom(el: Node) { + do { + if (el instanceof ShadowRoot && el.mode === 'closed') { + console.error(`PhotoSphereViewer: closed shadow DOM detected, the viewer might not work as expected`); + return; + } + el = el.parentNode; + } while (el); +} diff --git a/packages/gallery-plugin/src/GalleryComponent.ts b/packages/gallery-plugin/src/GalleryComponent.ts index 88c12a1c9..9bd09d0c0 100644 --- a/packages/gallery-plugin/src/GalleryComponent.ts +++ b/packages/gallery-plugin/src/GalleryComponent.ts @@ -129,7 +129,7 @@ export class GalleryComponent extends AbstractComponent { // prevent click on drag const currentMouse = this.isAboveBreakpoint ? (e as MouseEvent).clientX : (e as MouseEvent).clientY; if (Math.abs(this.state.initMouse - currentMouse) < 10) { - const item = utils.getClosest(e.target as HTMLElement, `[data-${GALLERY_ITEM_DATA_KEY}]`); + const item = utils.getMatchingTarget(e, `.psv-gallery-item`); if (item) { this.plugin.__click(item.dataset[GALLERY_ITEM_DATA]); } diff --git a/packages/map-plugin/src/components/MapComponent.ts b/packages/map-plugin/src/components/MapComponent.ts index 924f90c26..cbe614a32 100644 --- a/packages/map-plugin/src/components/MapComponent.ts +++ b/packages/map-plugin/src/components/MapComponent.ts @@ -173,7 +173,7 @@ export class MapComponent extends AbstractComponent { } handleEvent(e: Event) { - if (utils.getClosest(e.target as HTMLElement, `.${CONSTANTS.CAPTURE_EVENTS_CLASS}:not(.psv-map)`)) { + if (utils.getMatchingTarget(e, `.${CONSTANTS.CAPTURE_EVENTS_CLASS}:not(.psv-map)`)) { return; } switch (e.type) { @@ -213,7 +213,7 @@ export class MapComponent extends AbstractComponent { if (this.state.mousedown) { this.__move(event.clientX, event.clientY); e.stopPropagation(); - } else if (e.target === this.canvas) { + } else if (e.composedPath().includes(this.canvas)) { this.__handleHotspots(event.clientX, event.clientY); } break; @@ -246,7 +246,7 @@ export class MapComponent extends AbstractComponent { this.state.mousedown = false; e.stopPropagation(); } - if (e.target === this.canvas) { + if (e.composedPath().includes(this.canvas)) { this.__clickHotspot(mouse.clientX, mouse.clientY); } break; diff --git a/packages/map-plugin/src/components/MapZoomToolbar.ts b/packages/map-plugin/src/components/MapZoomToolbar.ts index 5e3dda56c..3a8644a16 100644 --- a/packages/map-plugin/src/components/MapZoomToolbar.ts +++ b/packages/map-plugin/src/components/MapZoomToolbar.ts @@ -39,7 +39,7 @@ export class MapZoomToolbar extends AbstractComponent { switch (e.type) { case 'mousedown': case 'touchstart': { - const button = utils.getClosest(e.target as HTMLElement, 'svg'); + const button = utils.getMatchingTarget(e, 'svg[data-delta]'); const delta: string = button?.dataset['delta']; if (delta) { cancelAnimationFrame(this.animation); diff --git a/packages/markers-plugin/src/MarkersPlugin.ts b/packages/markers-plugin/src/MarkersPlugin.ts index 738d416da..3d0b24e3f 100644 --- a/packages/markers-plugin/src/MarkersPlugin.ts +++ b/packages/markers-plugin/src/MarkersPlugin.ts @@ -235,17 +235,23 @@ export class MarkersPlugin extends AbstractConfigurablePlugin< } break; - case 'mouseenter': - this.__onEnterMarker(e as MouseEvent, this.__getTargetMarker(e.target as HTMLElement)); + case 'mouseenter': { + const marker = this.__getTargetMarker(utils.getEventTarget(e)); + this.__onEnterMarker(e as MouseEvent, marker); break; + } - case 'mouseleave': - this.__onLeaveMarker(this.__getTargetMarker(e.target as HTMLElement)); + case 'mouseleave': { + const marker = this.__getTargetMarker(utils.getEventTarget(e)); + this.__onLeaveMarker(marker); break; + } - case 'mousemove': - this.__onHoverMarker(e as MouseEvent, this.__getTargetMarker(e.target as HTMLElement, true)); + case 'mousemove': { + const marker = this.__getTargetMarker(utils.getEventTarget(e), true); + this.__onHoverMarker(e as MouseEvent, marker); break; + } case 'contextmenu': e.preventDefault(); @@ -602,7 +608,7 @@ export class MarkersPlugin extends AbstractConfigurablePlugin< content: MARKERS_LIST_TEMPLATE(markers, this.viewer.config.lang[MarkersButton.id]), noMargin: true, clickHandler: (target) => { - const li = utils.getClosest(target, 'li'); + const li = utils.getClosest(target, '.psv-panel-menu-item'); const markerId = li ? li.dataset[MARKER_DATA] : undefined; if (markerId) { @@ -797,7 +803,7 @@ export class MarkersPlugin extends AbstractConfigurablePlugin< } // the marker could have been deleted in an event handler - if (this.markers[marker.id]) { + if (this.markers[marker.id] && !e.data.rightclick) { if (marker.config.tooltip?.trigger === 'click') { if (marker.tooltip) { this.hideMarkerTooltip(marker.id); diff --git a/packages/overlays-plugin/src/OverlaysPlugin.ts b/packages/overlays-plugin/src/OverlaysPlugin.ts index f19457015..d85dcda48 100644 --- a/packages/overlays-plugin/src/OverlaysPlugin.ts +++ b/packages/overlays-plugin/src/OverlaysPlugin.ts @@ -96,6 +96,9 @@ export class OverlaysPlugin extends AbstractConfigurablePlugin< this.clearOverlays(); } } else if (e instanceof events.ClickEvent) { + if (e.data.rightclick) { + return false; + } const overlay = e.data.objects .map((o) => o.userData[OVERLAY_DATA] as ParsedOverlayConfig['id']) .filter((o) => !!o) diff --git a/packages/plan-plugin/src/components/PlanComponent.ts b/packages/plan-plugin/src/components/PlanComponent.ts index dacf41900..90fc7cbd0 100644 --- a/packages/plan-plugin/src/components/PlanComponent.ts +++ b/packages/plan-plugin/src/components/PlanComponent.ts @@ -165,7 +165,7 @@ export class PlanComponent extends AbstractComponent { } handleEvent(e: Event) { - if (utils.getClosest(e.target as HTMLElement, `.${CONSTANTS.CAPTURE_EVENTS_CLASS}:not(.psv-plan)`)) { + if (utils.getMatchingTarget(e, `.${CONSTANTS.CAPTURE_EVENTS_CLASS}:not(.psv-plan)`)) { return; } switch (e.type) { diff --git a/packages/settings-plugin/src/SettingsComponent.ts b/packages/settings-plugin/src/SettingsComponent.ts index 475ada8bb..1afbc9db1 100644 --- a/packages/settings-plugin/src/SettingsComponent.ts +++ b/packages/settings-plugin/src/SettingsComponent.ts @@ -23,7 +23,7 @@ export class SettingsComponent extends AbstractComponent { handleEvent(e: Event) { switch (e.type) { case 'click': - this.__click(e.target as HTMLElement); + this.__click(e); break; case 'transitionend': @@ -41,7 +41,7 @@ export class SettingsComponent extends AbstractComponent { this.plugin.hideSettings(); break; case CONSTANTS.KEY_CODES.Enter: - this.__click(e.target as HTMLElement); + this.__click(e); break; } } @@ -87,8 +87,8 @@ export class SettingsComponent extends AbstractComponent { /** * Handle clicks on items */ - private __click(element: HTMLElement) { - const li = utils.getClosest(element, 'li'); + private __click(e: Event) { + const li = utils.getMatchingTarget(e, '.psv-settings-item'); if (!li) { return; } diff --git a/packages/virtual-tour-plugin/src/ArrowsRenderer.ts b/packages/virtual-tour-plugin/src/ArrowsRenderer.ts index a1468fa32..bf9f1f55b 100644 --- a/packages/virtual-tour-plugin/src/ArrowsRenderer.ts +++ b/packages/virtual-tour-plugin/src/ArrowsRenderer.ts @@ -96,28 +96,31 @@ export class ArrowsRenderer extends AbstractComponent { this.render() break; case events.ClickEvent.type: { - const link = this.getTargetLink((e as events.ClickEvent).data.target, true); + if ((e as events.ClickEvent).data.rightclick) { + break; + } + const link = this.__getTargetLink((e as events.ClickEvent).data.target, true); if (link) { this.plugin.setCurrentNode(link.nodeId, null, link); } break; } case 'mouseenter': { - const link = this.getTargetLink(e.target as HTMLElement); + const link = this.__getTargetLink(utils.getEventTarget(e)); if (link) { this.plugin.__onEnterArrow(link, e as MouseEvent); } break; } case 'mouseleave': { - const link = this.getTargetLink(e.target as HTMLElement); + const link = this.__getTargetLink(utils.getEventTarget(e)); if (link) { this.plugin.__onLeaveArrow(link); } break; } case 'mousemove': { - const link = this.getTargetLink(e.target as HTMLElement, true); + const link = this.__getTargetLink(utils.getEventTarget(e), true); if (link) { this.plugin.__onHoverArrow(e as MouseEvent); } @@ -262,7 +265,7 @@ export class ArrowsRenderer extends AbstractComponent { } } - private getTargetLink(target: HTMLElement, closest = false): VirtualTourLink { + private __getTargetLink(target: HTMLElement, closest = false): VirtualTourLink { const target2 = closest ? utils.getClosest(target, '.psv-virtual-tour-link') : target; return target2 ? (target2 as any)[LINK_DATA] : undefined; }