From 2c2cc0f5b3c96056c2af20da1271e85c0da2652a Mon Sep 17 00:00:00 2001 From: wangxingkang Date: Tue, 16 Jan 2024 19:01:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useReact.ts | 106 ------------------------------ src/layer/config.ts | 19 ------ src/layer/constant.ts | 21 ++++++ src/layer/demos/basic.tsx | 85 ------------------------ src/layer/hooks/useEvents.ts | 94 -------------------------- src/layer/index.md | 60 ----------------- src/layer/layer.tsx | 15 +++-- src/layer/types.ts | 4 +- src/map/map.tsx | 4 +- src/source/source.tsx | 20 ++---- src/source/types.ts | 1 + stories/MarkerCluster.stories.tsx | 36 ++++++++++ 12 files changed, 78 insertions(+), 387 deletions(-) delete mode 100644 src/hooks/useReact.ts create mode 100644 src/layer/constant.ts delete mode 100644 src/layer/demos/basic.tsx delete mode 100644 src/layer/hooks/useEvents.ts delete mode 100644 src/layer/index.md create mode 100644 stories/MarkerCluster.stories.tsx diff --git a/src/hooks/useReact.ts b/src/hooks/useReact.ts deleted file mode 100644 index 7473c1c..0000000 --- a/src/hooks/useReact.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { useDeepCompareEffect, useUnmount, usePrevious } from '@pansy/react-hooks'; -import { isEqual } from 'lodash-es'; -import { isFunction } from '@pansy/shared'; - -import { toCapitalString } from '@/utils'; - -import type { Instance } from '../types'; - -export interface Options { - ins?: Ins; - setterMap?: Record; - converterMap?: Record; - unmount?: () => void; -} - -/** - * 将参数分隔为事件和非事件两种 - * @param props - * @returns - */ -const splitPropsByEvent = (props: Record = {}) => { - let eventProps: Record = {}; - let notEventProps: Record = {}; - - Object.keys(props).forEach((key) => { - const item = props[key]; - if (isFunction(item) && /^on[A-Z]/.test(key)) { - eventProps[key] = item; - } else { - notEventProps[key] = item; - } - }); - - return { eventProps, notEventProps }; -}; - -/** - * 提供实例事件监听/动态属性的设置能力 - * @param props - * @param options - * @returns - */ -export const useReact = < - P extends Record, - Ins extends Instance, - Events extends Record, ->( - props: P = {} as P, - options: Options, -) => { - const { ins, setterMap = {}, converterMap = {}, unmount } = options; - const { notEventProps } = splitPropsByEvent(props); - const prevProps = usePrevious(notEventProps); - - useDeepCompareEffect(() => { - if (ins) { - reactivePropChange(notEventProps as P, true); - } - }, [notEventProps]); - - useUnmount(() => { - if (ins && 'remove' in ins) { - ins.remove(); - } - unmount?.(); - }); - - const reactivePropChange = (nextProps: P, shouldDetectChange = true) => { - if (!ins) return; - - try { - Object.keys(nextProps).forEach((key) => { - let willReactive = true; - if (shouldDetectChange) { - willReactive = !isEqual(nextProps[key], prevProps?.[key]); - } - if (!willReactive) return; - - // @ts-ignore - let setterParam = nextProps[key]; - - // 对值进行转换 - if (key in converterMap) { - // @ts-ignore - setterParam = converterMap[key](nextProps[key]); - } - - if (key in setterMap) { - setterMap[key](setterParam, ins, nextProps); - } else { - const trySetterName = `set${toCapitalString(key)}`; - - if (trySetterName in ins) { - ins[trySetterName](setterParam); - } - } - }); - } catch (err) {} - }; - - const onInstanceCreated = () => { - reactivePropChange(props, false); - }; - - return { onInstanceCreated }; -}; diff --git a/src/layer/config.ts b/src/layer/config.ts index 9d36e64..e69de29 100644 --- a/src/layer/config.ts +++ b/src/layer/config.ts @@ -1,19 +0,0 @@ -import type { EventMapping } from './types'; - -export const eventMapping: EventMapping = { - onClick: 'click', - onDoubleClick: 'dblclick', - onContextMenu: 'contextmenu', - - onMouseDown: 'mousedown', - onMouseUp: 'mouseup', - onMouseMove: 'mousemove', - onMouseEnter: 'mouseenter', - onMouseLeave: 'mouseleave', - onMouseOver: 'mouseover', - onMouseOut: 'mouseout', - - onTouchStart: 'touchstart', - onTouchEnd: 'touchend', - onTouchCancel: 'touchcancel', -}; diff --git a/src/layer/constant.ts b/src/layer/constant.ts new file mode 100644 index 0000000..99c6230 --- /dev/null +++ b/src/layer/constant.ts @@ -0,0 +1,21 @@ +import type { EventMapping, KeysOfUnion } from './types'; + +export const LayerEventMap: EventMapping = { + onClick: 'click', + onDoubleClick: 'dblclick', + onContextMenu: 'contextmenu', + + onMouseDown: 'mousedown', + onMouseUp: 'mouseup', + onMouseMove: 'mousemove', + onMouseEnter: 'mouseenter', + onMouseLeave: 'mouseleave', + onMouseOver: 'mouseover', + onMouseOut: 'mouseout', + + onTouchStart: 'touchstart', + onTouchEnd: 'touchend', + onTouchCancel: 'touchcancel', +}; + +export const LayerEventList = Object.keys(LayerEventMap) as KeysOfUnion[]; diff --git a/src/layer/demos/basic.tsx b/src/layer/demos/basic.tsx deleted file mode 100644 index 860c90c..0000000 --- a/src/layer/demos/basic.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Layer, Map, Source } from '@pansy/react-mapbox-gl'; - -export default () => { - const clusterLayer = { - id: 'clusters', - type: 'circle', - source: 'earthquakes', - filter: ['has', 'point_count'], - paint: { - 'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 750, '#f28cb1'], - 'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40], - }, - }; - - const clusterCountLayer = { - id: 'cluster-count', - type: 'symbol', - source: 'earthquakes', - filter: ['has', 'point_count'], - layout: { - 'text-field': '{point_count_abbreviated}', - 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], - 'text-size': 12, - }, - }; - - const unclusteredPointLayer = { - id: 'unclustered-point', - type: 'circle', - source: 'earthquakes', - filter: ['!', ['has', 'point_count']], - paint: { - 'circle-color': '#11b4da', - 'circle-radius': 4, - 'circle-stroke-width': 1, - 'circle-stroke-color': '#fff', - }, - }; - - return ( -
- - - { - const map = e.target; - - const features = map.queryRenderedFeatures(e.point, { - layers: ['clusters'], - }); - - // @ts-ignore - const clusterId = features[0].properties.cluster_id; - - // @ts-ignore - map.getSource('earthquakes').getClusterExpansionZoom(clusterId, (err, zoom) => { - if (err) return; - - map.easeTo({ - // @ts-ignore - center: features[0].geometry.coordinates, - zoom: zoom, - }); - }); - }} - onMouseEnter={(e) => { - e.target.getCanvas().style.cursor = 'pointer'; - }} - onMouseLeave={(e) => { - e.target.getCanvas().style.cursor = ''; - }} - /> - - - -
- ); -}; diff --git a/src/layer/hooks/useEvents.ts b/src/layer/hooks/useEvents.ts deleted file mode 100644 index 56e6036..0000000 --- a/src/layer/hooks/useEvents.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useRef, useEffect } from 'react'; -import { useDeepCompareEffect, useUnmount } from '@pansy/react-hooks'; -import { isEqual } from 'lodash-es'; - -import type { Map } from 'mapbox-gl'; -import type { LayerProps, LayerType, LayerEvents, EventMapping, LayerEventKeys } from '../types'; - -/** - * 提供事件绑定的能力 - * @param ins 地图实例 - * @param eventMapping 事件名称对应表 - * @param props - */ -export const useEvents = ( - ins: Map, - eventMapping: EventMapping, - props: LayerProps, -) => { - const listeners = useRef>({}); - - useEffect(() => { - if (ins) { - listeners.current = listenEvents(eventMapping, props); - } - }, [ins]); - - useDeepCompareEffect(() => { - if (ins) { - listeners.current = updateEvents(listeners.current, props); - } - }, [props]); - - useUnmount(() => { - unlistenEvents(listeners.current); - listeners.current = {}; - }); - - const listenEvents = (eventMapping: Partial, props: LayerProps) => - (Object.keys(eventMapping) as LayerEventKeys[]).reduce((listeners, event) => { - const eventCallback = props[event]; - const eventName = eventMapping[event]; - - if (eventName && eventCallback) { - // @ts-ignore - ins.on(eventName, props.id, eventCallback); - // @ts-ignore - listeners[event] = eventCallback; - } - - return listeners; - }, {}); - - const updateEvents = (listeners: Partial = {}, currentProps: LayerProps) => { - // 解绑旧的事件 - const toListenOff = (Object.keys(eventMapping) as LayerEventKeys[]).filter( - (eventKey) => - (listeners[eventKey] && typeof currentProps[eventKey] !== 'function') || - !isEqual(listeners[eventKey], currentProps[eventKey]), - ); - - toListenOff.forEach((key) => { - if (listeners[key]) { - // @ts-ignore - ins.off(key, props.id, listeners[key]); - delete listeners[key]; - } - }); - - // 绑定新的事件 - const toListenOn = (Object.keys(eventMapping) as LayerEventKeys[]) - .filter((key) => !listeners[key] && typeof currentProps[key] === 'function') - .reduce>((acc, next) => ((acc[next] = eventMapping[next]), acc), {}); - - const newListeners = listenEvents(toListenOn, currentProps); - - return { ...listeners, ...newListeners }; - }; - - /** - * 解除监听 - * @param listeners - * @param ins - */ - const unlistenEvents = (listeners: Partial = {}) => { - const unlistenList = (Object.keys(eventMapping) as LayerEventKeys[]).filter( - (eventKey) => listeners[eventKey], - ); - - unlistenList.forEach((key) => { - // @ts-ignore - ins.off(key, props.id, listeners[key]); - }); - }; -}; diff --git a/src/layer/index.md b/src/layer/index.md deleted file mode 100644 index 1b5a4d6..0000000 --- a/src/layer/index.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Layer -order: 1 -toc: content -group: - title: 地图组件 - order: 1 -nav: - title: 组件 - path: /components ---- - -## 介绍 - -## 代码示例 - -### 基本示例 - - - -## API - -### 静态属性 - -| 属性 | 说明 | 类型 | 默认值 | -| --- | --- | --- | --- | -| id | Layer 的唯一标识名称 | `string` | -- | -| type | Layer 类型 | `fill`\| `line`\| `symbol`\| `circle`\| `heatmap`\| `fill-extrusion`\| `raster`\| `hillshade`\| `background`\| `sky` | -- | -| metadata | 元数据 | `any` | -- | -| source | Layer 对应的 source | `string`\|`AnySourceData`\|`undefined` | -- | -| 'source-layer' | 在矢量瓦片中指定 layer | `string`\|`undefined` | -- | - -### 动态属性 - -| 属性 | 说明 | 类型 | 默认值 | -| ----------- | --------------------- | ------------------------ | ------ | -| minzoom | Layer 的最小层级,0-24 | `number`\|`undefined` | -- | -| maxzoom | Layer 的最大层级,0-24 | `number`\|`undefined` | -- | -| interactive | 是否可交互 | `boolean`\|`undefined` | `true` | -| filter | Layer 的过滤的表达式 | `any[]` \| `undefined` | -- | -| layout | Layer 布局的样式 | `AnyLayout`\|`undefined` | -- | -| paint | Layer 绘制的样式 | `AnyPaint`\|`undefined` | -- | - -### 事件 - -| 属性 | 说明 | -| ------------- | ------------------------------------------------------------------------------ | -| onClick | 当指针设备(一般为鼠标)在该图层同一点处点击并释放时触发 | -| onDoubleClick | 当指针设备(一般为鼠标)快速双击该图层同一点时触发 | -| onContextMenu | 点击鼠标右键或点开图层上的快捷菜单(context menu)时触发 | -| onMouseDown | 当指针设备(一般为鼠标)在该图层中被按压时触发 | -| onMouseUp | 当指针设备(一般为鼠标)在该图层中被释放时触发 | -| onMouseMove | 当指针设备(一般为鼠标)在该图层中移动时触发 | -| onMouseEnter | 当指针设备(一般为鼠标)从某一图层外部或地图画布外部进入该图层的可见部分时触发 | -| onMouseLeave | 当指针设备(一般为鼠标)离开指定图层的可见部分或离开地图画布时触发 | -| onMouseOver | 当指针设备(一般为鼠标)在该图层中移动时触发 | -| onMouseOut | 当指针设备(一般为鼠标)离开该图层画布时触发 | -| onTouchStart | 当 touchstart 事件在该图层中触发时启动 | -| onTouchEnd | 当 touchend 事件在该图层中触发时启动 | -| onTouchCancel | 当 touchcancel 事件在该图层中触发时启动 | diff --git a/src/layer/layer.tsx b/src/layer/layer.tsx index 4e5c862..78009ab 100644 --- a/src/layer/layer.tsx +++ b/src/layer/layer.tsx @@ -1,18 +1,21 @@ import { useEffect } from 'react'; -import { useMap } from '@/hooks/useMap'; -import { useEvents } from './hooks/useEvents'; +import { useMap } from '../hooks/useMap'; +import { useEvents } from '../hooks/useEvents'; -import { eventMapping } from './config'; +import { LayerEventList, LayerEventMap } from './constant'; -import type { AnyLayer } from 'mapbox-gl'; +import type { AnyLayer, Map } from 'mapbox-gl'; import type { LayerProps, LayerType } from './types'; export const Layer = (props: LayerProps) => { const { before, ...rest } = props; - const map = useMap(); + const { map } = useMap(); - useEvents(map, eventMapping, props); + useEvents>(map, props, { + eventMap: LayerEventMap, + eventList: LayerEventList, + }); useEffect(() => { if (map) { diff --git a/src/layer/types.ts b/src/layer/types.ts index 322ca02..5a87d6a 100644 --- a/src/layer/types.ts +++ b/src/layer/types.ts @@ -22,7 +22,9 @@ import type { HillshadePaint, MapLayerEventType, } from 'mapbox-gl'; -import type { KeysOfUnion } from '@pansy/shared/types'; +import type { KeysOfUnion } from '../types'; + +export type { KeysOfUnion } from '../types'; export type LayerType = | 'background' diff --git a/src/map/map.tsx b/src/map/map.tsx index 24244f1..9ee2073 100644 --- a/src/map/map.tsx +++ b/src/map/map.tsx @@ -1,10 +1,10 @@ import Mapbox from 'mapbox-gl'; import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { MapContext } from './context'; -import { usePropsReactive } from '@/hooks/usePropsReactive'; +import { useEvents } from '../hooks/useEvents'; +import { usePropsReactive } from '../hooks/usePropsReactive'; import { allProps, setterMap, converterMap } from './config'; import { defaultContainerStyle, MapEventMap, MapEventList } from './constant'; -import { useEvents } from '@/hooks/useEvents'; import 'mapbox-gl/dist/mapbox-gl.css'; import './map.css'; diff --git a/src/source/source.tsx b/src/source/source.tsx index dcb0376..8b1cbca 100644 --- a/src/source/source.tsx +++ b/src/source/source.tsx @@ -1,30 +1,22 @@ import { forwardRef, useEffect, useState, useImperativeHandle } from 'react'; -import { useMap } from '@/hooks/useMap'; -import { useReact } from '@/hooks/useReact'; - +import { useMap } from '../hooks/useMap'; +import { usePropsReactive } from '../hooks/usePropsReactive'; import { setterMap, converterMap } from './config'; -import type { FC } from 'react'; import type { GeoJSONSource } from 'mapbox-gl'; -import type { SourceProps, EventMapping } from './types'; +import type { SourceProps } from './types'; -export const Source: FC = forwardRef((props, ref) => { +export const Source = forwardRef((props, ref) => { const { id, ...rest } = props; - const map = useMap(); + const { map } = useMap(); const [source, setSource] = useState(); useImperativeHandle(ref, () => source as GeoJSONSource, [source]); - const { onInstanceCreated } = useReact(props, { - ins: source, - events: {}, + const { onInstanceCreated } = usePropsReactive(props, source!, { setterMap, converterMap, - unmount: () => { - //@ts-ignore - map && map.style && map.removeSource(id); - }, }); useEffect(() => { diff --git a/src/source/types.ts b/src/source/types.ts index 34b3a5f..e03ad7d 100644 --- a/src/source/types.ts +++ b/src/source/types.ts @@ -1,5 +1,6 @@ import type { GeoJSONSourceOptions } from 'mapbox-gl'; +export type { KeysOfUnion } from '../types'; export type SourceEvents = {}; export type EventMapping = { [T in keyof SourceEvents]: string }; diff --git a/stories/MarkerCluster.stories.tsx b/stories/MarkerCluster.stories.tsx new file mode 100644 index 0000000..a017d78 --- /dev/null +++ b/stories/MarkerCluster.stories.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Map, Marker } from '../src'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: '示例/聚合', + render: (props) => { + return ( + + +
123
+
+
+ ); + }, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + argTypes: {}, + args: { + lngLat: [-122.414, 37.776], + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Event: Story = { + args: { + onClick: (e) => { + console.log(e); + }, + }, +};