UNPKG

@deck.gl/core

Version:

deck.gl core library

293 lines (258 loc) 9.36 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import type Deck from './deck'; import type Viewport from '../viewports/viewport'; import type {PickingInfo} from './picking/pick-info'; import type {MjolnirPointerEvent, MjolnirGestureEvent} from 'mjolnir.js'; import type Layer from './layer'; import {EVENT_HANDLERS} from './constants'; import {deepEqual} from '../utils/deep-equal'; export interface Widget<PropsT = any> { /** Unique identifier of the widget. */ id: string; /** Widget prop types. */ props: PropsT; /** * The view id that this widget is being attached to. Default `null`. * If assigned, this widget will only respond to events occurred inside the specific view that matches this id. */ viewId?: string | null; /** Widget positioning within the view. Default 'top-left'. */ placement?: WidgetPlacement; // Populated by core when mounted _element?: HTMLDivElement | null; // Lifecycle hooks /** Called when the widget is added to a Deck instance. * @returns an optional UI element that should be appended to the Deck container */ onAdd: (params: { /** The Deck instance that the widget is attached to */ deck: Deck<any>; /** The id of the view that the widget is attached to */ viewId: string | null; }) => HTMLDivElement | null; /** Called when the widget is removed */ onRemove?: () => void; /** Called to update widget options */ setProps: (props: Partial<PropsT>) => void; // Optional event hooks /** Called when the containing view is changed */ onViewportChange?: (viewport: Viewport) => void; /** Called when the containing view is redrawn */ onRedraw?: (params: {viewports: Viewport[]; layers: Layer[]}) => void; /** Called when a hover event occurs */ onHover?: (info: PickingInfo, event: MjolnirPointerEvent) => void; /** Called when a click event occurs */ onClick?: (info: PickingInfo, event: MjolnirGestureEvent) => void; /** Called when a drag event occurs */ onDrag?: (info: PickingInfo, event: MjolnirGestureEvent) => void; /** Called when a dragstart event occurs */ onDragStart?: (info: PickingInfo, event: MjolnirGestureEvent) => void; /** Called when a dragend event occurs */ onDragEnd?: (info: PickingInfo, event: MjolnirGestureEvent) => void; } const PLACEMENTS = { 'top-left': {top: 0, left: 0}, 'top-right': {top: 0, right: 0}, 'bottom-left': {bottom: 0, left: 0}, 'bottom-right': {bottom: 0, right: 0}, fill: {top: 0, left: 0, bottom: 0, right: 0} } as const; const DEFAULT_PLACEMENT = 'top-left'; export type WidgetPlacement = keyof typeof PLACEMENTS; const ROOT_CONTAINER_ID = '__root'; export class WidgetManager { deck: Deck<any>; parentElement?: HTMLElement | null; /** Widgets added via the imperative API */ private defaultWidgets: Widget[] = []; /** Widgets received from the declarative API */ private widgets: Widget[] = []; /** Resolved widgets from both imperative and declarative APIs */ private resolvedWidgets: Widget[] = []; /** Mounted HTML containers */ private containers: {[id: string]: HTMLDivElement} = {}; /** Viewport provided to widget on redraw */ private lastViewports: {[id: string]: Viewport} = {}; constructor({deck, parentElement}: {deck: Deck<any>; parentElement?: HTMLElement | null}) { this.deck = deck; this.parentElement = parentElement; } getWidgets(): Widget[] { return this.resolvedWidgets; } /** Declarative API to configure widgets */ setProps(props: {widgets?: Widget[]}) { if (props.widgets && !deepEqual(props.widgets, this.widgets, 1)) { this._setWidgets(props.widgets); } } finalize() { for (const widget of this.getWidgets()) { this._remove(widget); } this.defaultWidgets.length = 0; this.resolvedWidgets.length = 0; for (const id in this.containers) { this.containers[id].remove(); } } /** Imperative API. Widgets added this way are not affected by the declarative prop. */ addDefault(widget: Widget) { if (!this.defaultWidgets.find(w => w.id === widget.id)) { this._add(widget); this.defaultWidgets.push(widget); // Update widget list this._setWidgets(this.widgets); } } /** Resolve widgets from the declarative prop */ private _setWidgets(nextWidgets: Widget[]) { const oldWidgetMap: Record<string, Widget | null> = {}; for (const widget of this.resolvedWidgets) { oldWidgetMap[widget.id] = widget; } // Clear and rebuild the list this.resolvedWidgets.length = 0; // Add all default widgets for (const widget of this.defaultWidgets) { oldWidgetMap[widget.id] = null; this.resolvedWidgets.push(widget); } for (let widget of nextWidgets) { const oldWidget = oldWidgetMap[widget.id]; if (!oldWidget) { // Widget is new this._add(widget); } else if ( // Widget placement changed oldWidget.viewId !== widget.viewId || oldWidget.placement !== widget.placement ) { this._remove(oldWidget); this._add(widget); } else if (widget !== oldWidget) { // Widget props changed oldWidget.setProps(widget.props); widget = oldWidget; } // mark as matched oldWidgetMap[widget.id] = null; this.resolvedWidgets.push(widget); } for (const id in oldWidgetMap) { const oldWidget = oldWidgetMap[id]; if (oldWidget) { // No longer exists this._remove(oldWidget); } } this.widgets = nextWidgets; } private _add(widget: Widget) { const {viewId = null, placement = DEFAULT_PLACEMENT} = widget; const element = widget.onAdd({deck: this.deck, viewId}); if (element) { this._getContainer(viewId, placement).append(element); } widget._element = element; } private _remove(widget: Widget) { widget.onRemove?.(); if (widget._element) { widget._element.remove(); } widget._element = undefined; } /* global document */ private _getContainer(viewId: string | null, placement: WidgetPlacement): HTMLDivElement { const containerId = viewId || ROOT_CONTAINER_ID; let viewContainer = this.containers[containerId]; if (!viewContainer) { viewContainer = document.createElement('div'); viewContainer.style.pointerEvents = 'none'; viewContainer.style.position = 'absolute'; viewContainer.style.overflow = 'hidden'; this.parentElement?.append(viewContainer); this.containers[containerId] = viewContainer; } let container = viewContainer.querySelector<HTMLDivElement>(`.${placement}`); if (!container) { container = document.createElement('div'); container.className = placement; container.style.position = 'absolute'; container.style.zIndex = '2'; Object.assign(container.style, PLACEMENTS[placement]); viewContainer.append(container); } return container; } private _updateContainers() { const canvasWidth = this.deck.width; const canvasHeight = this.deck.height; for (const id in this.containers) { const viewport = this.lastViewports[id] || null; const visible = id === ROOT_CONTAINER_ID || viewport; const container = this.containers[id]; if (visible) { container.style.display = 'block'; // Align the container with the view container.style.left = `${viewport ? viewport.x : 0}px`; container.style.top = `${viewport ? viewport.y : 0}px`; container.style.width = `${viewport ? viewport.width : canvasWidth}px`; container.style.height = `${viewport ? viewport.height : canvasHeight}px`; } else { container.style.display = 'none'; } } } onRedraw({viewports, layers}: {viewports: Viewport[]; layers: Layer[]}) { const viewportsById: {[id: string]: Viewport} = viewports.reduce((acc, v) => { acc[v.id] = v; return acc; }, {}); for (const widget of this.getWidgets()) { const {viewId} = widget; if (viewId) { // Attached to a specific view const viewport = viewportsById[viewId]; if (viewport) { if (widget.onViewportChange) { widget.onViewportChange(viewport); } widget.onRedraw?.({viewports: [viewport], layers}); } } else { // Not attached to a specific view if (widget.onViewportChange) { for (const viewport of viewports) { widget.onViewportChange(viewport); } } widget.onRedraw?.({viewports, layers}); } } this.lastViewports = viewportsById; this._updateContainers(); } onHover(info: PickingInfo, event: MjolnirPointerEvent) { for (const widget of this.getWidgets()) { const {viewId} = widget; if (!viewId || viewId === info.viewport?.id) { widget.onHover?.(info, event); } } } onEvent(info: PickingInfo, event: MjolnirGestureEvent) { const eventHandlerProp = EVENT_HANDLERS[event.type]; if (!eventHandlerProp) { return; } for (const widget of this.getWidgets()) { const {viewId} = widget; if (!viewId || viewId === info.viewport?.id) { widget[eventHandlerProp]?.(info, event); } } } }