mirror of
https://github.com/visgl/react-map-gl.git
synced 2025-12-08 20:16:02 +00:00
[v7] Map component (#1652)
This commit is contained in:
parent
170217280e
commit
5b674be3c4
@ -1,8 +1,8 @@
|
||||
module.exports = {
|
||||
extends: '@istanbuljs/nyc-config-typescript',
|
||||
all: 'true',
|
||||
sourceMap: false,
|
||||
instrument: true,
|
||||
extensions: ['.ts', '.tsx'],
|
||||
include: ['src']
|
||||
};
|
||||
module.exports = {
|
||||
extends: '@istanbuljs/nyc-config-typescript',
|
||||
all: 'true',
|
||||
sourceMap: false,
|
||||
instrument: true,
|
||||
extensions: ['.ts', '.tsx'],
|
||||
include: ['src']
|
||||
};
|
||||
|
||||
4
src/components/map-context.ts
Normal file
4
src/components/map-context.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import type {MapboxMap} from '../utils/types';
|
||||
|
||||
export default React.createContext<MapboxMap>(null);
|
||||
@ -1,9 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import {useState, useRef, useEffect, forwardRef, useImperativeHandle} from 'react';
|
||||
|
||||
export type MapProps = {
|
||||
id: string;
|
||||
import Mapbox from '../mapbox/mapbox';
|
||||
import type {MapboxProps} from '../mapbox/mapbox';
|
||||
import MapContext from './map-context';
|
||||
|
||||
import type {CSSProperties} from 'react';
|
||||
import type {MapboxMap} from '../utils/types';
|
||||
import useIsomorphicLayoutEffect from '../utils/use-isomorphic-layout-effect';
|
||||
|
||||
export interface MapRef {
|
||||
getMap(): MapboxMap;
|
||||
}
|
||||
|
||||
export type MapProps = MapboxProps & {
|
||||
/** Map container id */
|
||||
id?: string;
|
||||
/** Map container CSS style */
|
||||
style?: CSSProperties;
|
||||
children?: any;
|
||||
|
||||
ref?: React.Ref<MapRef>;
|
||||
};
|
||||
|
||||
export default function Map(props: MapProps) {
|
||||
return <div id={props.id} />;
|
||||
}
|
||||
const defaultProps: MapProps = {
|
||||
// Constraints
|
||||
minZoom: 0,
|
||||
maxZoom: 22,
|
||||
minPitch: 0,
|
||||
maxPitch: 85,
|
||||
|
||||
// Interaction handlers
|
||||
scrollZoom: true,
|
||||
boxZoom: true,
|
||||
dragRotate: true,
|
||||
dragPan: true,
|
||||
keyboard: true,
|
||||
doubleClickZoom: true,
|
||||
touchZoomRotate: true,
|
||||
touchPitch: true,
|
||||
|
||||
// Style
|
||||
mapStyle: {version: 8},
|
||||
styleDiffing: true,
|
||||
projection: 'mercator',
|
||||
renderWorldCopies: true
|
||||
};
|
||||
|
||||
const Map = forwardRef((props: MapProps, ref) => {
|
||||
const [mapInstance, setMapInstance] = useState<Mapbox>(null);
|
||||
const containerRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const map = new Mapbox(props);
|
||||
map.initialize(containerRef.current);
|
||||
setMapInstance(map);
|
||||
return () => map.destroy();
|
||||
}, []);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (mapInstance) {
|
||||
mapInstance.setProps(props);
|
||||
}
|
||||
});
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getMap: () => mapInstance.getMap()
|
||||
}),
|
||||
[mapInstance]
|
||||
);
|
||||
|
||||
const style: CSSProperties = {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
...props.style
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={props.id} ref={containerRef} style={style}>
|
||||
{mapInstance && (
|
||||
<MapContext.Provider value={mapInstance.getMap()}>{props.children}</MapContext.Provider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Map.defaultProps = defaultProps;
|
||||
|
||||
export default Map;
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
export {default} from './components/map';
|
||||
|
||||
export {default as Map, MapProps} from './components/map';
|
||||
export {default as Map, MapProps, MapRef} from './components/map';
|
||||
|
||||
// Types
|
||||
export * from './utils/types';
|
||||
|
||||
601
src/mapbox/mapbox.ts
Normal file
601
src/mapbox/mapbox.ts
Normal file
@ -0,0 +1,601 @@
|
||||
import mapboxgl from '../utils/mapboxgl';
|
||||
import {Transform, transformToViewState, applyViewStateToTransform} from '../utils/transform';
|
||||
import {deepEqual} from '../utils/deep-equal';
|
||||
|
||||
import type {
|
||||
ProjectionSpecification,
|
||||
ViewState,
|
||||
ViewStateChangeEvent,
|
||||
MapboxOptions,
|
||||
Style,
|
||||
MapMouseEvent,
|
||||
MapLayerMouseEvent,
|
||||
MapLayerTouchEvent,
|
||||
MapWheelEvent,
|
||||
MapDataEvent,
|
||||
MapboxEvent,
|
||||
ErrorEvent,
|
||||
MapboxGeoJSONFeature
|
||||
} from '../utils/types';
|
||||
|
||||
export type MapboxProps = Omit<
|
||||
MapboxOptions,
|
||||
'center' | 'accessToken' | 'container' | 'style' | 'bounds' | 'fitBoundsOptions'
|
||||
> &
|
||||
ViewState & {
|
||||
mapboxAccessToken?: string;
|
||||
|
||||
/** Camera options used when constructing the Map instance */
|
||||
initialViewState?: Pick<MapboxOptions, 'bounds' | 'fitBoundsOptions'> & ViewState;
|
||||
|
||||
/** If provided, render into an external WebGL context */
|
||||
gl?: WebGLRenderingContext;
|
||||
|
||||
/** Aternative way to specify camera state */
|
||||
viewState?: ViewState;
|
||||
|
||||
/** Mapbox style */
|
||||
mapStyle?: string | Style;
|
||||
/** Enable diffing when the map style changes */
|
||||
styleDiffing?: boolean;
|
||||
/** Default layers to query on pointer events */
|
||||
interactiveLayerIds?: string[];
|
||||
/** The projection the map should be rendered in */
|
||||
projection?: ProjectionSpecification | string;
|
||||
/** CSS cursor */
|
||||
cursor?: string;
|
||||
|
||||
// Callbacks
|
||||
onMouseDown?: (e: MapLayerMouseEvent) => void;
|
||||
onMouseUp?: (e: MapLayerMouseEvent) => void;
|
||||
onMouseOver?: (e: MapLayerMouseEvent) => void;
|
||||
onMouseMove?: (e: MapLayerMouseEvent) => void;
|
||||
onClick?: (e: MapLayerMouseEvent) => void;
|
||||
onDblClick?: (e: MapLayerMouseEvent) => void;
|
||||
onMouseEnter?: (e: MapLayerMouseEvent) => void;
|
||||
onMouseLeave?: (e: MapLayerMouseEvent) => void;
|
||||
onMouseOut?: (e: MapLayerMouseEvent) => void;
|
||||
onContextMenu?: (e: MapLayerMouseEvent) => void;
|
||||
onWheel?: (e: MapWheelEvent) => void;
|
||||
onTouchStart?: (e: MapLayerTouchEvent) => void;
|
||||
onTouchEnd?: (e: MapLayerTouchEvent) => void;
|
||||
onTouchMove?: (e: MapLayerTouchEvent) => void;
|
||||
onTouchCancel?: (e: MapLayerTouchEvent) => void;
|
||||
|
||||
onMoveStart?: (e: ViewStateChangeEvent) => void;
|
||||
onMove?: (e: ViewStateChangeEvent) => void;
|
||||
onMoveEnd?: (e: ViewStateChangeEvent) => void;
|
||||
onDragStart?: (e: ViewStateChangeEvent) => void;
|
||||
onDrag?: (e: ViewStateChangeEvent) => void;
|
||||
onDragEnd?: (e: ViewStateChangeEvent) => void;
|
||||
onZoomStart?: (e: ViewStateChangeEvent) => void;
|
||||
onZoom?: (e: ViewStateChangeEvent) => void;
|
||||
onZoomEnd?: (e: ViewStateChangeEvent) => void;
|
||||
onRotateStart?: (e: ViewStateChangeEvent) => void;
|
||||
onRotate?: (e: ViewStateChangeEvent) => void;
|
||||
onRotateEnd?: (e: ViewStateChangeEvent) => void;
|
||||
onPitchStart?: (e: ViewStateChangeEvent) => void;
|
||||
onPitch?: (e: ViewStateChangeEvent) => void;
|
||||
onPitchEnd?: (e: ViewStateChangeEvent) => void;
|
||||
onBoxZoomStart?: (e: ViewStateChangeEvent) => void;
|
||||
onBoxZoomEnd?: (e: ViewStateChangeEvent) => void;
|
||||
onBoxZoomCancel?: (e: ViewStateChangeEvent) => void;
|
||||
|
||||
onResize?: (e: MapboxEvent) => void;
|
||||
onLoad?: (e: MapboxEvent) => void;
|
||||
onRender?: (e: MapboxEvent) => void;
|
||||
onIdle?: (e: MapboxEvent) => void;
|
||||
onError?: (e: ErrorEvent) => void;
|
||||
onRemove?: (e: MapboxEvent) => void;
|
||||
onData?: (e: MapDataEvent) => void;
|
||||
onStyleData?: (e: MapDataEvent) => void;
|
||||
onSourceData?: (e: MapDataEvent) => void;
|
||||
};
|
||||
|
||||
const pointerEvents = {
|
||||
mousedown: 'onMouseDown',
|
||||
mouseup: 'onMouseUp',
|
||||
mouseover: 'onMouseOver',
|
||||
mousemove: 'onMouseMove',
|
||||
click: 'onClick',
|
||||
dblclick: 'onDblClick',
|
||||
mouseenter: 'onMouseEnter',
|
||||
mouseleave: 'onMouseLeave',
|
||||
mouseout: 'onMouseOut',
|
||||
contextmenu: 'onContextMenu',
|
||||
touchstart: 'onTouchStart',
|
||||
touchend: 'onTouchEnd',
|
||||
touchmove: 'onTouchMove',
|
||||
touchcancel: 'onTouchCancel'
|
||||
};
|
||||
const cameraEvents = {
|
||||
movestart: 'onMoveStart',
|
||||
move: 'onMove',
|
||||
moveend: 'onMoveEnd',
|
||||
dragstart: 'onDragStart',
|
||||
drag: 'onDrag',
|
||||
dragend: 'onDragEnd',
|
||||
zoomstart: 'onZoomStart',
|
||||
zoom: 'onZoom',
|
||||
zoomend: 'onZoomEnd',
|
||||
rotatestart: 'onRotateStart',
|
||||
rotate: 'onRotate',
|
||||
rotateend: 'onRotateEnd',
|
||||
pitchstart: 'onPitchStart',
|
||||
pitch: 'onPitch',
|
||||
pitchend: 'onPitchEnd',
|
||||
boxzoomstart: 'onBoxZoomStart',
|
||||
boxzoomend: 'onBoxZoomEnd',
|
||||
boxzoomcancel: 'onBoxZoomCancel'
|
||||
};
|
||||
const otherEvents = {
|
||||
wheel: 'onWheel',
|
||||
resize: 'onResize',
|
||||
load: 'onLoad',
|
||||
render: 'onRender',
|
||||
idle: 'onIdle',
|
||||
remove: 'onRemove',
|
||||
data: 'onData',
|
||||
styledata: 'onStyleData',
|
||||
sourcedata: 'onSourceData'
|
||||
};
|
||||
const settingNames: (keyof MapboxProps)[] = [
|
||||
'minZoom',
|
||||
'maxZoom',
|
||||
'minPitch',
|
||||
'maxPitch',
|
||||
'maxBounds',
|
||||
'projection',
|
||||
'renderWorldCopies'
|
||||
];
|
||||
const handlerNames: (keyof MapboxProps)[] = [
|
||||
'scrollZoom',
|
||||
'boxZoom',
|
||||
'dragRotate',
|
||||
'dragPan',
|
||||
'keyboard',
|
||||
'doubleClickZoom',
|
||||
'touchZoomRotate',
|
||||
'touchPitch'
|
||||
];
|
||||
|
||||
/**
|
||||
* A wrapper for mapbox-gl's Map class
|
||||
*/
|
||||
export default class Mapbox {
|
||||
// mapboxgl.Map instance. Not using type here because we are accessing
|
||||
// private members and methods
|
||||
private _map: any = null;
|
||||
// User-supplied props
|
||||
props: MapboxProps;
|
||||
|
||||
// Mapbox map is stateful.
|
||||
// During method calls/user interactions, map.transform is mutated and
|
||||
// deviate from user-supplied props.
|
||||
// In order to control the map reactively, we shadow the transform
|
||||
// with the one below, which reflects the view state resolved from
|
||||
// both user-supplied props and the underlying state
|
||||
private _renderTransform: Transform;
|
||||
|
||||
// Internal states
|
||||
private _internalUpdate: boolean = false;
|
||||
private _inRender: boolean = false;
|
||||
private _hoveredFeatures: MapboxGeoJSONFeature[] = null;
|
||||
private _moved: boolean = false;
|
||||
private _zoomed: boolean = false;
|
||||
private _pitched: boolean = false;
|
||||
private _rotated: boolean = false;
|
||||
private _nextProps: MapboxProps | null;
|
||||
|
||||
constructor(props: MapboxProps) {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
getMap() {
|
||||
return this._map;
|
||||
}
|
||||
|
||||
setProps(props: MapboxProps) {
|
||||
if (this._inRender) {
|
||||
this._nextProps = props;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldProps = this.props;
|
||||
this.props = props;
|
||||
|
||||
const settingsChanged = this._updateSettings(props, oldProps);
|
||||
if (settingsChanged) {
|
||||
this._renderTransform = this._map.transform.clone();
|
||||
}
|
||||
const viewStateChanged = this._updateViewState(props, true);
|
||||
this._updateStyle(props, oldProps);
|
||||
this._updateHandlers(props, oldProps);
|
||||
|
||||
// If 1) view state has changed to match props and
|
||||
// 2) the props change is not triggered by map events,
|
||||
// it's driven by an external state change. Redraw immediately
|
||||
if (settingsChanged || (viewStateChanged && !this._map.isMoving())) {
|
||||
this.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
initialize(container: HTMLDivElement) {
|
||||
const {props} = this;
|
||||
const mapOptions = {
|
||||
...props,
|
||||
...props.initialViewState,
|
||||
accessToken: props.mapboxAccessToken || getAccessTokenFromEnv() || null,
|
||||
container,
|
||||
style: props.mapStyle
|
||||
};
|
||||
|
||||
const viewState = mapOptions.initialViewState || mapOptions.viewState || mapOptions;
|
||||
Object.assign(mapOptions, {
|
||||
center: [viewState.longitude || 0, viewState.latitude || 0],
|
||||
zoom: viewState.zoom || 0,
|
||||
pitch: viewState.pitch || 0,
|
||||
bearing: viewState.bearing || 0
|
||||
});
|
||||
|
||||
if (props.gl) {
|
||||
// eslint-disable-next-line
|
||||
const getContext = HTMLCanvasElement.prototype.getContext;
|
||||
// Hijack canvas.getContext to return our own WebGLContext
|
||||
// This will be called inside the mapboxgl.Map constructor
|
||||
// @ts-expect-error
|
||||
HTMLCanvasElement.prototype.getContext = () => {
|
||||
// Unhijack immediately
|
||||
HTMLCanvasElement.prototype.getContext = getContext;
|
||||
return props.gl;
|
||||
};
|
||||
}
|
||||
|
||||
const map: any = new mapboxgl.Map(mapOptions);
|
||||
if (viewState.padding) {
|
||||
map.setPadding(viewState.padding);
|
||||
}
|
||||
this._renderTransform = map.transform.clone();
|
||||
|
||||
// Hack
|
||||
// Insert code into map's render cycle
|
||||
const renderMap = map._render;
|
||||
map._render = this._render.bind(this, renderMap);
|
||||
const runRenderTaskQueue = map._renderTaskQueue.run;
|
||||
map._renderTaskQueue.run = (arg: number) => {
|
||||
runRenderTaskQueue.call(map._renderTaskQueue, arg);
|
||||
this._onBeforeRepaint();
|
||||
};
|
||||
// Insert code into map's event pipeline
|
||||
const fireEvent = map.fire;
|
||||
map.fire = this._fireEvent.bind(this, fireEvent);
|
||||
|
||||
// add listeners
|
||||
for (const eventName in pointerEvents) {
|
||||
map.on(eventName, this._onPointerEvent);
|
||||
}
|
||||
for (const eventName in cameraEvents) {
|
||||
map.on(eventName, this._onCameraEvent);
|
||||
}
|
||||
for (const eventName in otherEvents) {
|
||||
map.on(eventName, this._onEvent);
|
||||
}
|
||||
map.on('error', this._onError);
|
||||
this._map = map;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._map.remove();
|
||||
}
|
||||
|
||||
// Force redraw the map now. Typically resize() and jumpTo() is reflected in the next
|
||||
// render cycle, which is managed by Mapbox's animation loop.
|
||||
// This removes the synchronization issue caused by requestAnimationFrame.
|
||||
redraw() {
|
||||
const map = this._map;
|
||||
// map._render will throw error if style does not exist
|
||||
// https://github.com/mapbox/mapbox-gl-js/blob/fb9fc316da14e99ff4368f3e4faa3888fb43c513
|
||||
// /src/ui/map.js#L1834
|
||||
if (map.style) {
|
||||
// cancel the scheduled update
|
||||
if (map._frame) {
|
||||
map._frame.cancel();
|
||||
map._frame = null;
|
||||
}
|
||||
// the order is important - render() may schedule another update
|
||||
map._render();
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from map.jumpTo
|
||||
/* Update camera to match props
|
||||
@param {object} nextProps
|
||||
@param {bool} triggerEvents - should fire camera events
|
||||
@returns {bool} true if anything is changed
|
||||
*/
|
||||
_updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean {
|
||||
if (this._internalUpdate) {
|
||||
return false;
|
||||
}
|
||||
const map = this._map;
|
||||
|
||||
const tr = this._renderTransform;
|
||||
// Take a snapshot of the transform before mutation
|
||||
const {zoom, pitch, bearing} = tr;
|
||||
const changed = applyViewStateToTransform(tr, {
|
||||
...transformToViewState(map.transform),
|
||||
...nextProps
|
||||
});
|
||||
|
||||
if (changed && triggerEvents) {
|
||||
// Delay DOM control updates to the next render cycle
|
||||
this._moved = true;
|
||||
this._zoomed = this._zoomed || zoom !== tr.zoom;
|
||||
this._rotated = this._rotated || bearing !== tr.bearing;
|
||||
this._pitched = this._pitched || pitch !== tr.pitch;
|
||||
}
|
||||
|
||||
// Avoid manipulating the real transform when interaction/animation is ongoing
|
||||
// as it would interfere with Mapbox's handlers
|
||||
if (!map.isMoving()) {
|
||||
applyViewStateToTransform(map.transform, nextProps);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/* Update camera constraints and projection settings to match props
|
||||
@param {object} nextProps
|
||||
@param {object} currProps
|
||||
@returns {bool} true if anything is changed
|
||||
*/
|
||||
_updateSettings(nextProps: MapboxProps, currProps: MapboxProps): boolean {
|
||||
const map = this._map;
|
||||
let changed = false;
|
||||
for (const propName of settingNames) {
|
||||
if (!deepEqual(nextProps[propName], currProps[propName])) {
|
||||
changed = true;
|
||||
map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]?.(nextProps[propName]);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/* Update map style to match props
|
||||
@param {object} nextProps
|
||||
@param {object} currProps
|
||||
@returns {bool} true if style is changed
|
||||
*/
|
||||
_updateStyle(nextProps: MapboxProps, currProps: MapboxProps): boolean {
|
||||
if (nextProps.cursor !== currProps.cursor) {
|
||||
this._map.getCanvas().style.cursor = nextProps.cursor;
|
||||
}
|
||||
if (nextProps.mapStyle !== currProps.mapStyle) {
|
||||
const options: any = {
|
||||
diff: nextProps.styleDiffing
|
||||
};
|
||||
if ('localIdeographFontFamily' in nextProps) {
|
||||
options.localIdeographFontFamily = nextProps.localIdeographFontFamily;
|
||||
}
|
||||
this._map.setStyle(nextProps.mapStyle, options);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Update interaction handlers to match props
|
||||
@param {object} nextProps
|
||||
@param {object} currProps
|
||||
@returns {bool} true if anything is changed
|
||||
*/
|
||||
_updateHandlers(nextProps: MapboxProps, currProps: MapboxProps): boolean {
|
||||
const map = this._map;
|
||||
let changed = false;
|
||||
for (const propName of handlerNames) {
|
||||
const newValue = nextProps[propName];
|
||||
if (!deepEqual(newValue, currProps[propName])) {
|
||||
changed = true;
|
||||
if (newValue) {
|
||||
map[propName].enable(newValue);
|
||||
} else {
|
||||
map[propName].disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
_onEvent = (e: MapboxEvent) => {
|
||||
// @ts-ignore
|
||||
const cb = this.props[otherEvents[e.type]];
|
||||
if (cb) {
|
||||
cb(e);
|
||||
}
|
||||
};
|
||||
|
||||
_onError = (e: ErrorEvent) => {
|
||||
if (this.props.onError) {
|
||||
this.props.onError(e);
|
||||
} else {
|
||||
console.error(e.error);
|
||||
}
|
||||
};
|
||||
|
||||
_updateHover(e: MapMouseEvent) {
|
||||
const {props} = this;
|
||||
const shouldTrackHoveredFeatures =
|
||||
props.interactiveLayerIds && (props.onMouseMove || props.onMouseEnter || props.onMouseLeave);
|
||||
|
||||
if (shouldTrackHoveredFeatures) {
|
||||
const eventType = e.type;
|
||||
const wasHovering = this._hoveredFeatures?.length > 0;
|
||||
let features;
|
||||
if (eventType === 'mousemove') {
|
||||
try {
|
||||
features = this._map.queryRenderedFeatures(e.point, {
|
||||
layers: props.interactiveLayerIds
|
||||
});
|
||||
} catch {
|
||||
features = [];
|
||||
}
|
||||
} else {
|
||||
features = [];
|
||||
}
|
||||
const isHovering = features.length > 0;
|
||||
|
||||
if (!isHovering && wasHovering) {
|
||||
e.type = 'mouseleave';
|
||||
this._onPointerEvent(e);
|
||||
}
|
||||
this._hoveredFeatures = features;
|
||||
if (isHovering && !wasHovering) {
|
||||
e.type = 'mouseenter';
|
||||
this._onPointerEvent(e);
|
||||
}
|
||||
e.type = eventType;
|
||||
} else {
|
||||
this._hoveredFeatures = null;
|
||||
}
|
||||
}
|
||||
|
||||
_onPointerEvent = (e: MapLayerMouseEvent | MapLayerTouchEvent) => {
|
||||
// @ts-ignore
|
||||
const cb = this.props[pointerEvents[e.type]];
|
||||
if (cb) {
|
||||
if (this.props.interactiveLayerIds && e.type !== 'mouseover' && e.type !== 'mouseout') {
|
||||
const features =
|
||||
this._hoveredFeatures ||
|
||||
this._map.queryRenderedFeatures(e.point, {
|
||||
layers: this.props.interactiveLayerIds
|
||||
});
|
||||
if (!features.length) {
|
||||
return;
|
||||
}
|
||||
e.features = features;
|
||||
}
|
||||
cb(e);
|
||||
delete e.features;
|
||||
}
|
||||
};
|
||||
|
||||
_onCameraEvent = (e: ViewStateChangeEvent) => {
|
||||
if (this._internalUpdate) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const cb = this.props[cameraEvents[e.type]];
|
||||
if (cb) {
|
||||
cb(e);
|
||||
}
|
||||
};
|
||||
|
||||
_fireEvent(baseFire: Function, event: string | MapboxEvent, properties?: object) {
|
||||
const map = this._map;
|
||||
const tr = map.transform;
|
||||
|
||||
const eventType = typeof event === 'string' ? event : event.type;
|
||||
switch (eventType) {
|
||||
case 'resize':
|
||||
this._renderTransform.resize(tr.width, tr.height);
|
||||
break;
|
||||
|
||||
case 'move':
|
||||
this._updateViewState(this.props, false);
|
||||
break;
|
||||
|
||||
case 'mousemove':
|
||||
case 'mouseout':
|
||||
// @ts-ignore
|
||||
this._updateHover(event);
|
||||
break;
|
||||
}
|
||||
if (typeof event === 'object' && event.type in cameraEvents) {
|
||||
(event as ViewStateChangeEvent).viewState = transformToViewState(tr);
|
||||
}
|
||||
// Replace map.transform with ours during the callbacks
|
||||
map.transform = this._renderTransform;
|
||||
baseFire.call(map, event, properties);
|
||||
map.transform = tr;
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
_render(baseRender: Function, arg: number) {
|
||||
const map = this._map;
|
||||
this._inRender = true;
|
||||
|
||||
if (this._moved) {
|
||||
this._internalUpdate = true;
|
||||
map.fire('move');
|
||||
|
||||
if (this._zoomed) {
|
||||
map.fire('zoom');
|
||||
this._zoomed = false;
|
||||
}
|
||||
|
||||
if (this._rotated) {
|
||||
map.fire('rotate');
|
||||
this._rotated = false;
|
||||
}
|
||||
|
||||
if (this._pitched) {
|
||||
map.fire('pitch');
|
||||
this._pitched = false;
|
||||
}
|
||||
|
||||
this._moved = false;
|
||||
this._internalUpdate = false;
|
||||
}
|
||||
|
||||
// map.transform will be swapped out in _onBeforeRender
|
||||
const tr = map.transform;
|
||||
baseRender.call(map, arg);
|
||||
map.transform = tr;
|
||||
this._inRender = false;
|
||||
|
||||
// We do not allow props to change during a render
|
||||
// When render is done, apply any pending changes
|
||||
if (this._nextProps) {
|
||||
this.setProps(this._nextProps);
|
||||
this._nextProps = null;
|
||||
}
|
||||
}
|
||||
|
||||
_onBeforeRepaint() {
|
||||
// Make sure camera matches the current props
|
||||
this._map.transform = this._renderTransform;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access token can be provided via one of:
|
||||
* mapboxAccessToken prop
|
||||
* access_token query parameter
|
||||
* MapboxAccessToken environment variable
|
||||
* REACT_APP_MAPBOX_ACCESS_TOKEN environment variable
|
||||
* @returns access token
|
||||
*/
|
||||
function getAccessTokenFromEnv(): string {
|
||||
let accessToken = null;
|
||||
|
||||
/* global location, process */
|
||||
if (typeof location !== 'undefined') {
|
||||
const match = /access_token=([^&\/]*)/.exec(location.search);
|
||||
accessToken = match && match[1];
|
||||
}
|
||||
|
||||
// Note: This depends on bundler plugins (e.g. webpack) importing environment correctly
|
||||
try {
|
||||
accessToken = accessToken || process.env.MapboxAccessToken;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
accessToken = accessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
60
src/utils/deep-equal.ts
Normal file
60
src/utils/deep-equal.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import type {PointLike} from 'mapbox-gl';
|
||||
|
||||
/**
|
||||
* Compare two points
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns true if the points are equal
|
||||
*/
|
||||
export function arePointsEqual(a?: PointLike, b?: PointLike): boolean {
|
||||
const ax = Array.isArray(a) ? a[0] : a ? a.x : 0;
|
||||
const ay = Array.isArray(a) ? a[1] : a ? a.y : 0;
|
||||
const bx = Array.isArray(b) ? b[0] : b ? b.x : 0;
|
||||
const by = Array.isArray(b) ? b[1] : b ? b.y : 0;
|
||||
return ax === bx && ay === by;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare any two objects
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns true if the objects are deep equal
|
||||
*/
|
||||
export function deepEqual(a: any, b: any): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
if (!Array.isArray(b) || a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof a === 'object' && typeof b === 'object') {
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
if (aKeys.length !== bKeys.length) {
|
||||
return false;
|
||||
}
|
||||
for (const key of aKeys) {
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
export default mapboxgl;
|
||||
// @ts-ignore
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
export default mapboxgl;
|
||||
76
src/utils/transform.ts
Normal file
76
src/utils/transform.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import mapboxgl from './mapboxgl';
|
||||
|
||||
import type {PaddingOptions, ViewState} from './types';
|
||||
|
||||
/**
|
||||
* Stub for mapbox's Transform class
|
||||
* https://github.com/mapbox/mapbox-gl-js/blob/main/src/geo/transform.js
|
||||
*/
|
||||
export type Transform = {
|
||||
center: {lng: number; lat: number};
|
||||
zoom: number;
|
||||
bearing: number;
|
||||
pitch: number;
|
||||
padding: PaddingOptions;
|
||||
|
||||
clone: () => Transform;
|
||||
resize: (width: number, height: number) => void;
|
||||
isPaddingEqual: (value: PaddingOptions) => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Capture a transform's current state
|
||||
* @param transform
|
||||
* @returns descriptor of the view state
|
||||
*/
|
||||
export function transformToViewState(tr: Transform): ViewState {
|
||||
return {
|
||||
longitude: tr.center.lng,
|
||||
latitude: tr.center.lat,
|
||||
zoom: tr.zoom,
|
||||
pitch: tr.pitch,
|
||||
bearing: tr.bearing,
|
||||
padding: tr.padding
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutate a transform to match the given view state
|
||||
* @param transform
|
||||
* @param viewState
|
||||
* @returns true if the transform has changed
|
||||
*/
|
||||
export function applyViewStateToTransform(
|
||||
tr: Transform,
|
||||
vs: ViewState | {viewState: ViewState}
|
||||
): boolean {
|
||||
// @ts-ignore
|
||||
const v: ViewState = vs.viewState || vs;
|
||||
let changed = false;
|
||||
|
||||
if ('longitude' in v && 'latitude' in v) {
|
||||
const center = tr.center;
|
||||
tr.center = new mapboxgl.LngLat(v.longitude, v.latitude);
|
||||
changed = changed || center !== tr.center;
|
||||
}
|
||||
if ('zoom' in v) {
|
||||
const zoom = tr.zoom;
|
||||
tr.zoom = v.zoom;
|
||||
changed = changed || zoom !== tr.zoom;
|
||||
}
|
||||
if ('bearing' in v) {
|
||||
const bearing = tr.bearing;
|
||||
tr.bearing = v.bearing;
|
||||
changed = changed || bearing !== tr.bearing;
|
||||
}
|
||||
if ('pitch' in v) {
|
||||
const pitch = tr.pitch;
|
||||
tr.pitch = v.pitch;
|
||||
changed = changed || pitch !== tr.pitch;
|
||||
}
|
||||
if (v.padding && !tr.isPaddingEqual(v.padding)) {
|
||||
changed = true;
|
||||
tr.padding = v.padding;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
51
src/utils/types.ts
Normal file
51
src/utils/types.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type {PaddingOptions, MapboxEvent} from 'mapbox-gl';
|
||||
|
||||
/** Defines the projection that the map should be rendered in */
|
||||
export type ProjectionSpecification = {
|
||||
name:
|
||||
| 'albers'
|
||||
| 'equalEarth'
|
||||
| 'equirectangular'
|
||||
| 'lambertConformalConic'
|
||||
| 'mercator'
|
||||
| 'naturalEarth'
|
||||
| 'winkelTripel';
|
||||
center?: [number, number];
|
||||
parallels?: [number, number];
|
||||
};
|
||||
|
||||
/** Describes the camera's state */
|
||||
export type ViewState = {
|
||||
/** Longitude at map center */
|
||||
longitude?: number;
|
||||
/** Latitude at map center */
|
||||
latitude?: number;
|
||||
/** Map zoom level */
|
||||
zoom?: number;
|
||||
/** Map rotation bearing in degrees counter-clockwise from north */
|
||||
bearing?: number;
|
||||
/** Map angle in degrees at which the camera is looking at the ground */
|
||||
pitch?: number;
|
||||
/** Dimensions in pixels applied on each side of the viewport for shifting the vanishing point. */
|
||||
padding?: PaddingOptions;
|
||||
};
|
||||
|
||||
export type ViewStateChangeEvent = MapboxEvent & {
|
||||
viewState: ViewState;
|
||||
};
|
||||
|
||||
// re-export mapbox types
|
||||
export type {
|
||||
MapboxOptions,
|
||||
Style,
|
||||
PaddingOptions,
|
||||
MapMouseEvent,
|
||||
MapLayerMouseEvent,
|
||||
MapLayerTouchEvent,
|
||||
MapWheelEvent,
|
||||
MapDataEvent,
|
||||
MapboxEvent,
|
||||
ErrorEvent,
|
||||
MapboxGeoJSONFeature,
|
||||
Map as MapboxMap
|
||||
} from 'mapbox-gl';
|
||||
7
src/utils/use-isomorphic-layout-effect.ts
Normal file
7
src/utils/use-isomorphic-layout-effect.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// From https://github.com/streamich/react-use/blob/master/src/useIsomorphicLayoutEffect.ts
|
||||
// useLayoutEffect but does not trigger warning in server-side rendering
|
||||
import {useEffect, useLayoutEffect} from 'react';
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
export default useIsomorphicLayoutEffect;
|
||||
10
test/node.js
10
test/node.js
@ -1,5 +1,5 @@
|
||||
const register = require('@babel/register').default;
|
||||
|
||||
register({ extensions: ['.ts', '.tsx', '.js'] });
|
||||
|
||||
require('./src');
|
||||
const register = require('@babel/register').default;
|
||||
|
||||
register({extensions: ['.ts', '.tsx', '.js']});
|
||||
|
||||
require('./src');
|
||||
|
||||
@ -1 +1,4 @@
|
||||
import './components/map.spec';
|
||||
|
||||
import './utils/deep-equal.spec';
|
||||
import './utils/transform.spec';
|
||||
|
||||
95
test/src/utils/deep-equal.spec.js
Normal file
95
test/src/utils/deep-equal.spec.js
Normal file
@ -0,0 +1,95 @@
|
||||
import test from 'tape-promise/tape';
|
||||
import {deepEqual, arePointsEqual} from 'react-map-gl/utils/deep-equal';
|
||||
|
||||
test('deepEqual', t => {
|
||||
const testCases = [
|
||||
{
|
||||
a: null,
|
||||
b: null,
|
||||
result: true
|
||||
},
|
||||
{
|
||||
a: undefined,
|
||||
b: 0,
|
||||
result: false
|
||||
},
|
||||
{
|
||||
a: [1, 2, 3],
|
||||
b: [1, 2, 3],
|
||||
result: true
|
||||
},
|
||||
{
|
||||
a: [1, 2],
|
||||
b: [1, 2, 3],
|
||||
result: false
|
||||
},
|
||||
{
|
||||
a: [1, 2],
|
||||
b: {0: 1, 1: 2},
|
||||
result: false
|
||||
},
|
||||
{
|
||||
a: {x: 0, y: 0, offset: [1, -1]},
|
||||
b: {x: 0, y: 0, offset: [1, -1]},
|
||||
result: true
|
||||
},
|
||||
{
|
||||
a: {x: 0, y: 0},
|
||||
b: {x: 0, y: 0, offset: [1, -1]},
|
||||
result: false
|
||||
},
|
||||
{
|
||||
a: {x: 0, y: 0, z: 0},
|
||||
b: {x: 0, y: 0, offset: [1, -1]},
|
||||
result: false
|
||||
}
|
||||
];
|
||||
|
||||
for (const {a, b, result} of testCases) {
|
||||
t.is(deepEqual(a, b), result, `${JSON.stringify(a)} vs ${JSON.stringify(b)}`);
|
||||
if (a !== b) {
|
||||
t.is(deepEqual(b, a), result, `${JSON.stringify(b)} vs ${JSON.stringify(a)}`);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('arePointsEqual', t => {
|
||||
const testCases = [
|
||||
{
|
||||
a: undefined,
|
||||
b: undefined,
|
||||
result: true
|
||||
},
|
||||
{
|
||||
a: undefined,
|
||||
b: [0, 0],
|
||||
result: true
|
||||
},
|
||||
{
|
||||
a: undefined,
|
||||
b: [0, 1],
|
||||
result: false
|
||||
},
|
||||
{
|
||||
a: undefined,
|
||||
b: [1, 0],
|
||||
result: false
|
||||
},
|
||||
{
|
||||
a: {x: 1, y: 1},
|
||||
b: [1, 1],
|
||||
result: true
|
||||
}
|
||||
];
|
||||
|
||||
for (const {a, b, result} of testCases) {
|
||||
t.is(arePointsEqual(a, b), result, `${JSON.stringify(a)}, ${JSON.stringify(b)}`);
|
||||
if (a !== b) {
|
||||
t.is(arePointsEqual(b, a), result, `${JSON.stringify(b)}, ${JSON.stringify(a)}`);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
128
test/src/utils/transform.js
Normal file
128
test/src/utils/transform.js
Normal file
@ -0,0 +1,128 @@
|
||||
function wrap(n, min, max) {
|
||||
const d = max - min;
|
||||
const w = ((((n - min) % d) + d) % d) + min;
|
||||
return w === min ? max : w;
|
||||
}
|
||||
|
||||
function clamp(n, min, max) {
|
||||
return Math.min(max, Math.max(min, n));
|
||||
}
|
||||
|
||||
/**
|
||||
* A dummy class that simulates mapbox's EdgeInsets
|
||||
*/
|
||||
class EdgeInsets {
|
||||
constructor(top = 0, bottom = 0, left = 0, right = 0) {
|
||||
this.top = top;
|
||||
this.bottom = bottom;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
set(target) {
|
||||
if (target.top !== null) this.top = target.top;
|
||||
if (target.bottom !== null) this.bottom = target.bottom;
|
||||
if (target.left !== null) this.left = target.left;
|
||||
if (target.right !== null) this.right = target.right;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return (
|
||||
this.top === other.top &&
|
||||
this.bottom === other.bottom &&
|
||||
this.left === other.left &&
|
||||
this.right === other.right
|
||||
);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
top: this.top,
|
||||
bottom: this.bottom,
|
||||
left: this.left,
|
||||
right: this.right
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dummy class that simulates mapbox's Transform
|
||||
*/
|
||||
export default class Transform {
|
||||
constructor() {
|
||||
this.minZoom = 0;
|
||||
this.maxZoom = 22;
|
||||
this.minPitch = 0;
|
||||
this.maxPitch = 60;
|
||||
this.minLat = -85.051129;
|
||||
this.maxLat = 85.051129;
|
||||
this.minLng = -180;
|
||||
this.maxLng = 180;
|
||||
this.width = 1;
|
||||
this.height = 1;
|
||||
this._center = {lng: 0, lat: 0};
|
||||
this._zoom = 0;
|
||||
this.angle = 0;
|
||||
this._pitch = 0;
|
||||
this._edgeInsets = new EdgeInsets();
|
||||
}
|
||||
|
||||
get bearing() {
|
||||
return wrap(this.rotation, -180, 180);
|
||||
}
|
||||
|
||||
set bearing(bearing) {
|
||||
this.rotation = bearing;
|
||||
}
|
||||
|
||||
get rotation() {
|
||||
return (-this.angle / Math.PI) * 180;
|
||||
}
|
||||
|
||||
set rotation(rotation) {
|
||||
const b = (-rotation * Math.PI) / 180;
|
||||
if (this.angle === b) return;
|
||||
this.angle = b;
|
||||
}
|
||||
|
||||
get pitch() {
|
||||
return (this._pitch / Math.PI) * 180;
|
||||
}
|
||||
set pitch(pitch) {
|
||||
const p = (clamp(pitch, this.minPitch, this.maxPitch) / 180) * Math.PI;
|
||||
if (this._pitch === p) return;
|
||||
this._pitch = p;
|
||||
}
|
||||
|
||||
get zoom() {
|
||||
return this._zoom;
|
||||
}
|
||||
set zoom(zoom) {
|
||||
const z = Math.min(Math.max(zoom, this.minZoom), this.maxZoom);
|
||||
if (this._zoom === z) return;
|
||||
this._zoom = z;
|
||||
}
|
||||
|
||||
get center() {
|
||||
return this._center;
|
||||
}
|
||||
set center(center) {
|
||||
if (center.lat === this._center.lat && center.lng === this._center.lng) return;
|
||||
this._center = center;
|
||||
}
|
||||
|
||||
get padding() {
|
||||
return this._edgeInsets.toJSON();
|
||||
}
|
||||
set padding(padding) {
|
||||
if (this._edgeInsets.equals(padding)) return;
|
||||
// Update edge-insets inplace
|
||||
this._edgeInsets.set(padding);
|
||||
}
|
||||
|
||||
isPaddingEqual(padding) {
|
||||
return this._edgeInsets.equals(padding);
|
||||
}
|
||||
}
|
||||
94
test/src/utils/transform.spec.js
Normal file
94
test/src/utils/transform.spec.js
Normal file
@ -0,0 +1,94 @@
|
||||
import test from 'tape-promise/tape';
|
||||
import {transformToViewState, applyViewStateToTransform} from 'react-map-gl/utils/transform';
|
||||
|
||||
import Transform from './transform';
|
||||
|
||||
test('applyViewStateToTransform', t => {
|
||||
const tr = new Transform();
|
||||
|
||||
let changed = applyViewStateToTransform(tr, {});
|
||||
t.notOk(changed, 'empty view state');
|
||||
|
||||
changed = applyViewStateToTransform(tr, {longitude: -10, latitude: 5});
|
||||
t.ok(changed, 'center changed');
|
||||
t.deepEqual(
|
||||
transformToViewState(tr),
|
||||
{
|
||||
longitude: -10,
|
||||
latitude: 5,
|
||||
zoom: 0,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
padding: {left: 0, right: 0, top: 0, bottom: 0}
|
||||
},
|
||||
'view state is correct'
|
||||
);
|
||||
|
||||
changed = applyViewStateToTransform(tr, {zoom: -1});
|
||||
t.notOk(changed, 'zoom is clamped');
|
||||
|
||||
changed = applyViewStateToTransform(tr, {zoom: 10});
|
||||
t.ok(changed, 'zoom changed');
|
||||
t.deepEqual(
|
||||
transformToViewState(tr),
|
||||
{
|
||||
longitude: -10,
|
||||
latitude: 5,
|
||||
zoom: 10,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
padding: {left: 0, right: 0, top: 0, bottom: 0}
|
||||
},
|
||||
'view state is correct'
|
||||
);
|
||||
|
||||
changed = applyViewStateToTransform(tr, {pitch: 30});
|
||||
t.ok(changed, 'pitch changed');
|
||||
t.deepEqual(
|
||||
transformToViewState(tr),
|
||||
{
|
||||
longitude: -10,
|
||||
latitude: 5,
|
||||
zoom: 10,
|
||||
pitch: 30,
|
||||
bearing: 0,
|
||||
padding: {left: 0, right: 0, top: 0, bottom: 0}
|
||||
},
|
||||
'view state is correct'
|
||||
);
|
||||
|
||||
changed = applyViewStateToTransform(tr, {bearing: 270});
|
||||
t.ok(changed, 'bearing changed');
|
||||
t.deepEqual(
|
||||
transformToViewState(tr),
|
||||
{
|
||||
longitude: -10,
|
||||
latitude: 5,
|
||||
zoom: 10,
|
||||
pitch: 30,
|
||||
bearing: -90,
|
||||
padding: {left: 0, right: 0, top: 0, bottom: 0}
|
||||
},
|
||||
'view state is correct'
|
||||
);
|
||||
|
||||
changed = applyViewStateToTransform(tr, {padding: {left: 10, right: 10, top: 10, bottom: 10}});
|
||||
t.ok(changed, 'padding changed');
|
||||
t.deepEqual(
|
||||
transformToViewState(tr),
|
||||
{
|
||||
longitude: -10,
|
||||
latitude: 5,
|
||||
zoom: 10,
|
||||
pitch: 30,
|
||||
bearing: -90,
|
||||
padding: {left: 10, right: 10, top: 10, bottom: 10}
|
||||
},
|
||||
'view state is correct'
|
||||
);
|
||||
|
||||
changed = applyViewStateToTransform(tr, {viewState: {pitch: 30}});
|
||||
t.notOk(changed, 'nothing changed');
|
||||
|
||||
t.end();
|
||||
});
|
||||
@ -1,25 +1,24 @@
|
||||
const webpack = require('webpack');
|
||||
const getWebpackConfig = require('ocular-dev-tools/config/webpack.config');
|
||||
|
||||
const BABEL_CONFIG = {
|
||||
presets: ['@babel/env', '@babel/react'],
|
||||
plugins: ['version-inline', '@babel/proposal-class-properties']
|
||||
};
|
||||
const {getWebpackConfig} = require('ocular-dev-tools');
|
||||
|
||||
module.exports = env => {
|
||||
const config = getWebpackConfig(env);
|
||||
|
||||
config.module.rules.push({
|
||||
// This is required to handle inline worker!
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: BABEL_CONFIG
|
||||
config.resolve = {...config.resolve, extensions: ['.ts', '.tsx', '.js', '.json']};
|
||||
|
||||
config.module.rules = [
|
||||
...config.module.rules.filter(r => r.loader !== 'babel-loader'),
|
||||
{
|
||||
// Compile source using babel
|
||||
test: /(\.js|\.ts|\.tsx)$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: [/node_modules/],
|
||||
options: {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties']
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
config.plugins = (config.plugins || []).concat([
|
||||
new webpack.DefinePlugin({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user