@vis.gl/react-google-maps
Version:
React components and hooks for the Google Maps JavaScript API
1,319 lines (1,293 loc) • 178 kB
JavaScript
import * as React from 'react';
import React__default, { useMemo, useState, useReducer, useCallback, useEffect, useRef, useContext, useLayoutEffect, forwardRef, useImperativeHandle, Children, createContext } from 'react';
import { createPortal } from 'react-dom';
// This file is automatically updated by the build process.
const VERSION = '1.9.0';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
const MSG_REPEATED_SET_OPTIONS = (options) => `The setOptions() function should only be called once. The options passed ` +
`to the additional call (${JSON.stringify(options)}) will be ignored.`;
const MSG_IMPORT_LIBRARY_EXISTS = (options) => `The google.maps.importLibrary() function is already defined, and ` +
`@googlemaps/js-api-loader will use the existing function instead of ` +
`overwriting it. The options passed to setOptions ` +
`(${JSON.stringify(options)}) will be ignored.`;
const MSG_SET_OPTIONS_NOT_CALLED = "No options were set before calling importLibrary. Make sure to configure " +
"the loader using setOptions().";
const MSG_SCRIPT_ELEMENT_EXISTS = "There already is a script loading the Google Maps JavaScript " +
"API, and no google.maps.importLibrary function is defined. " +
"@googlemaps/js-api-loader will proceed to bootstrap the API " +
"with the specified options, but the existing script might cause " +
"problems using the API. Make sure to remove the script " +
"loading the API.";
const MSG_API_KEY_USED = "The 'apiKey' parameter was used in setOptions(), but 'key' is the correct " +
"parameter name. Please update your configuration.";
const MSG_TRUSTED_TYPES_POLICY_FAILED = (policyName, error) => `Failed to create Trusted Types policy "${policyName}": ${error instanceof Error ? error.message : String(error)}.\n\n` +
`If your Content Security Policy uses "require-trusted-types-for 'script'", ` +
`allow this policy with "trusted-types ${policyName} google-maps-api-loader google-maps-api#html lit-html". ` +
`The "google-maps-api-loader", "lit-html", and "google-maps-api#html" policies are required for full Maps JavaScript API execution. ` +
`Falling back to a string script URL.`;
const __DEV__$1 = process.env.NODE_ENV !== 'production';
const logDevWarning = __DEV__$1
? (message) => {
console.warn(`[@googlemaps/js-api-loader] ${message}`);
}
: () => { };
const logDevNotice = __DEV__$1
? (message) => {
console.info(`[@googlemaps/js-api-loader] ${message}`);
}
: () => { };
/*
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const TRUSTED_TYPES_POLICY_NAME = "@googlemaps/js-api-loader";
const fallbackPolicy = { createScriptURL: (url) => url };
let policy;
/*
* Tries to create a Trusted Types policy when supported. Falls back to a string passthrough
* when Trusted Types is unsupported, blocked by CSP, or already registered.
*/
function getPolicy() {
if (policy) {
return policy;
}
const trustedTypes = globalThis.trustedTypes;
if (!trustedTypes) {
policy = fallbackPolicy;
return policy;
}
try {
policy = trustedTypes.createPolicy(TRUSTED_TYPES_POLICY_NAME, {
createScriptURL: (url) => url,
});
}
catch (e) {
logDevWarning(MSG_TRUSTED_TYPES_POLICY_FAILED(TRUSTED_TYPES_POLICY_NAME, e));
policy = fallbackPolicy;
}
return policy;
}
function setScriptSrc(script, src) {
script.src = getPolicy().createScriptURL(src);
}
/*
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const bootstrap = bootstrapParams => {
var bootstrapPromise;
var script;
var bootstrapParamsKey;
var PRODUCT_NAME = "The Google Maps JavaScript API";
var GOOGLE = "google";
var IMPORT_API_NAME = "importLibrary";
var PENDING_BOOTSTRAP_KEY = "__ib__";
var doc = document;
var global_ = window;
var google_ = global_[GOOGLE] || (global_[GOOGLE] = {});
var namespace = google_.maps || (google_.maps = {});
var libraries = new Set();
var searchParams = new URLSearchParams();
var triggerBootstrap = () => bootstrapPromise || (bootstrapPromise = new Promise(async(resolve, reject) => {
await (script = doc.createElement("script"));
searchParams.set("libraries", [...libraries] + "");
for (bootstrapParamsKey in bootstrapParams) {
searchParams.set(bootstrapParamsKey.replace(/[A-Z]/g, g => "_" + g[0].toLowerCase()), bootstrapParams[bootstrapParamsKey]);
}
searchParams.set("callback", GOOGLE + ".maps." + PENDING_BOOTSTRAP_KEY);
setScriptSrc(script, "https://maps.googleapis.com/maps/api/js?" + searchParams);
namespace[PENDING_BOOTSTRAP_KEY] = resolve;
script.onerror = () => bootstrapPromise = reject(Error(PRODUCT_NAME + " could not load."));
script.nonce = doc.querySelector("script[nonce]")?.nonce || "";
doc.head.append(script);
}));
namespace[IMPORT_API_NAME] ? console.warn(PRODUCT_NAME + " only loads once. Ignoring:", bootstrapParams) : namespace[IMPORT_API_NAME] = (libraryName, ...args) => libraries.add(libraryName) && triggerBootstrap().then(() => namespace[IMPORT_API_NAME](libraryName, ...args));
};
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const __DEV__ = process.env.NODE_ENV !== "production";
let setOptionsWasCalled_ = false;
/**
* Sets the options for the Maps JavaScript API.
*
* Has to be called before any library is loaded.
*
* See https://developers.google.com/maps/documentation/javascript/load-maps-js-api#required_parameters
* for the full documentation of available options.
*
* @param options The options to set.
*/
function setOptions(options) {
if (setOptionsWasCalled_) {
logDevWarning(MSG_REPEATED_SET_OPTIONS(options));
return;
}
if (options.apiKey) {
logDevWarning(MSG_API_KEY_USED);
if (!options.key) {
options.key = options.apiKey;
}
}
installImportLibrary_(options);
setOptionsWasCalled_ = true;
}
async function importLibrary(libraryName) {
if (!setOptionsWasCalled_) {
logDevWarning(MSG_SET_OPTIONS_NOT_CALLED);
}
if (!window?.google?.maps?.importLibrary) {
throw new Error("google.maps.importLibrary is not installed.");
}
return (await google.maps.importLibrary(libraryName));
}
/**
* The installImportLibrary_ function makes sure that a usable version of the
* `google.maps.importLibrary` function exists.
*/
function installImportLibrary_(options) {
const importLibraryExists = Boolean(window.google?.maps?.importLibrary);
if (importLibraryExists) {
logDevNotice(MSG_IMPORT_LIBRARY_EXISTS(options));
}
else if (__DEV__) {
const scriptEl = document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]');
if (scriptEl) {
logDevWarning(MSG_SCRIPT_ELEMENT_EXISTS);
}
}
// If the google.maps.importLibrary function already exists, bootstrap()
// won't do anything, so we won't call it
if (!importLibraryExists) {
bootstrap(options);
}
}
const APILoadingStatus = {
NOT_LOADED: 'NOT_LOADED',
LOADING: 'LOADING',
LOADED: 'LOADED',
FAILED: 'FAILED',
AUTH_FAILURE: 'AUTH_FAILURE'
};
const DEFAULT_SOLUTION_CHANNEL = 'GMP_visgl_rgmlibrary_v1_default';
const DEFAULT_INTERNAL_USAGE_ATTRIBUTION_IDS = [
`gmp_visgl_reactgooglemaps_v${VERSION}`
];
const APIProviderContext = React__default.createContext(null);
// loading the Maps JavaScript API can only happen once in the runtime, so these
// variables are kept at the module level.
let loadingStatus = APILoadingStatus.NOT_LOADED;
let serializedApiParams;
const listeners = new Set();
/**
* Called to update the local status and notify the listeners for any mounted
* components.
* @internal
*/
function updateLoadingStatus(status) {
if (status === loadingStatus) {
return;
}
loadingStatus = status;
listeners.forEach(listener => listener(loadingStatus));
}
/**
* Local hook to set up the map-instance management context.
* @internal
*/
function useMapInstances() {
const [mapInstances, setMapInstances] = useState({});
const addMapInstance = (mapInstance, id = 'default') => {
setMapInstances(instances => (Object.assign(Object.assign({}, instances), { [id]: mapInstance })));
};
const removeMapInstance = (id = 'default') => {
setMapInstances((_a) => {
var _b = id; _a[_b]; var remaining = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
return remaining;
});
};
const clearMapInstances = () => {
setMapInstances({});
};
return { mapInstances, addMapInstance, removeMapInstance, clearMapInstances };
}
/**
* local hook to set up the 3D map-instance management context.
*/
function useMap3DInstances() {
const [map3dInstances, setMap3DInstances] = useState({});
const addMap3DInstance = (map3dInstance, id = 'default') => {
setMap3DInstances(instances => (Object.assign(Object.assign({}, instances), { [id]: map3dInstance })));
};
const removeMap3DInstance = (id = 'default') => {
setMap3DInstances((_a) => {
var _b = id; _a[_b]; var remaining = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
return remaining;
});
};
const clearMap3DInstances = () => {
setMap3DInstances({});
};
return {
map3dInstances,
addMap3DInstance,
removeMap3DInstance,
clearMap3DInstances
};
}
/**
* Local hook to handle the loading of the maps API.
* @internal
*/
function useGoogleMapsApiLoader(props) {
const { onLoad, onError, apiKey, version, libraries = [], region, language, authReferrerPolicy, channel, solutionChannel, fetchAppCheckToken } = props;
const [status, setStatus] = useState(loadingStatus);
const [loadedLibraries, addLoadedLibrary] = useReducer((loadedLibraries, action) => {
return loadedLibraries[action.name]
? loadedLibraries
: Object.assign(Object.assign({}, loadedLibraries), { [action.name]: action.value });
}, {});
const currentSerializedParams = useMemo(() => {
const params = {
apiKey,
version,
libraries: libraries.join(','),
region,
language,
authReferrerPolicy,
channel,
solutionChannel
};
return JSON.stringify(params);
}, [
apiKey,
version,
libraries,
region,
language,
authReferrerPolicy,
channel,
solutionChannel
]);
const importLibraryCallback = useCallback((name) => __awaiter(this, void 0, void 0, function* () {
if (loadedLibraries[name]) {
return loadedLibraries[name];
}
const res = yield importLibrary(name);
addLoadedLibrary({ name, value: res });
return res;
}), [loadedLibraries]);
// effect: we want to get notified of global loading-status changes
useEffect(() => {
listeners.add(setStatus);
// sync component state on mount (shouldn't be different from the initial state)
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional synchronization of status from singleton
setStatus(loadingStatus);
return () => {
listeners.delete(setStatus);
};
}, []);
// effect: set and store options
useEffect(() => {
(() => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
// This indicates that the API has been loaded with a different set of parameters.
// While this is not blocking, it's not recommended and we should warn the user.
if (serializedApiParams &&
serializedApiParams !== currentSerializedParams) {
console.warn(`The Google Maps JavaScript API has already been loaded with different parameters. ` +
`The new parameters will be ignored. If you need to use different parameters, ` +
`please refresh the page.`);
}
const librariesToLoad = [
'core',
'maps',
...libraries
];
const options = Object.fromEntries(Object.entries({
key: apiKey,
v: version,
libraries,
region,
language,
authReferrerPolicy
}).filter(([, value]) => value !== undefined));
if (channel !== undefined && channel >= 0 && channel <= 999) {
options.channel = String(channel);
}
// solution-channel: when undefined, use the default; otherwise use
// an explicit value.
if (solutionChannel === undefined) {
options.solutionChannel = DEFAULT_SOLUTION_CHANNEL;
}
else if (solutionChannel !== '') {
options.solutionChannel = solutionChannel;
}
// If the google.maps namespace is already available, the API has been loaded externally.
if ((_b = (_a = window.google) === null || _a === void 0 ? void 0 : _a.maps) === null || _b === void 0 ? void 0 : _b.importLibrary) {
const shouldUpdateLoadingStatus = !serializedApiParams;
if (shouldUpdateLoadingStatus) {
serializedApiParams = currentSerializedParams;
setOptions(options);
}
yield Promise.all(librariesToLoad.map(name => importLibraryCallback(name)));
if (shouldUpdateLoadingStatus) {
updateLoadingStatus(APILoadingStatus.LOADED);
}
if (onLoad)
onLoad();
return;
}
// Abort if the API is already loading or has been loaded.
if (loadingStatus === APILoadingStatus.LOADING ||
loadingStatus === APILoadingStatus.LOADED) {
if (loadingStatus === APILoadingStatus.LOADED && onLoad)
onLoad();
return;
}
serializedApiParams = currentSerializedParams;
updateLoadingStatus(APILoadingStatus.LOADING);
// this will actually trigger loading the maps API
setOptions(options);
// wait for all requested libraries (inluding 'core' and 'maps') to
// finish loading
yield Promise.all(librariesToLoad.map(name => importLibraryCallback(name)));
updateLoadingStatus(APILoadingStatus.LOADED);
if (onLoad) {
onLoad();
}
}
catch (error) {
updateLoadingStatus(APILoadingStatus.FAILED);
if (onError) {
onError(error);
}
else {
console.error('The Google Maps JavaScript API failed to load.', error);
}
}
}))();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentSerializedParams, onLoad, onError, importLibraryCallback, libraries]);
// set the fetchAppCheckToken if provided
useEffect(() => {
if (status !== APILoadingStatus.LOADED)
return;
const settings = google.maps.Settings.getInstance();
if (fetchAppCheckToken) {
settings.fetchAppCheckToken = fetchAppCheckToken;
}
}, [status, fetchAppCheckToken]);
return {
status,
loadedLibraries,
importLibrary: importLibraryCallback
};
}
function useInternalUsageAttributionIds(props) {
return useMemo(() => props.disableUsageAttribution
? null
: DEFAULT_INTERNAL_USAGE_ATTRIBUTION_IDS, [props.disableUsageAttribution]);
}
/**
* Component to wrap the components from this library and load the Google Maps JavaScript API
*/
const APIProvider = props => {
const { children } = props, loaderProps = __rest(props, ["children"]);
const { mapInstances, addMapInstance, removeMapInstance, clearMapInstances } = useMapInstances();
const { map3dInstances, addMap3DInstance, removeMap3DInstance, clearMap3DInstances } = useMap3DInstances();
const { status, loadedLibraries, importLibrary } = useGoogleMapsApiLoader(loaderProps);
const internalUsageAttributionIds = useInternalUsageAttributionIds(loaderProps);
const contextValue = useMemo(() => ({
mapInstances,
addMapInstance,
removeMapInstance,
clearMapInstances,
map3dInstances,
addMap3DInstance,
removeMap3DInstance,
clearMap3DInstances,
status,
loadedLibraries,
importLibrary,
internalUsageAttributionIds
}), [
mapInstances,
addMapInstance,
removeMapInstance,
clearMapInstances,
map3dInstances,
addMap3DInstance,
removeMap3DInstance,
clearMap3DInstances,
status,
loadedLibraries,
importLibrary,
internalUsageAttributionIds
]);
return (React__default.createElement(APIProviderContext.Provider, { value: contextValue }, children));
};
/**
* @internal
* Resets module-level state for testing purposes only.
* This should never be used in production code.
*/
function __resetModuleState() {
loadingStatus = APILoadingStatus.NOT_LOADED;
serializedApiParams = undefined;
listeners.clear();
}
/**
* Sets up effects to bind event-handlers for all event-props in MapEventProps.
* @internal
*/
function useMapEvents(map, props) {
// note: calling a useEffect hook from within a loop is prohibited by the
// rules of hooks, but it's ok here since it's unconditional and the number
// and order of iterations is always strictly the same.
// (see https://legacy.reactjs.org/docs/hooks-rules.html)
for (const propName of eventPropNames) {
// fixme: this cast is essentially a 'trust me, bro' for typescript, but
// a proper solution seems way too complicated right now
const handler = props[propName];
const eventType = propNameToEventType[propName];
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (!map)
return;
if (!handler)
return;
const listener = google.maps.event.addListener(map, eventType, (ev) => {
handler(createMapEvent(eventType, map, ev));
});
return () => listener.remove();
}, [map, eventType, handler]);
}
}
/**
* Create the wrapped map-events used for the event-props.
* @param type the event type as it is specified to the maps api
* @param map the map instance the event originates from
* @param srcEvent the source-event if there is one.
*/
function createMapEvent(type, map, srcEvent) {
var _a;
const ev = {
type,
map,
detail: {},
stoppable: false,
stop: () => { }
};
if (cameraEventTypes.includes(type)) {
const camEvent = ev;
const center = map.getCenter();
const zoom = map.getZoom();
const heading = map.getHeading() || 0;
const tilt = map.getTilt() || 0;
const bounds = map.getBounds();
if (!center || !bounds || !Number.isFinite(zoom)) {
console.warn('[createEvent] at least one of the values from the map ' +
'returned undefined. This is not expected to happen. Please ' +
'report an issue at https://github.com/visgl/react-google-maps/issues/new');
}
camEvent.detail = {
center: (center === null || center === void 0 ? void 0 : center.toJSON()) || { lat: 0, lng: 0 },
zoom: zoom || 0,
heading: heading,
tilt: tilt,
bounds: (bounds === null || bounds === void 0 ? void 0 : bounds.toJSON()) || {
north: 90,
east: 180,
south: -90,
west: -180
}
};
return camEvent;
}
else if (mouseEventTypes.includes(type)) {
if (!srcEvent)
throw new Error('[createEvent] mouse events must provide a srcEvent');
const mouseEvent = ev;
mouseEvent.domEvent = srcEvent.domEvent;
mouseEvent.stoppable = true;
mouseEvent.stop = () => srcEvent.stop();
mouseEvent.detail = {
latLng: ((_a = srcEvent.latLng) === null || _a === void 0 ? void 0 : _a.toJSON()) || null,
placeId: srcEvent.placeId
};
return mouseEvent;
}
return ev;
}
/**
* maps the camelCased names of event-props to the corresponding event-types
* used in the maps API.
*/
const propNameToEventType = {
onBoundsChanged: 'bounds_changed',
onCenterChanged: 'center_changed',
onClick: 'click',
onContextmenu: 'contextmenu',
onDblclick: 'dblclick',
onDrag: 'drag',
onDragend: 'dragend',
onDragstart: 'dragstart',
onHeadingChanged: 'heading_changed',
onIdle: 'idle',
onIsFractionalZoomEnabledChanged: 'isfractionalzoomenabled_changed',
onMapCapabilitiesChanged: 'mapcapabilities_changed',
onMapTypeIdChanged: 'maptypeid_changed',
onMousemove: 'mousemove',
onMouseout: 'mouseout',
onMouseover: 'mouseover',
onProjectionChanged: 'projection_changed',
onRenderingTypeChanged: 'renderingtype_changed',
onTilesLoaded: 'tilesloaded',
onTiltChanged: 'tilt_changed',
onZoomChanged: 'zoom_changed',
// note: onCameraChanged is an alias for the bounds_changed event,
// since that is going to be fired in every situation where the camera is
// updated.
onCameraChanged: 'bounds_changed'
};
const cameraEventTypes = [
'bounds_changed',
'center_changed',
'heading_changed',
'tilt_changed',
'zoom_changed'
];
const mouseEventTypes = [
'click',
'contextmenu',
'dblclick',
'mousemove',
'mouseout',
'mouseover'
];
const eventPropNames = Object.keys(propNameToEventType);
/* eslint-disable react-hooks/refs */
// refs should not be used in render because changes to refs won't
// trigger a re-render, making them unreliable for holding state.
// In this case though, that is exactly what we want.
function useMemoized(value, isEqual) {
const ref = useRef(value);
if (!isEqual(value, ref.current)) {
ref.current = value;
}
return ref.current;
}
function useCustomCompareEffect(effect, dependencies, isEqual) {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(effect, [useMemoized(dependencies, isEqual)]);
}
const { getOwnPropertyNames, getOwnPropertySymbols } = Object;
// eslint-disable-next-line @typescript-eslint/unbound-method
const { hasOwnProperty } = Object.prototype;
/**
* Combine two comparators into a single comparators.
*/
function combineComparators(comparatorA, comparatorB) {
return function isEqual(a, b, state) {
return comparatorA(a, b, state) && comparatorB(a, b, state);
};
}
/**
* Wrap the provided `areItemsEqual` method to manage the circular state, allowing
* for circular references to be safely included in the comparison without creating
* stack overflows.
*/
function createIsCircular(areItemsEqual) {
return function isCircular(a, b, state) {
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') {
return areItemsEqual(a, b, state);
}
const { cache } = state;
const cachedA = cache.get(a);
const cachedB = cache.get(b);
if (cachedA && cachedB) {
return cachedA === b && cachedB === a;
}
cache.set(a, b);
cache.set(b, a);
const result = areItemsEqual(a, b, state);
cache.delete(a);
cache.delete(b);
return result;
};
}
/**
* Get the properties to strictly examine, which include both own properties that are
* not enumerable and symbol properties.
*/
function getStrictProperties(object) {
return getOwnPropertyNames(object).concat(getOwnPropertySymbols(object));
}
/**
* Whether the object contains the property passed as an own property.
*/
const hasOwn =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Object.hasOwn || ((object, property) => hasOwnProperty.call(object, property));
const PREACT_VNODE = '__v';
const PREACT_OWNER = '__o';
const REACT_OWNER = '_owner';
const { getOwnPropertyDescriptor, keys } = Object;
/**
* Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevalue) basis.
* Simplified, this maps to if the two values are referentially equal to one another (`a === b`) or both are `NaN`.
*
* @note
* When available in the environment, this is just a re-export of the global
* [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) method.
*/
const sameValueEqual =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Object.is
|| function sameValueEqual(a, b) {
return a === b ? a !== 0 || 1 / a === 1 / b : a !== a && b !== b;
};
/**
* Whether the values passed are equal based on a
* [Strict Equality Comparison](https://262.ecma-international.org/7.0/#sec-strict-equality-comparison) basis.
* Simplified, this maps to if the two values are referentially equal to one another (`a === b`).
*
* @note
* This is mainly available as a convenience function, such as being a default when a function to determine equality between
* two objects is used.
*/
function strictEqual(a, b) {
return a === b;
}
/**
* Whether the array buffers are equal in value.
*/
function areArrayBuffersEqual(a, b) {
return a.byteLength === b.byteLength && areTypedArraysEqual(new Uint8Array(a), new Uint8Array(b));
}
/**
* Whether the arrays are equal in value.
*/
function areArraysEqual(a, b, state) {
let index = a.length;
if (b.length !== index) {
return false;
}
while (index-- > 0) {
if (!state.equals(a[index], b[index], index, index, a, b, state)) {
return false;
}
}
return true;
}
/**
* Whether the dataviews are equal in value.
*/
function areDataViewsEqual(a, b) {
return (a.byteLength === b.byteLength
&& areTypedArraysEqual(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), new Uint8Array(b.buffer, b.byteOffset, b.byteLength)));
}
/**
* Whether the dates passed are equal in value.
*/
function areDatesEqual(a, b) {
return sameValueEqual(a.getTime(), b.getTime());
}
/**
* Whether the errors passed are equal in value.
*/
function areErrorsEqual(a, b) {
return a.name === b.name && a.message === b.message && a.cause === b.cause && a.stack === b.stack;
}
/**
* Whether the `Map`s are equal in value.
*/
function areMapsEqual(a, b, state) {
const size = a.size;
if (size !== b.size) {
return false;
}
if (!size) {
return true;
}
const matchedIndices = new Array(size);
const aIterable = a.entries();
let aResult;
let bResult;
let index = 0;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while ((aResult = aIterable.next())) {
if (aResult.done) {
break;
}
const bIterable = b.entries();
let hasMatch = false;
let matchIndex = 0;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while ((bResult = bIterable.next())) {
if (bResult.done) {
break;
}
if (matchedIndices[matchIndex]) {
matchIndex++;
continue;
}
const aEntry = aResult.value;
const bEntry = bResult.value;
if (state.equals(aEntry[0], bEntry[0], index, matchIndex, a, b, state)
&& state.equals(aEntry[1], bEntry[1], aEntry[0], bEntry[0], a, b, state)) {
hasMatch = matchedIndices[matchIndex] = true;
break;
}
matchIndex++;
}
if (!hasMatch) {
return false;
}
index++;
}
return true;
}
/**
* Whether the objects are equal in value.
*/
function areObjectsEqual(a, b, state) {
const properties = keys(a);
let index = properties.length;
if (keys(b).length !== index) {
return false;
}
// Decrementing `while` showed faster results than either incrementing or
// decrementing `for` loop and than an incrementing `while` loop. Declarative
// methods like `some` / `every` were not used to avoid incurring the garbage
// cost of anonymous callbacks.
while (index-- > 0) {
if (!isPropertyEqual(a, b, state, properties[index])) {
return false;
}
}
return true;
}
/**
* Whether the objects are equal in value with strict property checking.
*/
function areObjectsEqualStrict(a, b, state) {
const properties = getStrictProperties(a);
let index = properties.length;
if (getStrictProperties(b).length !== index) {
return false;
}
let property;
let descriptorA;
let descriptorB;
// Decrementing `while` showed faster results than either incrementing or
// decrementing `for` loop and than an incrementing `while` loop. Declarative
// methods like `some` / `every` were not used to avoid incurring the garbage
// cost of anonymous callbacks.
while (index-- > 0) {
property = properties[index];
if (!isPropertyEqual(a, b, state, property)) {
return false;
}
descriptorA = getOwnPropertyDescriptor(a, property);
descriptorB = getOwnPropertyDescriptor(b, property);
if ((descriptorA || descriptorB)
&& (!descriptorA
|| !descriptorB
|| descriptorA.configurable !== descriptorB.configurable
|| descriptorA.enumerable !== descriptorB.enumerable
|| descriptorA.writable !== descriptorB.writable)) {
return false;
}
}
return true;
}
/**
* Whether the primitive wrappers passed are equal in value.
*/
function arePrimitiveWrappersEqual(a, b) {
return sameValueEqual(a.valueOf(), b.valueOf());
}
/**
* Whether the regexps passed are equal in value.
*/
function areRegExpsEqual(a, b) {
return a.source === b.source && a.flags === b.flags;
}
/**
* Whether the `Set`s are equal in value.
*/
function areSetsEqual(a, b, state) {
const size = a.size;
if (size !== b.size) {
return false;
}
if (!size) {
return true;
}
const matchedIndices = new Array(size);
const aIterable = a.values();
let aResult;
let bResult;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while ((aResult = aIterable.next())) {
if (aResult.done) {
break;
}
const bIterable = b.values();
let hasMatch = false;
let matchIndex = 0;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while ((bResult = bIterable.next())) {
if (bResult.done) {
break;
}
if (!matchedIndices[matchIndex]
&& state.equals(aResult.value, bResult.value, aResult.value, bResult.value, a, b, state)) {
hasMatch = matchedIndices[matchIndex] = true;
break;
}
matchIndex++;
}
if (!hasMatch) {
return false;
}
}
return true;
}
/**
* Whether the TypedArray instances are equal in value.
*/
function areTypedArraysEqual(a, b) {
let index = a.byteLength;
if (b.byteLength !== index || a.byteOffset !== b.byteOffset) {
return false;
}
while (index-- > 0) {
if (a[index] !== b[index]) {
return false;
}
}
return true;
}
/**
* Whether the URL instances are equal in value.
*/
function areUrlsEqual(a, b) {
return (a.hostname === b.hostname
&& a.pathname === b.pathname
&& a.protocol === b.protocol
&& a.port === b.port
&& a.hash === b.hash
&& a.username === b.username
&& a.password === b.password);
}
function isPropertyEqual(a, b, state, property) {
if ((property === REACT_OWNER || property === PREACT_OWNER || property === PREACT_VNODE)
&& (a.$$typeof || b.$$typeof)) {
return true;
}
return hasOwn(b, property) && state.equals(a[property], b[property], property, property, a, b, state);
}
// eslint-disable-next-line @typescript-eslint/unbound-method
const toString = Object.prototype.toString;
/**
* Create a comparator method based on the type-specific equality comparators passed.
*/
function createEqualityComparator(config) {
const supportedComparatorMap = createSupportedComparatorMap(config);
const { areArraysEqual, areDatesEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, areRegExpsEqual, areSetsEqual, getUnsupportedCustomComparator, } = config;
/**
* compare the value of the two objects and return true if they are equivalent in values
*/
return function comparator(a, b, state) {
// If the items are strictly equal, no need to do a value comparison.
if (a === b) {
return true;
}
// If either of the items are nullish and fail the strictly equal check
// above, then they must be unequal.
if (a == null || b == null) {
return false;
}
const type = typeof a;
if (type !== typeof b) {
return false;
}
if (type !== 'object') {
if (type === 'number' || type === 'bigint') {
return areNumbersEqual(a, b, state);
}
if (type === 'function') {
return areFunctionsEqual(a, b, state);
}
// If a primitive value that is not strictly equal, it must be unequal.
return false;
}
const constructor = a.constructor;
// Checks are listed in order of commonality of use-case:
// 1. Common complex object types (plain object, array)
// 2. Common data values (date, regexp)
// 3. Less-common complex object types (map, set)
// 4. Less-common data values (promise, primitive wrappers)
// Inherently this is both subjective and assumptive, however
// when reviewing comparable libraries in the wild this order
// appears to be generally consistent.
// Constructors should match, otherwise there is potential for false positives
// between class and subclass or custom object and POJO.
if (constructor !== b.constructor) {
return false;
}
// Try to fast-path equality checks for other complex object types in the
// same realm to avoid capturing the string tag. Strict equality is used
// instead of `instanceof` because it is more performant for the common
// use-case. If someone is creating a subclass from a native class, it will be
// handled with the string tag comparison.
if (constructor === Object) {
return areObjectsEqual(a, b, state);
}
if (constructor === Array) {
return areArraysEqual(a, b, state);
}
if (constructor === Date) {
return areDatesEqual(a, b, state);
}
if (constructor === RegExp) {
return areRegExpsEqual(a, b, state);
}
if (constructor === Map) {
return areMapsEqual(a, b, state);
}
if (constructor === Set) {
return areSetsEqual(a, b, state);
}
if (constructor === Promise) {
// Avoid tag checks for promise values, since we know if they are not referentially equal
// then they are not equal.
return false;
}
// `isArray()` works on subclasses and is cross-realm, so we can avoid capturing
// the string tag or doing an `instanceof` in edge cases.
if (Array.isArray(a)) {
return areArraysEqual(a, b, state);
}
// Since this is a custom object, capture the string tag to determining its type.
// This is reasonably performant in modern environments like v8 and SpiderMonkey.
const tag = toString.call(a);
const supportedComparator = supportedComparatorMap[tag];
if (supportedComparator) {
return supportedComparator(a, b, state);
}
const unsupportedCustomComparator = getUnsupportedCustomComparator && getUnsupportedCustomComparator(a, b, state, tag);
if (unsupportedCustomComparator) {
return unsupportedCustomComparator(a, b, state);
}
// If not matching any tags that require a specific type of comparison, then we hard-code false because
// the only thing remaining is strict equality, which has already been compared. This is for a few reasons:
// - Certain types that cannot be introspected (e.g., `WeakMap`). For these types, this is the only
// comparison that can be made.
// - For types that can be introspected but do not have an objective definition of what
// equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare.
// In all cases, these decisions should be reevaluated based on changes to the language and
// common development practices.
return false;
};
}
/**
* Create the configuration object used for building comparators.
*/
function createEqualityComparatorConfig({ circular, createCustomConfig, strict, }) {
let config = {
areArrayBuffersEqual,
areArraysEqual: strict ? areObjectsEqualStrict : areArraysEqual,
areDataViewsEqual,
areDatesEqual: areDatesEqual,
areErrorsEqual: areErrorsEqual,
areFunctionsEqual: strictEqual,
areMapsEqual: strict ? combineComparators(areMapsEqual, areObjectsEqualStrict) : areMapsEqual,
areNumbersEqual: sameValueEqual,
areObjectsEqual: strict ? areObjectsEqualStrict : areObjectsEqual,
arePrimitiveWrappersEqual: arePrimitiveWrappersEqual,
areRegExpsEqual: areRegExpsEqual,
areSetsEqual: strict ? combineComparators(areSetsEqual, areObjectsEqualStrict) : areSetsEqual,
areTypedArraysEqual: strict
? combineComparators(areTypedArraysEqual, areObjectsEqualStrict)
: areTypedArraysEqual,
areUrlsEqual: areUrlsEqual,
getUnsupportedCustomComparator: undefined,
};
if (createCustomConfig) {
config = Object.assign({}, config, createCustomConfig(config));
}
if (circular) {
const areArraysEqual = createIsCircular(config.areArraysEqual);
const areMapsEqual = createIsCircular(config.areMapsEqual);
const areObjectsEqual = createIsCircular(config.areObjectsEqual);
const areSetsEqual = createIsCircular(config.areSetsEqual);
config = Object.assign({}, config, {
areArraysEqual,
areMapsEqual,
areObjectsEqual,
areSetsEqual,
});
}
return config;
}
/**
* Default equality comparator pass-through, used as the standard `isEqual` creator for
* use inside the built comparator.
*/
function createInternalEqualityComparator(compare) {
return function (a, b, _indexOrKeyA, _indexOrKeyB, _parentA, _parentB, state) {
return compare(a, b, state);
};
}
/**
* Create the `isEqual` function used by the consuming application.
*/
function createIsEqual({ circular, comparator, createState, equals, strict }) {
if (createState) {
return function isEqual(a, b) {
const { cache = circular ? new WeakMap() : undefined, meta } = createState();
return comparator(a, b, {
cache,
equals,
meta,
strict,
});
};
}
if (circular) {
return function isEqual(a, b) {
return comparator(a, b, {
cache: new WeakMap(),
equals,
meta: undefined,
strict,
});
};
}
const state = {
cache: undefined,
equals,
meta: undefined,
strict,
};
return function isEqual(a, b) {
return comparator(a, b, state);
};
}
/**
* Create a map of `toString()` values to their respective handlers for `tag`-based lookups.
*/
function createSupportedComparatorMap({ areArrayBuffersEqual, areArraysEqual, areDataViewsEqual, areDatesEqual, areErrorsEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, areUrlsEqual, }) {
return {
'[object Arguments]': areObjectsEqual,
'[object Array]': areArraysEqual,
'[object ArrayBuffer]': areArrayBuffersEqual,
'[object AsyncGeneratorFunction]': areFunctionsEqual,
'[object BigInt]': areNumbersEqual,
'[object BigInt64Array]': areTypedArraysEqual,
'[object BigUint64Array]': areTypedArraysEqual,
'[object Boolean]': arePrimitiveWrappersEqual,
'[object DataView]': areDataViewsEqual,
'[object Date]': areDatesEqual,
// If an error tag, it should be tested explicitly. Like RegExp, the properties are not
// enumerable, and therefore will give false positives if tested like a standard object.
'[object Error]': areErrorsEqual,
'[object Float16Array]': areTypedArraysEqual,
'[object Float32Array]': areTypedArraysEqual,
'[object Float64Array]': areTypedArraysEqual,
'[object Function]': areFunctionsEqual,
'[object GeneratorFunction]': areFunctionsEqual,
'[object Int8Array]': areTypedArraysEqual,
'[object Int16Array]': areTypedArraysEqual,
'[object Int32Array]': areTypedArraysEqual,
'[object Map]': areMapsEqual,
'[object Number]': arePrimitiveWrappersEqual,
'[object Object]': (a, b, state) =>
// The exception for value comparison is custom `Promise`-like class instances. These should
// be treated the same as standard `Promise` objects, which means strict equality, and if
// it reaches this point then that strict equality comparison has already failed.
typeof a.then !== 'function' && typeof b.then !== 'function' && areObjectsEqual(a, b, state),
// For RegExp, the properties are not enumerable, and therefore will give false positives if
// tested like a standard object.
'[object RegExp]': areRegExpsEqual,
'[object Set]': areSetsEqual,
'[object String]': arePrimitiveWrappersEqual,
'[object URL]': areUrlsEqual,
'[object Uint8Array]': areTypedArraysEqual,
'[object Uint8ClampedArray]': areTypedArraysEqual,
'[object Uint16Array]': areTypedArraysEqual,
'[object Uint32Array]': areTypedArraysEqual,
};
}
/**
* Whether the items passed are deeply-equal in value.
*/
const deepEqual = createCustomEqual();
/**
* Whether the items passed are deeply-equal in value based on strict comparison.
*/
createCustomEqual({ strict: true });
/**
* Whether the items passed are deeply-equal in value, including circular references.
*/
createCustomEqual({ circular: true });
/**
* Whether the items passed are deeply-equal in value, including circular references,
* based on strict comparison.
*/
createCustomEqual({
circular: true,
strict: true,
});
/**
* Whether the items passed are shallowly-equal in value.
*/
createCustomEqual({
createInternalComparator: () => sameValueEqual,
});
/**
* Whether the items passed are shallowly-equal in value based on strict comparison
*/
createCustomEqual({
strict: true,
createInternalComparator: () => sameValueEqual,
});
/**
* Whether the items passed are shallowly-equal in value, including circular references.
*/
createCustomEqual({
circular: true,
createInternalComparator: () => sameValueEqual,
});
/**
* Whether the items passed are shallowly-equal in value, including circular references,
* based on strict comparison.
*/
createCustomEqual({
circular: true,
createInternalComparator: () => sameValueEqual,
strict: true,
});
/**
* Create a custom equality comparison method.
*
* This can be done to create very targeted comparisons in extreme hot-path scenarios
* where the standard methods are not performant enough, but can also be used to provide
* support for legacy environments that do not support expected features like
* `RegExp.prototype.flags` out of the box.
*/
function createCustomEqual(options = {}) {
const { circular = false, createInternalComparator: createCustomInternalComparator, createState, strict = false, } = options;
const config = createEqualityComparatorConfig(options);
const comparator = createEqualityComparator(config);
const equals = createCustomInternalComparator
? createCustomInternalComparator(comparator)
: createInternalEqualityComparator(comparator);
return createIsEqual({ circular, comparator, createState, equals, strict });
}
function useDeepCompareEffect(effect, dependencies) {
useCustomCompareEffect(effect, dependencies, deepEqual);
}
const mapOptionKeys = new Set([
'backgroundColor',
'clickableIcons',
'controlSize',
'disableDefaultUI',
'disableDoubleClickZoom',
'draggable',
'draggableCursor',
'draggingCursor',
'fullscreenControl',
'fullscreenControlOptions',
'gestureHandling',
'headingInteractionEnabled',
'isFractionalZoomEnabled',
'keyboardShortcuts',
'mapTypeControl',
'mapTypeControlOptions',
'mapTypeId',
'maxZoom',
'minZoom',
'noClear',
'panControl',
'panControlOptions',
'restriction',
'rotateControl',
'rotateControlOptions',
'scaleControl',
'scaleControlOptions',
'scrollwheel',
'streetView',
'streetViewControl',
'streetViewControlOptions',
'styles',
'tiltInteractionEnabled',
'zoomControl',
'zoomControlOptions'
]);
/**
* Internal hook to update the map-options when props are changed.
*
* @param map the map instance
* @param mapProps the props to update the map-instance with
* @internal
*/
function useMapOptions(map, mapProps) {
/* eslint-disable react-hooks/exhaustive-deps --
*
* The following effects aren't triggered when the map is changed.
* In that case, the values will be or have been passed to the map