├── .nvmrc ├── docs ├── .nvmrc ├── public │ ├── .nojekyll │ ├── favicon.ico │ ├── marker-pin.png │ ├── marker-pin-draggable.png │ ├── manifest.json │ ├── vercel.svg │ └── next.svg ├── eslint.config.js ├── .env.prod ├── src │ ├── components │ │ ├── coordinates.ts │ │ ├── marker.tsx │ │ ├── info.tsx │ │ └── map-options.json │ └── app │ │ ├── layout.tsx │ │ ├── globals.css │ │ ├── page.module.css │ │ └── page.tsx ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── next.config.js ├── .commitlintrc.json ├── .husky ├── pre-commit ├── commit-msg └── common.sh ├── src ├── index.ts ├── utils │ ├── utils.ts │ └── types.ts ├── map │ ├── hooks │ │ ├── useGoogleMaps.ts │ │ ├── useMemoCompare.ts │ │ └── useScript.ts │ ├── markers.tsx │ ├── overlay-view.tsx │ ├── map.tsx │ └── overlay.tsx └── google-map.tsx ├── .prettierignore ├── CONTRIBUTING.md ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 2.feature_request.yml │ └── 1.bug_report.yml ├── dependabot.yml └── workflows │ ├── STALE_ISSUE.yml │ └── deploy.yml ├── .babelrc.json ├── .prettierrc.json ├── tsup.config.ts ├── .gitignore ├── tsconfig.json ├── LICENCE.md ├── eslint.config.js ├── package.json ├── dist ├── index.js ├── index.cjs ├── index.d.cts └── index.d.ts └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /docs/.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /docs/public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/common.sh" 3 | 4 | yarn lint-staged -------------------------------------------------------------------------------- /docs/eslint.config.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | extends: 'next/core-web-vitals', 4 | }, 5 | ] 6 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giorgiabosello/google-maps-react-markers/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/marker-pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giorgiabosello/google-maps-react-markers/HEAD/docs/public/marker-pin.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import GoogleMap from './google-map' 2 | 3 | export * from './utils/types' 4 | 5 | export default GoogleMap 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /docs/.env.prod: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_MEDIA_URL=/google-maps-react-markers 2 | NEXT_PUBLIC_GOOGLE_MAPS_API_KEY='AIzaSyDDu4eepAbq9gEMb7Dx3LBXFw9rwAQE_4M' -------------------------------------------------------------------------------- /docs/public/marker-pin-draggable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giorgiabosello/google-maps-react-markers/HEAD/docs/public/marker-pin-draggable.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | .prettierignore 4 | yarn.lock 5 | *.snap 6 | .npmrc 7 | .husky 8 | .gitignore 9 | .editorconfig 10 | .env.template -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **Working on your first Pull Request?** You can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub](https://kcd.im/pull-request) -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | command_exists () { 2 | command -v "$1" >/dev/null 2>&1 3 | } 4 | 5 | # Workaround for Windows 10, Git Bash and Yarn 6 | if command_exists winpty && test -t 1; then 7 | exec < /dev/tty 8 | fi -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/giorgiabosello/google-maps-react-markers/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [] 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const isArraysEqualEps = (arrayA: number[], arrayB: number[], eps = 0.000001): boolean => { 2 | if (arrayA.length && arrayB.length) { 3 | for (let i = 0; i !== arrayA.length; ++i) { 4 | if (Math.abs(arrayA[i] - arrayB[i]) > eps) { 5 | return false 6 | } 7 | } 8 | return true 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "semi": false, 5 | "useTabs": true, 6 | "overrides": [ 7 | { 8 | "files": "*.json", 9 | "options": { 10 | "printWidth": 200 11 | } 12 | }, 13 | { 14 | "files": "*.md", 15 | "options": { 16 | "useTabs": false, 17 | "semi": false 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "google-maps-react-markers", 3 | "name": "google-maps-react-markers", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /docs/src/components/coordinates.ts: -------------------------------------------------------------------------------- 1 | const coordinates = [ 2 | { 3 | lat: 45.4046987, 4 | lng: 12.2472504, 5 | name: 'Store draggable', 6 | }, 7 | ...[...Array(300)].map((_, i) => ({ 8 | lat: parseFloat((Math.random() * 180 - 90).toFixed(6)), 9 | lng: parseFloat((Math.random() * 360 - 180).toFixed(6)), 10 | name: `Store #${i + 1}`, 11 | })), 12 | ] 13 | 14 | export default coordinates 15 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup' 2 | 3 | const config: Options = { 4 | entry: ['src/index.ts'], 5 | treeshake: true, 6 | sourcemap: false, // process.env.NODE_ENV === 'development', 7 | minify: true, 8 | clean: true, 9 | dts: true, 10 | splitting: false, 11 | format: ['cjs', 'esm'], 12 | external: ['react'], 13 | injectStyle: false, 14 | } 15 | 16 | export default config 17 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # JetBrains IDE files 9 | .idea/ 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | # /dist 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | tsconfig.tsbuildinfo 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | #docs 28 | /docs/node_modules 29 | /docs/build 30 | 31 | #docs nextjs 32 | /docs/nextjs/node_modules 33 | /docs/nextjs/.next 34 | /docs/nextjs/next-env.d.ts -------------------------------------------------------------------------------- /docs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "jsx": "react-jsx", 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "noEmit": true, 11 | // Types should go into this directory. 12 | // Removing this would place the .d.ts files 13 | // next to the .js files 14 | "outDir": "dist", 15 | // Generate d.ts files 16 | "declaration": true, 17 | // go to js file when using IDE functions like 18 | // "Go to Definition" in VSCode 19 | "declarationMap": true 20 | }, 21 | "exclude": ["node_modules", "build"] 22 | } 23 | -------------------------------------------------------------------------------- /docs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import React from 'react' 4 | import './globals.css' 5 | 6 | const inter = Inter({ subsets: ['latin'] }) 7 | 8 | export const metadata: Metadata = { 9 | title: 'Google Maps React Markers', 10 | description: 11 | 'Google Maps library that accepts markers as react components, works with React 18+ and it is fully typed.', 12 | } 13 | 14 | export default function RootLayout({ children }: { children: React.ReactNode }) { 15 | return ( 16 | 17 | {children} 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/STALE_ISSUE.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 14 17 | stale-issue-label: 'stale' 18 | exempt-issue-labels: 'feature request' 19 | exempt-pr-labels: 'feature request' 20 | stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity.' 21 | close-issue-message: 'This issue was closed because it has been inactive for 14 days since being marked as stale.' 22 | days-before-pr-stale: -1 23 | days-before-pr-close: -1 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-maps-react-markers-nextjs-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev -H google-maps-react-markers.com.tech", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "engines": { 13 | "node": ">=18.17.0" 14 | }, 15 | "dependencies": { 16 | "google-maps-react-markers": "link:..", 17 | "next": "15.4.7", 18 | "react": "link:../node_modules/react", 19 | "react-dom": "link:../node_modules/react-dom" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "link:../node_modules/@types/node", 23 | "@types/react": "link:../node_modules/@types/react", 24 | "@types/react-dom": "link:../node_modules/@types/react-dom", 25 | "eslint": "link:../node_modules/eslint", 26 | "eslint-config-next": "15.1.6", 27 | "typescript": "link:../node_modules/typescript" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | -------------------------------------------------------------------------------- /docs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'img.shields.io', 8 | // port: '', 9 | // pathname: '', 10 | }, 11 | ], 12 | /** 13 | * Disable server-based image optimization. Next.js does not support 14 | * dynamic features with static exports. 15 | * 16 | * @see https://nextjs.org/docs/pages/api-reference/components/image#unoptimized 17 | */ 18 | unoptimized: true, 19 | }, 20 | /** 21 | * Enable static exports for the App Router. 22 | * 23 | * @see https://nextjs.org/docs/pages/building-your-application/deploying/static-exports 24 | */ 25 | output: process.env.NODE_ENV === 'development' ? 'standalone' : 'export', 26 | /** 27 | * Set base path. This is usually the slug of your repository. 28 | * 29 | * @see https://nextjs.org/docs/app/api-reference/next-config-js/basePath 30 | */ 31 | basePath: process.env.NODE_ENV === 'development' ? '' : '/google-maps-react-markers', 32 | } 33 | 34 | module.exports = nextConfig 35 | -------------------------------------------------------------------------------- /src/map/hooks/useGoogleMaps.ts: -------------------------------------------------------------------------------- 1 | import { IUseGoogleMaps, UseScriptStatus } from '../../utils/types' 2 | import useScript from './useScript' 3 | 4 | /** 5 | * @returns {"idle" | "loading" | "ready" | "error"} status 6 | */ 7 | export const useGoogleMaps = ({ 8 | apiKey, 9 | libraries = [], 10 | loadScriptExternally = false, 11 | status = 'idle', 12 | externalApiParams, 13 | callback, 14 | }: IUseGoogleMaps): UseScriptStatus => { 15 | if (typeof window !== 'undefined') window.googleMapsCallback = callback 16 | const apiParams = new URLSearchParams(externalApiParams)?.toString() 17 | const script = apiKey 18 | ? { 19 | src: `https://maps.googleapis.com/maps/api/js?key=${apiKey}&callback=googleMapsCallback&libraries=${libraries?.join( 20 | ',', 21 | )}${apiParams ? `&${apiParams}` : ''}`, 22 | attributes: { id: 'googleMapsApi' }, 23 | } 24 | : { 25 | src: `https://maps.googleapis.com/maps/api/js?callback=googleMapsCallback&libraries=${libraries?.join(',')}`, 26 | attributes: { id: 'googleMapsApi' }, 27 | } 28 | 29 | return useScript(script, loadScriptExternally ? status : undefined) 30 | } 31 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Giorgia Bosello 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/map/hooks/useMemoCompare.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | /** 4 | * A hook that compares the previous and current values of a reference. 5 | * @param {any} next - the current value of the reference 6 | * @param {function} compare - a function that compares the previous and current values 7 | * @returns {any} the previous value of the reference 8 | * @ref https://usehooks.com/useMemoCompare/ 9 | */ 10 | const useMemoCompare = (next: any, compare: Function): any => { 11 | // Ref for storing previous value 12 | const previousRef = useRef() 13 | const previous = previousRef.current 14 | // Pass previous and next value to compare function 15 | // to determine whether to consider them equal. 16 | const isEqual = compare(previous, next) 17 | // If not equal update previousRef to next value. 18 | // We only update if not equal so that this hook continues to return 19 | // the same old value if compare keeps returning true. 20 | useEffect(() => { 21 | if (!isEqual) { 22 | previousRef.current = next 23 | } 24 | }) 25 | // Finally, if equal then return the previous value 26 | return isEqual ? previous : next 27 | } 28 | 29 | export default useMemoCompare 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2.feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Create a bug report for Google Maps React Markers 3 | labels: ['template: enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: Thanks for taking the time to file a feature request! Please fill out this form as completely as possible. 8 | - type: markdown 9 | attributes: 10 | value: 'Feature requests will be converted to the GitHub Discussions "Ideas" section.' 11 | - type: textarea 12 | attributes: 13 | label: Describe the feature you'd like to request 14 | description: A clear and concise description of what you want and what your use case is. 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Describe the solution you'd like 20 | description: A clear and concise description of what you want to happen. 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Describe alternatives you've considered 26 | description: A clear and concise description of any alternative solutions or features you've considered. 27 | validations: 28 | required: true 29 | -------------------------------------------------------------------------------- /docs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/google-map.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import { useGoogleMaps } from './map/hooks/useGoogleMaps' 3 | import MapComponent from './map/map' 4 | import { GoogleMapProps } from './utils/types' 5 | 6 | const GoogleMap = forwardRef( 7 | ( 8 | { 9 | apiKey = '', 10 | libraries = ['places', 'geometry'], 11 | children = null, 12 | loadingContent = 'Google Maps is loading', 13 | idleContent = 'Google Maps is on idle', 14 | errorContent = 'Google Maps is on error', 15 | mapMinHeight = 'unset', 16 | containerProps = {}, 17 | loadScriptExternally = false, 18 | status: statusProp = 'idle', 19 | scriptCallback = () => {}, 20 | externalApiParams = {}, 21 | ...props 22 | }, 23 | ref, 24 | ) => { 25 | const renderers = { 26 | ready: {children}, 27 | loading: loadingContent, 28 | idle: idleContent, 29 | error: errorContent, 30 | } 31 | 32 | const status = useGoogleMaps({ 33 | apiKey, 34 | libraries, 35 | loadScriptExternally, 36 | status: statusProp, 37 | externalApiParams, 38 | callback: scriptCallback, 39 | }) 40 | 41 | return ( 42 |
47 | {renderers[status] || null} 48 |
49 | ) 50 | }, 51 | ) 52 | 53 | export default GoogleMap 54 | -------------------------------------------------------------------------------- /src/map/markers.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, isValidElement, useMemo } from 'react' 2 | import { MapMarkersProps } from '../utils/types' 3 | import OverlayView from './overlay-view' 4 | 5 | const noop = () => {} 6 | 7 | const MapMarkers = ({ children, map, maps }: MapMarkersProps) => { 8 | const markers = useMemo(() => { 9 | if (!map || !maps) return [] 10 | 11 | return Children.map(children, (child) => { 12 | if (isValidElement(child)) { 13 | const latLng = { lat: child.props.lat, lng: child.props.lng } as google.maps.LatLngLiteral 14 | const { zIndex, draggable = false, onDragStart = noop, onDrag = noop, onDragEnd = noop } = child.props || {} 15 | 16 | // clone child without draggable props 17 | // eslint-disable-next-line no-param-reassign 18 | child = cloneElement(child, { 19 | ...child.props, 20 | // draggable: undefined, 21 | onDragStart: undefined, 22 | onDrag: undefined, 23 | onDragEnd: undefined, 24 | }) 25 | 26 | return ( 27 | 39 | {child} 40 | 41 | ) 42 | } 43 | return null 44 | }) 45 | }, [children, map, maps]) 46 | 47 | return
{markers}
48 | } 49 | 50 | export default MapMarkers 51 | -------------------------------------------------------------------------------- /docs/src/components/marker.tsx: -------------------------------------------------------------------------------- 1 | import { LatLngLiteral } from 'google-maps-react-markers' 2 | import React from 'react' 3 | 4 | interface MarkerProps { 5 | className?: string 6 | draggable: boolean 7 | lat: number 8 | lng: number 9 | markerId: string 10 | onClick?: ( 11 | e: React.MouseEvent, 12 | props: { lat: number; lng: number; markerId: string }, 13 | ) => void 14 | onDrag?: (e: React.MouseEvent, props: { latLng: LatLngLiteral }) => void 15 | onDragEnd?: (e: React.MouseEvent, props: { latLng: LatLngLiteral }) => void 16 | onDragStart?: (e: React.MouseEvent, props: { latLng: LatLngLiteral }) => void 17 | } 18 | 19 | const Marker = ({ 20 | className, 21 | lat, 22 | lng, 23 | markerId, 24 | onClick, 25 | draggable, 26 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 27 | onDrag, 28 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 29 | onDragEnd, 30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 31 | onDragStart, 32 | ...props 33 | }: MarkerProps) => 34 | lat && lng ? ( 35 | // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 36 | (onClick ? onClick(e, { markerId, lat, lng }) : null)} 44 | style={{ fontSize: 40 }} 45 | alt={markerId} 46 | width={35} 47 | height={35} 48 | {...props} 49 | /> 50 | ) : null 51 | 52 | export default Marker 53 | -------------------------------------------------------------------------------- /src/map/overlay-view.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from 'react' 2 | import { createPortal } from 'react-dom' 3 | import { OverlayViewProps } from '../utils/types' 4 | import useMemoCompare from './hooks/useMemoCompare' 5 | import createOverlay from './overlay' 6 | 7 | const OverlayView = ({ 8 | pane = 'floatPane', 9 | position, 10 | map, 11 | maps, 12 | zIndex = 0, 13 | children, 14 | drag, 15 | }: OverlayViewProps): React.JSX.Element => { 16 | const container = useMemo(() => { 17 | const div = document.createElement('div') as HTMLDivElement 18 | div.style.position = 'absolute' 19 | return div as HTMLDivElement 20 | }, []) 21 | 22 | const overlay = useMemo( 23 | () => createOverlay({ container, pane, position, maps, drag }), 24 | [container, drag, maps, pane, position], 25 | ) 26 | 27 | // Because React does not do deep comparisons, a custom hook is used. 28 | // This fixes the issue where the overlay is not updated when the position changes. 29 | const childrenProps = useMemoCompare( 30 | children?.props as any, 31 | (prev: { lat: any; lng: any; draggable: boolean }, next: { lat: any; lng: any; draggable: boolean }) => 32 | prev && prev.lat === next.lat && prev.lng === next.lng && prev.draggable === next.draggable, 33 | ) 34 | 35 | useEffect(() => { 36 | if (!overlay.map && map) { 37 | overlay?.setMap(map) 38 | return () => { 39 | overlay?.setMap(null) 40 | } 41 | } 42 | return () => {} 43 | // overlay depends on map, so we don't need to add it to the dependency array 44 | // otherwise, it will re-render the overlay every time the map changes 45 | // ? added childrenProps to the dependency array to re-render the overlay when the children props change. 46 | }, [map, childrenProps]) 47 | 48 | // to move the container to the foreground and background 49 | useEffect(() => { 50 | container.style.zIndex = `${zIndex}` 51 | }, [zIndex, container]) 52 | 53 | return createPortal(children, container) 54 | } 55 | 56 | export default OverlayView 57 | -------------------------------------------------------------------------------- /docs/src/components/info.tsx: -------------------------------------------------------------------------------- 1 | import styles from '../app/page.module.css' 2 | 3 | interface InfoProps { 4 | bounds?: number[] 5 | coordinates?: { lat: number; lng: number; name: string }[] 6 | drag?: { 7 | dragEnd: { lat: number; lng: number } | null 8 | dragStart: { lat: number; lng: number } | null 9 | dragging: { lat: number; lng: number } | null 10 | } 11 | } 12 | 13 | const Info = ({ coordinates = [], bounds, drag }: InfoProps) => ( 14 |
15 |
16 |
17 |

📍 Current markers

18 | {coordinates?.map(({ lat, lng, name }, index) => ( 19 | // eslint-disable-next-line react/no-array-index-key 20 |
21 |

{`${name} 👉 { lat: ${lat},lng: ${lng} }`}

22 |
23 | ))} 24 |
25 |
26 |

🖐🏼 Drag

27 |

28 | Drag the yellow marker to see its coordinates 29 | change. 30 |

31 | {drag?.dragStart && drag?.dragEnd && ( 32 | <> 33 |

Drag start:

34 |

{drag.dragStart ? `lat: ${drag.dragStart.lat}, lng: ${drag.dragStart.lng}` : 'null'}

35 |

Dragging:

36 |

{drag.dragging ? `lat: ${drag.dragging.lat}, lng: ${drag.dragging.lng}` : 'null'}

37 |

Drag end:

38 |

{drag.dragEnd ? `lat: ${drag.dragEnd.lat}, lng: ${drag.dragEnd.lng}` : 'null'}

39 | 40 | )} 41 |
42 |
43 | {bounds && ( 44 |
45 |

🗺 Map bounds

46 |

Map bounds are used to calculate clusters.

47 |

Move the map to see the bounds change.

48 |
49 | {bounds?.map((bound: number, index: number) => { 50 | const name = ['SW lng', 'SW lat', 'NE lng', 'NE lat'][index] 51 | // eslint-disable-next-line react/no-array-index-key 52 | return

{`${name}: ${bound}`}

53 | })} 54 |
55 |
56 | )} 57 |
58 | ) 59 | 60 | export default Info 61 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | root: true, 4 | env: { 5 | browser: true, 6 | es2021: true, 7 | es6: true, 8 | node: true, 9 | }, 10 | extends: ['plugin:react/recommended', 'airbnb', 'prettier'], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | }, 19 | plugins: [ 20 | 'react', 21 | '@typescript-eslint', 22 | 'typescript-sort-keys', 23 | 'unused-imports', 24 | 'prettier', 25 | 'prefer-arrow', 26 | 'sort-class-members', 27 | ], 28 | rules: { 29 | '@typescript-eslint/no-unused-vars': 'error', 30 | 'import/extensions': 'off', 31 | 'import/no-extraneous-dependencies': 'off', 32 | 'import/no-unresolved': 'off', 33 | 'import/prefer-default-export': 'off', 34 | 'no-nested-ternary': 'off', 35 | 'no-plusplus': 'off', 36 | 'no-unused-vars': 'off', 37 | 'no-use-before-define': 'off', 38 | 'prettier/prettier': [ 39 | 'error', 40 | { 41 | endOfLine: 'auto', 42 | }, 43 | ], 44 | 'react/destructuring-assignment': 'off', 45 | 'react/function-component-definition': 'off', 46 | 'react/jsx-filename-extension': 'off', 47 | 'react/jsx-props-no-spreading': 'off', 48 | 'react/react-in-jsx-scope': 'off', 49 | 'react/require-default-props': 'off', 50 | 'typescript-sort-keys/interface': 'error', 51 | 'typescript-sort-keys/string-enum': 'error', 52 | 'unused-imports/no-unused-imports': 'error', 53 | 'prefer-arrow/prefer-arrow-functions': [ 54 | 'error', 55 | { 56 | disallowPrototype: true, 57 | singleReturnOnly: true, 58 | classPropertiesAllowed: false, 59 | }, 60 | ], 61 | 'sort-class-members/sort-class-members': [ 62 | 'error', 63 | { 64 | order: [ 65 | '[static-properties]', 66 | '[properties]', 67 | '[conventional-private-properties]', 68 | 'constructor', 69 | '[static-methods]', 70 | '[methods]', 71 | '[conventional-private-methods]', 72 | ], 73 | accessorPairPositioning: 'getThenSet', 74 | }, 75 | ], 76 | }, 77 | globals: { 78 | globalThis: 'readonly', 79 | google: 'readonly', 80 | }, 81 | }, 82 | ] 83 | -------------------------------------------------------------------------------- /docs/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 5 | 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 6 | 7 | --foreground-rgb: 0, 0, 0; 8 | --background-start-rgb: 214, 219, 220; 9 | --background-end-rgb: 255, 255, 255; 10 | 11 | --primary-glow: conic-gradient( 12 | from 180deg at 50% 50%, 13 | #16abff33 0deg, 14 | #0885ff33 55deg, 15 | #54d6ff33 120deg, 16 | #0071ff33 160deg, 17 | transparent 360deg 18 | ); 19 | --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); 20 | 21 | --tile-start-rgb: 239, 245, 249; 22 | --tile-end-rgb: 228, 232, 233; 23 | --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); 24 | 25 | --callout-rgb: 238, 240, 241; 26 | --callout-border-rgb: 172, 175, 176; 27 | --card-rgb: 180, 185, 188; 28 | --card-border-rgb: 131, 134, 135; 29 | --highlight-rgb: 255, 255, 107; 30 | } 31 | 32 | @media (prefers-color-scheme: dark) { 33 | :root { 34 | --foreground-rgb: 255, 255, 255; 35 | --background-start-rgb: 0, 0, 0; 36 | --background-end-rgb: 0, 0, 0; 37 | 38 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 39 | --secondary-glow: linear-gradient(to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3)); 40 | 41 | --tile-start-rgb: 2, 13, 46; 42 | --tile-end-rgb: 2, 5, 19; 43 | --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80); 44 | 45 | --callout-rgb: 20, 20, 20; 46 | --callout-border-rgb: 108, 108, 108; 47 | --card-rgb: 100, 100, 100; 48 | --card-border-rgb: 200, 200, 200; 49 | } 50 | } 51 | 52 | * { 53 | box-sizing: border-box; 54 | padding: 0; 55 | margin: 0; 56 | } 57 | 58 | html, 59 | body { 60 | max-width: 100vw; 61 | overflow-x: hidden; 62 | } 63 | 64 | body { 65 | color: rgb(var(--foreground-rgb)); 66 | background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); 67 | } 68 | 69 | a { 70 | color: inherit; 71 | text-decoration: none; 72 | } 73 | 74 | @media (prefers-color-scheme: dark) { 75 | html { 76 | color-scheme: dark; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report for Google Maps React Markers 3 | labels: ['template: bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions ["Help" section](https://github.com/giorgiabosello/google-maps-react-markers/discussions/categories/help). 8 | - type: checkboxes 9 | attributes: 10 | label: Verify latest release 11 | description: 'Verify that your issue reproduces in the [latest release](https://github.com/giorgiabosello/google-maps-react-markers/releases) before opening a new issue.' 12 | options: 13 | - label: I verified that the issue exists in the latest release 14 | required: true 15 | - type: dropdown 16 | attributes: 17 | label: I know how to solve the bug 18 | description: If you don't know how to solve the bug, the GitHub repository or the CodeSandbox example is required. 19 | multiple: false 20 | options: 21 | - 'I know how to solve the bug' 22 | - "I don't know how to solve the bug and I will provide an example." 23 | - type: input 24 | attributes: 25 | label: Link to the code that reproduces this issue 26 | description: A link to a GitHub repository or a [CodeSandbox](https://codesandbox.io/) minimal reproduction. You can find a minimal reproduction example [here](https://github.com/giorgiabosello/google-maps-react-markers/tree/master/docs/src) and it should include only changes that contribute to the issue. 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: To Reproduce 32 | description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Describe the Bug 38 | description: A clear and concise description of what the bug is. 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Expected Behavior 44 | description: A clear and concise description of what you expected to happen. 45 | validations: 46 | required: true 47 | - type: markdown 48 | attributes: 49 | value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. 50 | - type: markdown 51 | attributes: 52 | value: Contributors should be able to follow the steps provided in order to reproduce the bug. 53 | - type: markdown 54 | attributes: 55 | value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance! 56 | - type: input 57 | attributes: 58 | label: Which browser are you using? (if relevant) 59 | description: 'Please specify the exact version. For example: Chrome 100.0.4878.0' 60 | -------------------------------------------------------------------------------- /src/map/map.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react' 2 | import { EventProps, Map, MapProps, MapsLibrary } from '../utils/types' 3 | import { isArraysEqualEps } from '../utils/utils' 4 | import MapMarkers from './markers' 5 | 6 | function MapComponent({ 7 | children = null, 8 | style = { 9 | width: '100%', 10 | height: '100%', 11 | left: 0, 12 | top: 0, 13 | margin: 0, 14 | padding: 0, 15 | position: 'absolute', 16 | }, 17 | defaultCenter, 18 | defaultZoom, 19 | onGoogleApiLoaded, 20 | onChange, 21 | options = {}, 22 | events = [], 23 | }: MapProps) { 24 | const mapRef = useRef(null) 25 | const prevBoundsRef = useRef([]) 26 | const [map, setMap] = useState() 27 | const [maps, setMaps] = useState() 28 | const [googleApiCalled, setGoogleApiCalled] = useState(false) 29 | 30 | const onIdle = useCallback(() => { 31 | try { 32 | if (!map) { 33 | return 34 | } 35 | 36 | const zoom = map.getZoom() ?? defaultZoom 37 | const bounds = map.getBounds() 38 | const centerLatLng = [map.getCenter()?.lng(), map.getCenter()?.lat()] 39 | 40 | const ne = bounds?.getNorthEast() 41 | const sw = bounds?.getSouthWest() 42 | 43 | if (!ne || !sw || !bounds) { 44 | return 45 | } 46 | 47 | // east, north, south, west 48 | const boundsArray = [sw.lng(), sw.lat(), ne.lng(), ne.lat()] 49 | 50 | if (!isArraysEqualEps(boundsArray, prevBoundsRef.current)) { 51 | if (onChange) { 52 | onChange({ 53 | zoom, 54 | center: centerLatLng, 55 | bounds, 56 | }) 57 | } 58 | prevBoundsRef.current = boundsArray 59 | } 60 | } catch (e) { 61 | // eslint-disable-next-line no-console 62 | console.error(e) 63 | } 64 | }, [map, onChange]) 65 | 66 | useEffect(() => { 67 | if (mapRef.current && !map) { 68 | setMap( 69 | new google.maps.Map(mapRef.current as HTMLElement, { 70 | center: defaultCenter, 71 | zoom: defaultZoom, 72 | ...(options as google.maps.MapOptions), 73 | }), 74 | ) 75 | setMaps(google.maps) 76 | } 77 | }, [defaultCenter, defaultZoom, map, mapRef, options]) 78 | 79 | useEffect(() => { 80 | if (map && !googleApiCalled) { 81 | if (typeof onGoogleApiLoaded === 'function' && maps) { 82 | onGoogleApiLoaded({ map, maps, ref: mapRef.current }) 83 | } 84 | setGoogleApiCalled(true) 85 | 86 | if (google.maps.event.hasListeners(map, 'idle')) google.maps.event.clearListeners(map, 'idle') 87 | // Idle event is fired when the map becomes idle after panning or zooming. 88 | google.maps.event.addListener(map, 'idle', onIdle) 89 | } 90 | }, [googleApiCalled, map, onGoogleApiLoaded]) 91 | 92 | useEffect( 93 | () => 94 | // clear listeners on unmount 95 | () => { 96 | if (map) { 97 | google.maps.event.clearListeners(map, 'idle') 98 | } 99 | }, 100 | [map], 101 | ) 102 | 103 | return ( 104 | <> 105 |
{ 112 | acc[name] = handler 113 | return acc 114 | }, 115 | {} as { [key: string]: any }, 116 | ), {}} 117 | /> 118 | {children && map && maps && ( 119 | 120 | {children} 121 | 122 | )} 123 | 124 | ) 125 | } 126 | 127 | export default MapComponent 128 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-maps-react-markers", 3 | "version": "2.0.15", 4 | "description": "Google Maps library that accepts markers as react components, works with React 18+ and it is fully typed.", 5 | "author": "Giorgia Bosello", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/giorgiabosello/google-maps-react-markers.git" 10 | }, 11 | "homepage": "https://giorgiabosello.github.io/google-maps-react-markers/", 12 | "keywords": [ 13 | "react", 14 | "google-maps", 15 | "markers", 16 | "map", 17 | "google", 18 | "maps", 19 | "google-maps-react", 20 | "google-maps-react-markers", 21 | "react-component" 22 | ], 23 | "main": "dist/index.cjs", 24 | "module": "dist/index.js", 25 | "exports": { 26 | ".": { 27 | "import": "./dist/index.js", 28 | "require": "./dist/index.cjs" 29 | } 30 | }, 31 | "types": "dist/index.d.ts", 32 | "type": "module", 33 | "scripts": { 34 | "dev": "concurrently \"yarn build --watch\"", 35 | "build": "tsup --onSuccess \"yarn link:self\"", 36 | "type-check": "tsc", 37 | "lint": "eslint --ignore-pattern .gitignore \"{src,docs/src}/**/*.+(ts|js|tsx)\"", 38 | "lint:fix": "yarn lint --fix && prettier --write .", 39 | "prepare": "husky || true", 40 | "commit": "cz", 41 | "link:self": "yarn yalc publish && yarn link", 42 | "final:publish": "yarn build && npm publish" 43 | }, 44 | "files": [ 45 | "dist" 46 | ], 47 | "lint-staged": { 48 | "./{src,tests}/**/*.{ts,js,jsx,tsx}": [ 49 | "eslint --ignore-pattern .gitignore --fix" 50 | ], 51 | "*": "prettier --write" 52 | }, 53 | "config": { 54 | "commitizen": { 55 | "path": "./node_modules/@ryansonshine/cz-conventional-changelog" 56 | } 57 | }, 58 | "engines": { 59 | "node": ">=16" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "7.26.8", 63 | "@babel/preset-env": "7.26.9", 64 | "@babel/preset-react": "7.26.3", 65 | "@babel/preset-typescript": "7.26.0", 66 | "@commitlint/cli": "19.7.1", 67 | "@commitlint/config-conventional": "19.7.1", 68 | "@ryansonshine/commitizen": "4.2.8", 69 | "@ryansonshine/cz-conventional-changelog": "3.3.4", 70 | "@types/google.maps": "^3.58.1", 71 | "@types/node": "^22.13.1", 72 | "@types/react": "^19.0.8", 73 | "@types/react-dom": "^19.0.3", 74 | "@typescript-eslint/eslint-plugin": "8.27.0", 75 | "@typescript-eslint/parser": "8.23.0", 76 | "babel-loader": "9.2.1", 77 | "concurrently": "9.1.2", 78 | "eslint": "9.22.0", 79 | "eslint-config-airbnb": "19.0.4", 80 | "eslint-config-prettier": "10.0.1", 81 | "eslint-plugin-import": "2.31.0", 82 | "eslint-plugin-jsx-a11y": "6.10.2", 83 | "eslint-plugin-prefer-arrow": "1.2.3", 84 | "eslint-plugin-prettier": "5.5.4", 85 | "eslint-plugin-react": "7.37.4", 86 | "eslint-plugin-react-hooks": "5.2.0", 87 | "eslint-plugin-sort-class-members": "^1.21.0", 88 | "eslint-plugin-typescript-sort-keys": "3.3.0", 89 | "eslint-plugin-unused-imports": "4.2.0", 90 | "gh-pages": "^6.3.0", 91 | "husky": "9.1.7", 92 | "lint-staged": "15.4.3", 93 | "prettier": "^3.5.0", 94 | "prop-types": "15.8.1", 95 | "react": "^19.0.0", 96 | "react-dom": "^19.0.0", 97 | "tsup": "8.4.0", 98 | "typescript": "^5.7.3", 99 | "yalc": "1.0.0-pre.53" 100 | }, 101 | "peerDependencies": { 102 | "react": ">=18", 103 | "react-dom": ">=18" 104 | }, 105 | "resolutions": { 106 | "glob-parent": ">=5.1.2", 107 | "parse-url": ">=8.1.0", 108 | "semver": ">=7.5.2", 109 | "trim": ">=0.0.3", 110 | "trim-newlines": ">=3.0.1", 111 | "yaml": ">=2.2.2" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: Deploy Next.js site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ['master'] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: 'pages' 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Detect package manager 35 | id: detect-package-manager 36 | run: | 37 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 38 | echo "manager=yarn" >> $GITHUB_OUTPUT 39 | echo "command=install" >> $GITHUB_OUTPUT 40 | echo "runner=yarn" >> $GITHUB_OUTPUT 41 | exit 0 42 | elif [ -f "${{ github.workspace }}/package.json" ]; then 43 | echo "manager=npm" >> $GITHUB_OUTPUT 44 | echo "command=ci" >> $GITHUB_OUTPUT 45 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 46 | exit 0 47 | else 48 | echo "Unable to determine package manager" 49 | exit 1 50 | fi 51 | - name: Setup Node 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: 'lts/*' 55 | cache: ${{ steps.detect-package-manager.outputs.manager }} 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v4 58 | - name: Restore cache 59 | uses: actions/cache@v4 60 | with: 61 | path: | 62 | ./docs/.next/cache 63 | # Generate a new cache whenever packages or source files change. 64 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 65 | # If source files changed but packages didn't, rebuild from a prior cache. 66 | restore-keys: | 67 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- 68 | - name: Install dependencies (root) 69 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 70 | - name: Install dependencies 71 | run: cd docs && ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 72 | - name: Build with Next.js 73 | run: | 74 | cd docs 75 | touch .env.prod 76 | echo "NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=${{ secrets.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY }}" >> .env.prod 77 | ${{ steps.detect-package-manager.outputs.runner }} build 78 | - name: Upload artifact 79 | uses: actions/upload-pages-artifact@v3 80 | with: 81 | path: ./docs/out 82 | overwrite: true 83 | 84 | # Deployment job 85 | deploy: 86 | environment: 87 | name: github-pages 88 | url: ${{ steps.deployment.outputs.page_url }} 89 | runs-on: ubuntu-latest 90 | needs: build 91 | steps: 92 | - name: Deploy to GitHub Pages 93 | id: deployment 94 | uses: actions/deploy-pages@v4 95 | -------------------------------------------------------------------------------- /docs/src/components/map-options.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxZoom": 20, 3 | "minZoom": 3, 4 | "mapTypeId": "roadmap", 5 | "mapTypeControl": true, 6 | "scaleControl": true, 7 | "streetViewControl": true, 8 | "rotateControl": true, 9 | "fullscreenControl": true, 10 | "zoomControl": true, 11 | "styles": [ 12 | { 13 | "elementType": "geometry", 14 | "stylers": [ 15 | { 16 | "color": "#212121" 17 | } 18 | ] 19 | }, 20 | { 21 | "elementType": "labels.icon", 22 | "stylers": [ 23 | { 24 | "visibility": "off" 25 | } 26 | ] 27 | }, 28 | { 29 | "elementType": "labels.text.fill", 30 | "stylers": [ 31 | { 32 | "color": "#757575" 33 | } 34 | ] 35 | }, 36 | { 37 | "elementType": "labels.text.stroke", 38 | "stylers": [ 39 | { 40 | "color": "#212121" 41 | } 42 | ] 43 | }, 44 | { 45 | "featureType": "administrative", 46 | "elementType": "geometry", 47 | "stylers": [ 48 | { 49 | "color": "#757575" 50 | } 51 | ] 52 | }, 53 | { 54 | "featureType": "administrative.country", 55 | "elementType": "labels.text.fill", 56 | "stylers": [ 57 | { 58 | "color": "#9e9e9e" 59 | } 60 | ] 61 | }, 62 | { 63 | "featureType": "administrative.land_parcel", 64 | "stylers": [ 65 | { 66 | "visibility": "off" 67 | } 68 | ] 69 | }, 70 | { 71 | "featureType": "administrative.locality", 72 | "elementType": "labels.text.fill", 73 | "stylers": [ 74 | { 75 | "color": "#bdbdbd" 76 | } 77 | ] 78 | }, 79 | { 80 | "featureType": "poi", 81 | "elementType": "labels.text.fill", 82 | "stylers": [ 83 | { 84 | "color": "#757575" 85 | } 86 | ] 87 | }, 88 | { 89 | "featureType": "poi.park", 90 | "elementType": "geometry", 91 | "stylers": [ 92 | { 93 | "color": "#181818" 94 | } 95 | ] 96 | }, 97 | { 98 | "featureType": "poi.park", 99 | "elementType": "labels.text.fill", 100 | "stylers": [ 101 | { 102 | "color": "#616161" 103 | } 104 | ] 105 | }, 106 | { 107 | "featureType": "poi.park", 108 | "elementType": "labels.text.stroke", 109 | "stylers": [ 110 | { 111 | "color": "#1b1b1b" 112 | } 113 | ] 114 | }, 115 | { 116 | "featureType": "road", 117 | "elementType": "geometry.fill", 118 | "stylers": [ 119 | { 120 | "color": "#2c2c2c" 121 | } 122 | ] 123 | }, 124 | { 125 | "featureType": "road", 126 | "elementType": "labels.text.fill", 127 | "stylers": [ 128 | { 129 | "color": "#8a8a8a" 130 | } 131 | ] 132 | }, 133 | { 134 | "featureType": "road.arterial", 135 | "elementType": "geometry", 136 | "stylers": [ 137 | { 138 | "color": "#373737" 139 | } 140 | ] 141 | }, 142 | { 143 | "featureType": "road.highway", 144 | "elementType": "geometry", 145 | "stylers": [ 146 | { 147 | "color": "#3c3c3c" 148 | } 149 | ] 150 | }, 151 | { 152 | "featureType": "road.highway.controlled_access", 153 | "elementType": "geometry", 154 | "stylers": [ 155 | { 156 | "color": "#4e4e4e" 157 | } 158 | ] 159 | }, 160 | { 161 | "featureType": "road.local", 162 | "elementType": "labels.text.fill", 163 | "stylers": [ 164 | { 165 | "color": "#616161" 166 | } 167 | ] 168 | }, 169 | { 170 | "featureType": "transit", 171 | "elementType": "labels.text.fill", 172 | "stylers": [ 173 | { 174 | "color": "#757575" 175 | } 176 | ] 177 | }, 178 | { 179 | "featureType": "water", 180 | "elementType": "geometry", 181 | "stylers": [ 182 | { 183 | "color": "#000000" 184 | } 185 | ] 186 | }, 187 | { 188 | "featureType": "water", 189 | "elementType": "labels.text.fill", 190 | "stylers": [ 191 | { 192 | "color": "#3d3d3d" 193 | } 194 | ] 195 | } 196 | ] 197 | } 198 | -------------------------------------------------------------------------------- /docs/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 2rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | width: 100%; 16 | z-index: 2; 17 | font-family: var(--font-mono); 18 | margin-bottom: 1rem; 19 | padding: 1rem; 20 | background-color: rgba(var(--callout-rgb), 0.5); 21 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 22 | border-radius: var(--border-radius); 23 | } 24 | 25 | .card { 26 | padding: 1rem 1.2rem; 27 | border-radius: var(--border-radius); 28 | background: rgba(var(--card-rgb), 0); 29 | border: 1px solid rgba(var(--card-border-rgb), 0); 30 | transition: 31 | background 200ms, 32 | border 200ms; 33 | } 34 | 35 | .card span { 36 | display: inline-block; 37 | transition: transform 200ms; 38 | } 39 | 40 | .card h2 { 41 | font-weight: 600; 42 | margin-bottom: 0.7rem; 43 | } 44 | 45 | .card p { 46 | margin: 0; 47 | opacity: 0.6; 48 | font-size: 0.9rem; 49 | line-height: 1.5; 50 | max-width: 30ch; 51 | } 52 | 53 | .marker { 54 | transform: scale(0); 55 | animation: 400ms reveal forwards; 56 | } 57 | 58 | .mapContainer { 59 | position: relative; 60 | width: 100%; 61 | height: 100%; 62 | } 63 | 64 | .action { 65 | color: #000; 66 | background: rgba(var(--highlight-rgb)); 67 | border: 0; 68 | padding: 10px; 69 | margin: 5px 0; 70 | cursor: pointer; 71 | font-weight: bold; 72 | margin: 1rem 0 0; 73 | } 74 | 75 | .container { 76 | display: flex; 77 | flex-wrap: wrap; 78 | align-items: flex-start; 79 | width: 100%; 80 | gap: 16px; 81 | } 82 | 83 | .container h1, 84 | .container h2, 85 | .container h3 { 86 | color: rgba(var(--highlight-rgb)); 87 | margin: 0.5rem 0; 88 | } 89 | 90 | .container p { 91 | margin-bottom: 8px; 92 | } 93 | 94 | .info, 95 | .bounds { 96 | width: 100%; 97 | flex-basis: 100%; 98 | } 99 | 100 | .buttonContainer { 101 | width: 100%; 102 | flex-basis: 100%; 103 | text-align: center; 104 | margin-top: 1rem; 105 | } 106 | 107 | .grid { 108 | margin-top: 1rem; 109 | display: flex; 110 | justify-content: center; 111 | width: 100%; 112 | } 113 | 114 | .highlighted { 115 | position: absolute; 116 | bottom: 20px; 117 | left: 0; 118 | right: 0; 119 | margin: auto; 120 | max-width: fit-content; 121 | /* card style */ 122 | background-color: #000; 123 | padding: 10px; 124 | border-radius: 5px; 125 | /* text style */ 126 | color: #fff; 127 | font-size: 1rem; 128 | text-align: center; 129 | /* animation */ 130 | transform: scale(0); 131 | animation: 400ms reveal forwards; 132 | } 133 | 134 | .highlighted button { 135 | font-size: 1rem; 136 | color: rgba(var(--highlight-rgb)); 137 | background-color: transparent; 138 | border: 0; 139 | padding: 4px; 140 | cursor: pointer; 141 | } 142 | 143 | /* Enable hover only on non-touch devices */ 144 | @media (hover: hover) and (pointer: fine) { 145 | .card:hover { 146 | background: rgba(var(--card-rgb), 0.1); 147 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 148 | } 149 | 150 | .card:hover span { 151 | transform: translateX(4px); 152 | } 153 | } 154 | 155 | @media (prefers-reduced-motion) { 156 | .card:hover span { 157 | transform: none; 158 | } 159 | } 160 | 161 | /* Mobile */ 162 | @media (max-width: 700px) { 163 | .content { 164 | padding: 4rem; 165 | } 166 | 167 | .card { 168 | padding: 1rem 2.5rem; 169 | } 170 | 171 | .card h2 { 172 | margin-bottom: 0.5rem; 173 | } 174 | 175 | .description { 176 | font-size: 0.8rem; 177 | } 178 | 179 | .description a { 180 | padding: 1rem; 181 | } 182 | } 183 | 184 | /* Desktop */ 185 | @media (min-width: 700px) { 186 | .container { 187 | justify-content: space-between; 188 | } 189 | 190 | .info, 191 | .bounds { 192 | width: calc(50% - 16px); 193 | flex-basis: calc(50% - 16px); 194 | } 195 | } 196 | 197 | @keyframes reveal { 198 | to { 199 | transform: scale(1); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /docs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import GoogleMap, { LatLngBounds, MapContextProps } from 'google-maps-react-markers' 4 | import { useRef, useState } from 'react' 5 | import coordinates from '../components/coordinates' 6 | import Info from '../components/info' 7 | import mapOptions from '../components/map-options.json' 8 | import Marker from '../components/marker' 9 | import styles from './page.module.css' 10 | 11 | export default function Home() { 12 | const mapRef = useRef(null) 13 | const [mapReady, setMapReady] = useState(false) 14 | const [mapBounds, setMapBounds] = useState<{ bounds: number[]; zoom: number }>({ bounds: [], zoom: 0 }) 15 | const [highlighted, setHighlighted] = useState(null) 16 | 17 | const [dragStart, setDragStart] = useState<{ lat: number; lng: number } | null>(null) 18 | const [dragEnd, setDragEnd] = useState<{ lat: number; lng: number } | null>(null) 19 | const [dragging, setDragging] = useState<{ lat: number; lng: number } | null>(null) 20 | 21 | /** 22 | * @description This function is called when the map is ready 23 | * @param {Object} map - reference to the map instance 24 | * @param {Object} maps - reference to the maps library 25 | */ 26 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 27 | const onGoogleApiLoaded = ({ map, maps }: { map: MapContextProps['map']; maps: MapContextProps['maps'] }) => { 28 | mapRef.current = map 29 | setMapReady(true) 30 | } 31 | 32 | const onMarkerClick = (e: any, { markerId /* , lat, lng */ }: { lat: number; lng: number; markerId: string }) => { 33 | setHighlighted(markerId) 34 | } 35 | 36 | const onMapChange = ({ bounds, zoom }: { bounds: LatLngBounds; zoom: number }) => { 37 | const ne = bounds.getNorthEast() 38 | const sw = bounds.getSouthWest() 39 | /** 40 | * useSupercluster accepts bounds in the form of [westLng, southLat, eastLng, northLat] 41 | * const { clusters, supercluster } = useSupercluster({ 42 | * points: points, 43 | * bounds: mapBounds.bounds, 44 | * zoom: mapBounds.zoom, 45 | * }) 46 | */ 47 | setMapBounds({ ...mapBounds, bounds: [sw.lng(), sw.lat(), ne.lng(), ne.lat()], zoom }) 48 | setHighlighted(null) 49 | } 50 | 51 | return ( 52 |
53 |
54 | {mapReady && ( 55 | 63 | )} 64 |
65 |
66 | 75 | {coordinates.map(({ lat, lng, name }, index) => ( 76 | setDragging({ lat: latLng.lat, lng: latLng.lng })} 86 | onDragStart={(e, { latLng }) => setDragStart({ lat: latLng.lat, lng: latLng.lng })} 87 | onDragEnd={(e, { latLng }) => setDragEnd({ lat: latLng.lat, lng: latLng.lng })} 88 | // zIndex={highlighted === name ? 1000 : 0} 89 | /> 90 | ))} 91 | 92 | {highlighted && ( 93 |
94 | {highlighted} 95 | 98 |
99 | )} 100 |
101 | 102 | 115 |
116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /src/map/overlay.tsx: -------------------------------------------------------------------------------- 1 | import { Pane, createOverlayProps } from '../utils/types' 2 | 3 | // return lat, lng from LatLngLiteral 4 | const getLatLng = (LatLng: google.maps.LatLng | null) => { 5 | try { 6 | const latLng = { lat: LatLng?.lat(), lng: LatLng?.lng() } as google.maps.LatLngLiteral 7 | return latLng 8 | } catch (e) { 9 | return LatLng 10 | } 11 | } 12 | 13 | const createOverlay = ({ container, pane, position, maps, drag }: createOverlayProps) => { 14 | class Overlay extends google.maps.OverlayView { 15 | /** 16 | * onAdd is called when the map's panes are ready and the overlay has been 17 | * added to the map. 18 | */ 19 | onAdd = () => { 20 | const that = this 21 | // manage draggable 22 | if (drag?.draggable) { 23 | this.get('map') 24 | .getDiv() 25 | .addEventListener('mouseleave', () => { 26 | google.maps.event.trigger(this.container, 'mouseup') 27 | }) 28 | 29 | this.container.addEventListener('mousedown', (e: MouseEvent) => { 30 | this.container.style.cursor = 'grabbing' 31 | that.map?.set('draggable', false) 32 | that.set('origin', e) 33 | 34 | drag.onDragStart(e, { latLng: getLatLng(this.position) }) 35 | 36 | that.moveHandler = this.get('map') 37 | ?.getDiv() 38 | .addEventListener('mousemove', (evt: MouseEvent) => { 39 | const origin = that.get('origin') as MouseEvent 40 | if (!origin) return 41 | const left = origin.clientX - evt.clientX 42 | const top = origin.clientY - evt.clientY 43 | const pos = that.getProjection()?.fromLatLngToDivPixel(that.position) 44 | if (!pos) return 45 | const latLng = that.getProjection()?.fromDivPixelToLatLng(new maps.Point(pos.x - left, pos.y - top)) 46 | that.set('position', latLng) 47 | that.set('origin', evt) 48 | that.draw() 49 | drag.onDrag(evt, { latLng: getLatLng(latLng) }) 50 | }) 51 | }) 52 | 53 | this.container.addEventListener('mouseup', (e) => { 54 | that.map?.set('draggable', true) 55 | this.container.style.cursor = 'default' 56 | if (that.moveHandler) { 57 | google.maps.event.removeListener(that.moveHandler) 58 | that.moveHandler = null 59 | } 60 | that.set('position', that.position) // set position to last valid position 61 | that.set('origin', undefined) // unset origin so that the next mousedown starts fresh 62 | that.draw() 63 | drag.onDragEnd(e, { latLng: getLatLng(that.position) }) 64 | }) 65 | } 66 | // Add the element to the pane. 67 | const currentPane = this.getPanes()?.[this.pane] as HTMLElement 68 | currentPane?.classList.add('google-map-markers-overlay') 69 | currentPane?.appendChild(this.container) 70 | } 71 | 72 | draw = () => { 73 | // if (!this.map) return 74 | if (!this.container) return 75 | // We use the south-west and north-east points of the overlay to peg it to the correct position and size. 76 | // To do this, we need to retrieve the projection from the overlay. 77 | const overlayProjection = this.getProjection() as google.maps.MapCanvasProjection 78 | // Computes the pixel coordinates of the given geographical location in the DOM element that holds the draggable map. 79 | const point = overlayProjection?.fromLatLngToDivPixel(this.position) as google.maps.Point 80 | 81 | // Manage offset for the overlay, since the overlay is centered on the point 82 | // we need to offset the overlay by half of its width and height 83 | // to make the overlay appear where the point is 84 | const offset = { x: this.container.offsetWidth / 2, y: this.container.offsetHeight / 2 } 85 | 86 | // Hide the overlay if it is outside the map 87 | if (!point) return 88 | 89 | // Set the overlay's position 90 | this.container.style.transform = `translate(${point.x - offset.x}px, ${point.y - offset.y}px)` 91 | } 92 | 93 | /** 94 | * The onRemove() method will be called automatically from the API if 95 | * we ever set the overlay's map property to 'null'. 96 | */ 97 | onRemove = () => { 98 | if (this.container.parentNode !== null) { 99 | // remove DOM listeners 100 | google.maps.event.clearInstanceListeners(this.container) 101 | this.container.parentNode.removeChild(this.container) 102 | } 103 | } 104 | 105 | public container: HTMLDivElement 106 | 107 | public pane: Pane 108 | 109 | public position: google.maps.LatLng 110 | 111 | public map = this.getMap() 112 | 113 | public moveHandler: null 114 | 115 | // eslint-disable-next-line no-shadow 116 | constructor(container: HTMLDivElement, pane: Pane, position: google.maps.LatLng) { 117 | super() 118 | 119 | // Initialize all properties. 120 | this.container = container 121 | this.pane = pane 122 | this.position = position 123 | 124 | this.moveHandler = null 125 | } 126 | } 127 | 128 | return new Overlay(container, pane, position) 129 | } 130 | 131 | export default createOverlay 132 | -------------------------------------------------------------------------------- /src/map/hooks/useScript.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | import { useEffect, useState } from 'react' 3 | import { ScriptProps, UseScriptOptions, UseScriptStatus } from '../../utils/types' 4 | 5 | // Cached script statuses 6 | const cachedScriptStatuses: Record = {} 7 | 8 | function getScriptNode(src: string) { 9 | const node: HTMLScriptElement | null = document.querySelector(`script[src="${src}"]`) 10 | const status = node?.getAttribute('data-status') as UseScriptStatus | undefined 11 | 12 | return { 13 | node, 14 | status, 15 | } 16 | } 17 | 18 | /** 19 | * @description Hook to load external script. 20 | * @param {Object} script - Script to load. 21 | * @param {string} script.src - Script source. 22 | * @param {Object} [script.attributes] - Attributes to add to the script tag. 23 | * @param {Object} [script.callbacks] - Callbacks executed on completion. 24 | * @param {Function} [script.callbacks.onLoadCallback] - Callback executed on completion in case of success. 25 | * @param {Function} [script.callbacks.onErrorCallback] - Callbacks executed on completion in case of error. 26 | * @param {string} [script.elementIdToAppend] - HTML element id to append the script to. Default is HTML HEAD. 27 | * @returns {"idle" | "loading" | "ready" | "error"} status 28 | * 29 | * @example 30 | * const status = useScript({ 31 | * src: "https://script-to-load.js", 32 | * attributes: { id: "scriptId", class: "script-class" }, 33 | * callbacks: { 34 | * onLoadCallback: onLoadFunc, 35 | * onErrorCallback: onErrorFunc, 36 | * }, 37 | * elementIdToAppend: "script-container" 38 | * }, undefined, { removeOnUnmount: true, shouldPreventLoad: false }) 39 | */ 40 | 41 | function useScript( 42 | // eslint-disable-next-line default-param-last 43 | script: ScriptProps = { 44 | src: '', 45 | attributes: {}, 46 | callbacks: { onLoadCallback: () => {}, onErrorCallback: () => {} }, 47 | elementIdToAppend: '', 48 | }, 49 | forcedStatus?: UseScriptStatus, 50 | options: UseScriptOptions = { removeOnUnmount: false, shouldPreventLoad: false }, 51 | ): 'idle' | 'loading' | 'ready' | 'error' { 52 | const [status, setStatus] = useState(() => { 53 | if (!script.src || options?.shouldPreventLoad) { 54 | return 'idle' 55 | } 56 | 57 | if (typeof window === 'undefined') { 58 | // SSR Handling - always return 'loading' 59 | return 'loading' 60 | } 61 | 62 | return cachedScriptStatuses[script.src] ?? 'loading' 63 | }) 64 | 65 | useEffect( 66 | () => { 67 | if (forcedStatus) { 68 | setStatus(forcedStatus) 69 | return 70 | } 71 | 72 | if (!script?.src || options?.shouldPreventLoad) { 73 | return 74 | } 75 | 76 | const cachedScriptStatus = cachedScriptStatuses[script.src] 77 | if (cachedScriptStatus === 'ready' || cachedScriptStatus === 'error') { 78 | // If the script is already cached, set its status immediately 79 | setStatus(cachedScriptStatus) 80 | return 81 | } 82 | 83 | // Fetch existing script element by src 84 | // It may have been added by another instance of this hook 85 | const scriptToAdd = getScriptNode(script.src) 86 | let scriptNode = scriptToAdd.node 87 | 88 | if (!scriptNode) { 89 | // Create script element and add it to document body 90 | scriptNode = document.createElement('script') 91 | scriptNode.src = script.src 92 | scriptNode.async = true 93 | scriptNode.setAttribute('data-status', 'loading') 94 | // Add other script attributes, if they exist 95 | script.attributes && Object.entries(script.attributes).length > 0 96 | ? Object.entries(script.attributes).map(([key, value]) => scriptNode?.setAttribute(key, value)) 97 | : null 98 | if (script.elementIdToAppend && document.getElementById(script.elementIdToAppend)) { 99 | document.getElementById(script.elementIdToAppend)?.appendChild(scriptNode) 100 | } else { 101 | document.body.appendChild(scriptNode) 102 | } 103 | 104 | // Store status in attribute on script 105 | // This can be read by other instances of this hook 106 | const setAttributeFromEvent = (event: Event) => { 107 | const scriptStatus: UseScriptStatus = event.type === 'load' ? 'ready' : 'error' 108 | 109 | scriptNode?.setAttribute('data-status', scriptStatus) 110 | } 111 | 112 | scriptNode.addEventListener('load', setAttributeFromEvent) 113 | scriptNode.addEventListener('error', setAttributeFromEvent) 114 | } else { 115 | // Grab existing script status from attribute and set to state. 116 | const currentScriptStatus = scriptToAdd.status ?? cachedScriptStatus ?? 'loading' 117 | 118 | switch (currentScriptStatus) { 119 | case 'loading': 120 | case 'ready': 121 | script.callbacks?.onLoadCallback ? script.callbacks.onLoadCallback() : null 122 | break 123 | case 'error': 124 | script.callbacks?.onErrorCallback ? script.callbacks.onErrorCallback() : null 125 | break 126 | default: 127 | // loading: do nothing 128 | break 129 | } 130 | 131 | setStatus(currentScriptStatus) 132 | } 133 | 134 | // Script event handler to update status in state 135 | // Note: Even if the script already exists we still need to add 136 | // event handlers to update the state for this hook instance. 137 | const setStateFromEvent = (event: Event) => { 138 | const newStatus = event.type === 'load' ? 'ready' : 'error' 139 | event.type === 'load' 140 | ? script.callbacks?.onLoadCallback 141 | ? script.callbacks.onLoadCallback() 142 | : null 143 | : script.callbacks?.onErrorCallback 144 | ? script.callbacks.onErrorCallback() 145 | : null 146 | 147 | setStatus(newStatus) 148 | cachedScriptStatuses[script.src] = newStatus 149 | } 150 | // Add event listeners 151 | scriptNode.addEventListener('load', setStateFromEvent) 152 | scriptNode.addEventListener('error', setStateFromEvent) 153 | // Remove event listeners on cleanup 154 | // eslint-disable-next-line consistent-return 155 | return () => { 156 | if (scriptNode) { 157 | scriptNode.removeEventListener('load', setStateFromEvent) 158 | scriptNode.removeEventListener('error', setStateFromEvent) 159 | } 160 | 161 | if (scriptNode && options?.removeOnUnmount) { 162 | scriptNode.remove() 163 | } 164 | } 165 | }, 166 | 167 | // Re-run useEffect if script changes 168 | [script, forcedStatus, status], 169 | ) 170 | 171 | return status 172 | } 173 | 174 | export default useScript 175 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import {forwardRef,useRef,useState,useCallback,useEffect,useMemo,Children,isValidElement,cloneElement}from'react';import {createPortal}from'react-dom';import {jsx,jsxs,Fragment}from'react/jsx-runtime';var S={};function W(t){let r=document.querySelector(`script[src="${t}"]`),a=r==null?undefined:r.getAttribute("data-status");return {node:r,status:a}}function Y(t={src:"",attributes:{},callbacks:{onLoadCallback:()=>{},onErrorCallback:()=>{}},elementIdToAppend:""},r,a={removeOnUnmount:false,shouldPreventLoad:false}){let[l,s]=useState(()=>!t.src||a!=null&&a.shouldPreventLoad?"idle":typeof window>"u"?"loading":S[t.src]??"loading");return useEffect(()=>{var p,o,g;if(r){s(r);return}if(!(t!=null&&t.src)||a!=null&&a.shouldPreventLoad)return;let i=S[t.src];if(i==="ready"||i==="error"){s(i);return}let u=W(t.src),e=u.node;if(e){let c=u.status??i??"loading";switch(c){case "loading":case "ready":(o=t.callbacks)!=null&&o.onLoadCallback&&t.callbacks.onLoadCallback();break;case "error":(g=t.callbacks)!=null&&g.onErrorCallback&&t.callbacks.onErrorCallback();break;}s(c);}else {e=document.createElement("script"),e.src=t.src,e.async=true,e.setAttribute("data-status","loading"),t.attributes&&Object.entries(t.attributes).length>0&&Object.entries(t.attributes).map(([m,d])=>e==null?undefined:e.setAttribute(m,d)),t.elementIdToAppend&&document.getElementById(t.elementIdToAppend)?(p=document.getElementById(t.elementIdToAppend))==null||p.appendChild(e):document.body.appendChild(e);let c=m=>{let d=m.type==="load"?"ready":"error";e==null||e.setAttribute("data-status",d);};e.addEventListener("load",c),e.addEventListener("error",c);}let n=c=>{var d,b;let m=c.type==="load"?"ready":"error";c.type==="load"?(d=t.callbacks)!=null&&d.onLoadCallback&&t.callbacks.onLoadCallback():(b=t.callbacks)!=null&&b.onErrorCallback&&t.callbacks.onErrorCallback(),s(m),S[t.src]=m;};return e.addEventListener("load",n),e.addEventListener("error",n),()=>{e&&(e.removeEventListener("load",n),e.removeEventListener("error",n)),e&&(a!=null&&a.removeOnUnmount)&&e.remove();}},[t,r,l]),l}var H=Y;var O=({apiKey:t,libraries:r=[],loadScriptExternally:a=false,status:l="idle",externalApiParams:s,callback:i})=>{var n;typeof window<"u"&&(window.googleMapsCallback=i);let u=(n=new URLSearchParams(s))==null?undefined:n.toString(),e=t?{src:`https://maps.googleapis.com/maps/api/js?key=${t}&callback=googleMapsCallback&libraries=${r==null?undefined:r.join(",")}${u?`&${u}`:""}`,attributes:{id:"googleMapsApi"}}:{src:`https://maps.googleapis.com/maps/api/js?callback=googleMapsCallback&libraries=${r==null?undefined:r.join(",")}`,attributes:{id:"googleMapsApi"}};return H(e,a?l:undefined)};var T=(t,r,a=1e-6)=>{if(t.length&&r.length){for(let l=0;l!==t.length;++l)if(Math.abs(t[l]-r[l])>a)return false;return true}return false};var _=(t,r)=>{let a=useRef(),l=a.current,s=r(l,t);return useEffect(()=>{s||(a.current=t);}),s?l:t},x=_;var k=t=>{try{return {lat:t==null?void 0:t.lat(),lng:t==null?void 0:t.lng()}}catch{return t}},K=({container:t,pane:r,position:a,maps:l,drag:s})=>{class i extends google.maps.OverlayView{onAdd=()=>{var p;let e=this;s!=null&&s.draggable&&(this.get("map").getDiv().addEventListener("mouseleave",()=>{google.maps.event.trigger(this.container,"mouseup");}),this.container.addEventListener("mousedown",o=>{var g,c;this.container.style.cursor="grabbing",(g=e.map)==null||g.set("draggable",false),e.set("origin",o),s.onDragStart(o,{latLng:k(this.position)}),e.moveHandler=(c=this.get("map"))==null?undefined:c.getDiv().addEventListener("mousemove",m=>{var M,f;let d=e.get("origin");if(!d)return;let b=d.clientX-m.clientX,y=d.clientY-m.clientY,v=(M=e.getProjection())==null?undefined:M.fromLatLngToDivPixel(e.position);if(!v)return;let E=(f=e.getProjection())==null?undefined:f.fromDivPixelToLatLng(new l.Point(v.x-b,v.y-y));e.set("position",E),e.set("origin",m),e.draw(),s.onDrag(m,{latLng:k(E)});});}),this.container.addEventListener("mouseup",o=>{var g;(g=e.map)==null||g.set("draggable",true),this.container.style.cursor="default",e.moveHandler&&(google.maps.event.removeListener(e.moveHandler),e.moveHandler=null),e.set("position",e.position),e.set("origin",undefined),e.draw(),s.onDragEnd(o,{latLng:k(e.position)});}));let n=(p=this.getPanes())==null?undefined:p[this.pane];n==null||n.classList.add("google-map-markers-overlay"),n==null||n.appendChild(this.container);};draw=()=>{if(!this.container)return;let e=this.getProjection(),n=e==null?undefined:e.fromLatLngToDivPixel(this.position),p={x:this.container.offsetWidth/2,y:this.container.offsetHeight/2};n&&(this.container.style.transform=`translate(${n.x-p.x}px, ${n.y-p.y}px)`);};onRemove=()=>{this.container.parentNode!==null&&(google.maps.event.clearInstanceListeners(this.container),this.container.parentNode.removeChild(this.container));};container;pane;position;map=this.getMap();moveHandler;constructor(e,n,p){super(),this.container=e,this.pane=n,this.position=p,this.moveHandler=null;}}return new i(t,r,a)},U=K;var ee=({pane:t="floatPane",position:r,map:a,maps:l,zIndex:s=0,children:i,drag:u})=>{let e=useMemo(()=>{let o=document.createElement("div");return o.style.position="absolute",o},[]),n=useMemo(()=>U({container:e,pane:t,position:r,maps:l,drag:u}),[e,u,l,t,r]),p=x(i==null?undefined:i.props,(o,g)=>o&&o.lat===g.lat&&o.lng===g.lng&&o.draggable===g.draggable);return useEffect(()=>!n.map&&a?(n==null||n.setMap(a),()=>{n==null||n.setMap(null);}):()=>{},[a,p]),useEffect(()=>{e.style.zIndex=`${s}`;},[s,e]),createPortal(i,e)},R=ee;var P=()=>{},ae=({children:t,map:r,maps:a})=>{let l=useMemo(()=>!r||!a?[]:Children.map(t,s=>{if(isValidElement(s)){let i={lat:s.props.lat,lng:s.props.lng},{zIndex:u,draggable:e=false,onDragStart:n=P,onDrag:p=P,onDragEnd:o=P}=s.props||{};return s=cloneElement(s,{...s.props,onDragStart:undefined,onDrag:undefined,onDragEnd:undefined}),jsx(R,{position:new a.LatLng(i.lat,i.lng),map:r,maps:a,zIndex:u,drag:{draggable:e,onDragStart:n,onDrag:p,onDragEnd:o},children:s})}return null}),[t,r,a]);return jsx("div",{children:l})},$=ae;function le({children:t=null,style:r={width:"100%",height:"100%",left:0,top:0,margin:0,padding:0,position:"absolute"},defaultCenter:a,defaultZoom:l,onGoogleApiLoaded:s,onChange:i,options:u={},events:e=[]}){let n=useRef(null),p=useRef([]),[o,g]=useState(),[c,m]=useState(),[d,b]=useState(false),y=useCallback(()=>{var v,E;try{if(!o)return;let M=o.getZoom()??l,f=o.getBounds(),F=[(v=o.getCenter())==null?void 0:v.lng(),(E=o.getCenter())==null?void 0:E.lat()],h=f==null?void 0:f.getNorthEast(),L=f==null?void 0:f.getSouthWest();if(!h||!L||!f)return;let D=[L.lng(),L.lat(),h.lng(),h.lat()];T(D,p.current)||(i&&i({zoom:M,center:F,bounds:f}),p.current=D);}catch(M){console.error(M);}},[o,i]);return useEffect(()=>{n.current&&!o&&(g(new google.maps.Map(n.current,{center:a,zoom:l,...u})),m(google.maps));},[a,l,o,n,u]),useEffect(()=>{o&&!d&&(typeof s=="function"&&c&&s({map:o,maps:c,ref:n.current}),b(true),google.maps.event.hasListeners(o,"idle")&&google.maps.event.clearListeners(o,"idle"),google.maps.event.addListener(o,"idle",y));},[d,o,s]),useEffect(()=>()=>{o&&google.maps.event.clearListeners(o,"idle");},[o]),jsxs(Fragment,{children:[jsx("div",{ref:n,style:r,className:"google-map",...e==null?undefined:e.reduce((v,{name:E,handler:M})=>(v[E]=M,v),{})}),t&&o&&c&&jsx($,{map:o,maps:c,children:t})]})}var q=le;var ue=forwardRef(({apiKey:t="",libraries:r=["places","geometry"],children:a=null,loadingContent:l="Google Maps is loading",idleContent:s="Google Maps is on idle",errorContent:i="Google Maps is on error",mapMinHeight:u="unset",containerProps:e={},loadScriptExternally:n=false,status:p="idle",scriptCallback:o=()=>{},externalApiParams:g={},...c},m)=>{let d={ready:jsx(q,{...c,children:a}),loading:l,idle:s,error:i},b=O({apiKey:t,libraries:r,loadScriptExternally:n,status:p,externalApiParams:g,callback:o});return jsx("div",{ref:m,style:{height:"100%",width:"100%",overflow:"hidden",position:"relative",minHeight:u},...e,children:d[b]||null})}),B=ue;var qe=B; 2 | export{qe as default}; -------------------------------------------------------------------------------- /dist/index.cjs: -------------------------------------------------------------------------------- 1 | 'use strict';var react=require('react'),reactDom=require('react-dom'),jsxRuntime=require('react/jsx-runtime');var S={};function W(t){let r=document.querySelector(`script[src="${t}"]`),a=r==null?undefined:r.getAttribute("data-status");return {node:r,status:a}}function Y(t={src:"",attributes:{},callbacks:{onLoadCallback:()=>{},onErrorCallback:()=>{}},elementIdToAppend:""},r,a={removeOnUnmount:false,shouldPreventLoad:false}){let[l,s]=react.useState(()=>!t.src||a!=null&&a.shouldPreventLoad?"idle":typeof window>"u"?"loading":S[t.src]??"loading");return react.useEffect(()=>{var p,o,g;if(r){s(r);return}if(!(t!=null&&t.src)||a!=null&&a.shouldPreventLoad)return;let i=S[t.src];if(i==="ready"||i==="error"){s(i);return}let u=W(t.src),e=u.node;if(e){let c=u.status??i??"loading";switch(c){case "loading":case "ready":(o=t.callbacks)!=null&&o.onLoadCallback&&t.callbacks.onLoadCallback();break;case "error":(g=t.callbacks)!=null&&g.onErrorCallback&&t.callbacks.onErrorCallback();break;}s(c);}else {e=document.createElement("script"),e.src=t.src,e.async=true,e.setAttribute("data-status","loading"),t.attributes&&Object.entries(t.attributes).length>0&&Object.entries(t.attributes).map(([m,d])=>e==null?undefined:e.setAttribute(m,d)),t.elementIdToAppend&&document.getElementById(t.elementIdToAppend)?(p=document.getElementById(t.elementIdToAppend))==null||p.appendChild(e):document.body.appendChild(e);let c=m=>{let d=m.type==="load"?"ready":"error";e==null||e.setAttribute("data-status",d);};e.addEventListener("load",c),e.addEventListener("error",c);}let n=c=>{var d,b;let m=c.type==="load"?"ready":"error";c.type==="load"?(d=t.callbacks)!=null&&d.onLoadCallback&&t.callbacks.onLoadCallback():(b=t.callbacks)!=null&&b.onErrorCallback&&t.callbacks.onErrorCallback(),s(m),S[t.src]=m;};return e.addEventListener("load",n),e.addEventListener("error",n),()=>{e&&(e.removeEventListener("load",n),e.removeEventListener("error",n)),e&&(a!=null&&a.removeOnUnmount)&&e.remove();}},[t,r,l]),l}var H=Y;var O=({apiKey:t,libraries:r=[],loadScriptExternally:a=false,status:l="idle",externalApiParams:s,callback:i})=>{var n;typeof window<"u"&&(window.googleMapsCallback=i);let u=(n=new URLSearchParams(s))==null?undefined:n.toString(),e=t?{src:`https://maps.googleapis.com/maps/api/js?key=${t}&callback=googleMapsCallback&libraries=${r==null?undefined:r.join(",")}${u?`&${u}`:""}`,attributes:{id:"googleMapsApi"}}:{src:`https://maps.googleapis.com/maps/api/js?callback=googleMapsCallback&libraries=${r==null?undefined:r.join(",")}`,attributes:{id:"googleMapsApi"}};return H(e,a?l:undefined)};var T=(t,r,a=1e-6)=>{if(t.length&&r.length){for(let l=0;l!==t.length;++l)if(Math.abs(t[l]-r[l])>a)return false;return true}return false};var _=(t,r)=>{let a=react.useRef(),l=a.current,s=r(l,t);return react.useEffect(()=>{s||(a.current=t);}),s?l:t},x=_;var k=t=>{try{return {lat:t==null?void 0:t.lat(),lng:t==null?void 0:t.lng()}}catch{return t}},K=({container:t,pane:r,position:a,maps:l,drag:s})=>{class i extends google.maps.OverlayView{onAdd=()=>{var p;let e=this;s!=null&&s.draggable&&(this.get("map").getDiv().addEventListener("mouseleave",()=>{google.maps.event.trigger(this.container,"mouseup");}),this.container.addEventListener("mousedown",o=>{var g,c;this.container.style.cursor="grabbing",(g=e.map)==null||g.set("draggable",false),e.set("origin",o),s.onDragStart(o,{latLng:k(this.position)}),e.moveHandler=(c=this.get("map"))==null?undefined:c.getDiv().addEventListener("mousemove",m=>{var M,f;let d=e.get("origin");if(!d)return;let b=d.clientX-m.clientX,y=d.clientY-m.clientY,v=(M=e.getProjection())==null?undefined:M.fromLatLngToDivPixel(e.position);if(!v)return;let E=(f=e.getProjection())==null?undefined:f.fromDivPixelToLatLng(new l.Point(v.x-b,v.y-y));e.set("position",E),e.set("origin",m),e.draw(),s.onDrag(m,{latLng:k(E)});});}),this.container.addEventListener("mouseup",o=>{var g;(g=e.map)==null||g.set("draggable",true),this.container.style.cursor="default",e.moveHandler&&(google.maps.event.removeListener(e.moveHandler),e.moveHandler=null),e.set("position",e.position),e.set("origin",undefined),e.draw(),s.onDragEnd(o,{latLng:k(e.position)});}));let n=(p=this.getPanes())==null?undefined:p[this.pane];n==null||n.classList.add("google-map-markers-overlay"),n==null||n.appendChild(this.container);};draw=()=>{if(!this.container)return;let e=this.getProjection(),n=e==null?undefined:e.fromLatLngToDivPixel(this.position),p={x:this.container.offsetWidth/2,y:this.container.offsetHeight/2};n&&(this.container.style.transform=`translate(${n.x-p.x}px, ${n.y-p.y}px)`);};onRemove=()=>{this.container.parentNode!==null&&(google.maps.event.clearInstanceListeners(this.container),this.container.parentNode.removeChild(this.container));};container;pane;position;map=this.getMap();moveHandler;constructor(e,n,p){super(),this.container=e,this.pane=n,this.position=p,this.moveHandler=null;}}return new i(t,r,a)},U=K;var ee=({pane:t="floatPane",position:r,map:a,maps:l,zIndex:s=0,children:i,drag:u})=>{let e=react.useMemo(()=>{let o=document.createElement("div");return o.style.position="absolute",o},[]),n=react.useMemo(()=>U({container:e,pane:t,position:r,maps:l,drag:u}),[e,u,l,t,r]),p=x(i==null?undefined:i.props,(o,g)=>o&&o.lat===g.lat&&o.lng===g.lng&&o.draggable===g.draggable);return react.useEffect(()=>!n.map&&a?(n==null||n.setMap(a),()=>{n==null||n.setMap(null);}):()=>{},[a,p]),react.useEffect(()=>{e.style.zIndex=`${s}`;},[s,e]),reactDom.createPortal(i,e)},R=ee;var P=()=>{},ae=({children:t,map:r,maps:a})=>{let l=react.useMemo(()=>!r||!a?[]:react.Children.map(t,s=>{if(react.isValidElement(s)){let i={lat:s.props.lat,lng:s.props.lng},{zIndex:u,draggable:e=false,onDragStart:n=P,onDrag:p=P,onDragEnd:o=P}=s.props||{};return s=react.cloneElement(s,{...s.props,onDragStart:undefined,onDrag:undefined,onDragEnd:undefined}),jsxRuntime.jsx(R,{position:new a.LatLng(i.lat,i.lng),map:r,maps:a,zIndex:u,drag:{draggable:e,onDragStart:n,onDrag:p,onDragEnd:o},children:s})}return null}),[t,r,a]);return jsxRuntime.jsx("div",{children:l})},$=ae;function le({children:t=null,style:r={width:"100%",height:"100%",left:0,top:0,margin:0,padding:0,position:"absolute"},defaultCenter:a,defaultZoom:l,onGoogleApiLoaded:s,onChange:i,options:u={},events:e=[]}){let n=react.useRef(null),p=react.useRef([]),[o,g]=react.useState(),[c,m]=react.useState(),[d,b]=react.useState(false),y=react.useCallback(()=>{var v,E;try{if(!o)return;let M=o.getZoom()??l,f=o.getBounds(),F=[(v=o.getCenter())==null?void 0:v.lng(),(E=o.getCenter())==null?void 0:E.lat()],h=f==null?void 0:f.getNorthEast(),L=f==null?void 0:f.getSouthWest();if(!h||!L||!f)return;let D=[L.lng(),L.lat(),h.lng(),h.lat()];T(D,p.current)||(i&&i({zoom:M,center:F,bounds:f}),p.current=D);}catch(M){console.error(M);}},[o,i]);return react.useEffect(()=>{n.current&&!o&&(g(new google.maps.Map(n.current,{center:a,zoom:l,...u})),m(google.maps));},[a,l,o,n,u]),react.useEffect(()=>{o&&!d&&(typeof s=="function"&&c&&s({map:o,maps:c,ref:n.current}),b(true),google.maps.event.hasListeners(o,"idle")&&google.maps.event.clearListeners(o,"idle"),google.maps.event.addListener(o,"idle",y));},[d,o,s]),react.useEffect(()=>()=>{o&&google.maps.event.clearListeners(o,"idle");},[o]),jsxRuntime.jsxs(jsxRuntime.Fragment,{children:[jsxRuntime.jsx("div",{ref:n,style:r,className:"google-map",...e==null?undefined:e.reduce((v,{name:E,handler:M})=>(v[E]=M,v),{})}),t&&o&&c&&jsxRuntime.jsx($,{map:o,maps:c,children:t})]})}var q=le;var ue=react.forwardRef(({apiKey:t="",libraries:r=["places","geometry"],children:a=null,loadingContent:l="Google Maps is loading",idleContent:s="Google Maps is on idle",errorContent:i="Google Maps is on error",mapMinHeight:u="unset",containerProps:e={},loadScriptExternally:n=false,status:p="idle",scriptCallback:o=()=>{},externalApiParams:g={},...c},m)=>{let d={ready:jsxRuntime.jsx(q,{...c,children:a}),loading:l,idle:s,error:i},b=O({apiKey:t,libraries:r,loadScriptExternally:n,status:p,externalApiParams:g,callback:o});return jsxRuntime.jsx("div",{ref:m,style:{height:"100%",width:"100%",overflow:"hidden",position:"relative",minHeight:u},...e,children:d[b]||null})}),B=ue;var qe=B; 2 | module.exports=qe; -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | declare global { 4 | interface Window { 5 | google?: any 6 | googleMapsCallback?: () => void 7 | } 8 | } 9 | 10 | export type MapMouseEvent = google.maps.MapMouseEvent 11 | export type Map = google.maps.Map 12 | export type MapsLibrary = typeof google.maps 13 | export type MapPanes = google.maps.MapPanes 14 | export type MapOptions = google.maps.MapOptions 15 | export type LatLng = google.maps.LatLng 16 | export type LatLngLiteral = google.maps.LatLngLiteral 17 | export type LatLngBounds = google.maps.LatLngBounds 18 | export type LatLngBoundsLiteral = google.maps.LatLngBoundsLiteral 19 | 20 | /** 21 | * The drag configuration of the overlay. 22 | */ 23 | export type Drag = { 24 | /** 25 | * Whether the overlay is draggable. 26 | * @default false 27 | */ 28 | draggable: boolean 29 | /** 30 | * Callback fired repeatedly when the overlay is dragged. 31 | * @param e The event. 32 | * @param props The props. 33 | */ 34 | onDrag: (e: MouseEvent, props: {}) => void 35 | /** 36 | * Callback fired when the drag has ended. 37 | * @param e The event. 38 | * @param props The props. 39 | */ 40 | onDragEnd: (e: MouseEvent, props: {}) => void 41 | /** 42 | * Callback fired when the drag has started. 43 | * @param e The event. 44 | * @param props The props. 45 | */ 46 | onDragStart: (e: MouseEvent, props: {}) => void 47 | } 48 | 49 | export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error' 50 | 51 | export type Pane = 'floatPane' | 'mapPane' | 'markerLayer' | 'overlayLayer' | 'overlayMouseTarget' 52 | 53 | export interface IUseGoogleMaps { 54 | /** 55 | * The Google Maps API key. 56 | */ 57 | apiKey?: string 58 | /** 59 | * The Google Maps API callback. 60 | */ 61 | callback?: () => void 62 | /** 63 | * The Google Maps API params to be appended to the script URL. 64 | * @example 65 | * { 66 | * language: 'en', 67 | * region: 'GB', 68 | * } 69 | */ 70 | externalApiParams?: { [key: string]: any } 71 | /** 72 | * The Google Maps API libraries to be loaded. 73 | * @default ['places', 'geometry'] 74 | */ 75 | libraries?: string[] 76 | /** 77 | * Whether to manage the script loading externally. 78 | * @default false 79 | */ 80 | loadScriptExternally?: boolean 81 | /** 82 | * The status of the script loading. 83 | * To be used only if `loadScriptExternally` is `true`. 84 | */ 85 | status?: UseScriptStatus 86 | } 87 | 88 | export interface ScriptProps { 89 | /** 90 | * The attributes to be passed to the script element. 91 | */ 92 | attributes?: { [key: string]: string } 93 | /** 94 | * The callback to be called when the script has loaded 95 | * successfully or has failed to load. 96 | */ 97 | callbacks?: { 98 | onErrorCallback?: () => void 99 | onLoadCallback?: () => void 100 | } 101 | /** 102 | * The ID of the HTML element where the script will be appended. 103 | * If not provided, the script will be appended to the `body` element. 104 | */ 105 | elementIdToAppend?: string 106 | /** 107 | * The URL of the script to be loaded. 108 | */ 109 | src: string 110 | } 111 | 112 | export interface UseScriptOptions { 113 | /** 114 | * Whether to remove the script when the component unmounts. 115 | */ 116 | removeOnUnmount?: boolean 117 | /** 118 | * Whether to prevent the script from loading. 119 | * @default false 120 | */ 121 | shouldPreventLoad?: boolean 122 | } 123 | 124 | export interface MapContextProps { 125 | /** 126 | * The Google Maps instance. 127 | */ 128 | map: Map 129 | /** 130 | * The Google Maps API object. 131 | */ 132 | maps: MapsLibrary 133 | } 134 | 135 | export interface OverlayViewProps extends MapContextProps { 136 | /** 137 | * The children to be rendered within the overlay. 138 | */ 139 | children?: React.ReactElement 140 | /** 141 | * The drag configuration of the overlay. 142 | * @default { draggable: false } 143 | */ 144 | drag?: Drag 145 | /** 146 | * The map pane in which to render the overlay. 147 | * @default 'floatPane' 148 | */ 149 | pane?: Pane | undefined 150 | /** 151 | * The geographical coordinates of the overlay. 152 | */ 153 | position: LatLng 154 | /** 155 | * The z-index of the overlay. 156 | * @default 0 157 | */ 158 | zIndex?: number | 0 159 | } 160 | 161 | export interface createOverlayProps { 162 | /** 163 | * The HTML container element in which to render the overlay. 164 | */ 165 | container: HTMLDivElement 166 | /** 167 | * The drag configuration of the overlay. 168 | * @default { draggable: false } 169 | */ 170 | drag?: Drag 171 | /** 172 | * The Google Maps API object. 173 | */ 174 | maps: MapContextProps['maps'] 175 | /** 176 | * The map pane in which to render the overlay. 177 | * @default 'floatPane' 178 | */ 179 | pane: Pane 180 | /** 181 | * The geographical coordinates of the overlay. 182 | */ 183 | position: LatLng 184 | } 185 | 186 | export interface EventProps { 187 | /** 188 | * The event handler. 189 | * @param e The event. 190 | */ 191 | handler: (e: any) => void 192 | /** 193 | * The HTML Event attribute name. 194 | * @reference https://www.w3schools.com/tags/ref_eventattributes.asp 195 | * @example 196 | * 'onClick' 197 | */ 198 | name: string 199 | } 200 | 201 | export interface onGoogleApiLoadedProps extends MapContextProps { 202 | /** 203 | * The ref of the Map. 204 | */ 205 | ref: HTMLDivElement | null 206 | } 207 | 208 | export interface MapMarkersProps extends MapContextProps { 209 | /** 210 | * The Markers to be rendered on the map. 211 | */ 212 | children: React.ReactNode 213 | } 214 | 215 | export interface MapProps { 216 | /** 217 | * The Markers to be rendered on the map 218 | */ 219 | children?: React.ReactNode 220 | /** 221 | * The default center of the map. 222 | */ 223 | defaultCenter: LatLngLiteral 224 | /** 225 | * The default zoom of the map. 226 | */ 227 | defaultZoom: number 228 | /** 229 | * The events to pass to the Google Maps instance (`div`). 230 | * @type {Array} 231 | * @example 232 | * [ 233 | * { 234 | * name: 'onClick', 235 | * handler: (event) => { ... } 236 | * } 237 | * ] 238 | */ 239 | events?: EventProps[] 240 | /** 241 | * The callback fired when the map changes (center, zoom, bounds). 242 | */ 243 | onChange?: (options: { bounds: LatLngBounds; center: (number | undefined)[]; zoom: number }) => void 244 | /** 245 | * The callback fired when the map is loaded. 246 | */ 247 | onGoogleApiLoaded?: (props: onGoogleApiLoadedProps) => void 248 | /** 249 | * The options to pass to the Google Maps instance. 250 | * @reference https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions 251 | */ 252 | options?: MapOptions 253 | /** 254 | * The style of the map container. 255 | * @default { 256 | * width: '100%', 257 | * height: '100%', 258 | * left: 0, 259 | * top: 0, 260 | * margin: 0, 261 | * padding: 0, 262 | * position: 'absolute' 263 | * } 264 | */ 265 | style?: React.CSSProperties 266 | } 267 | 268 | export interface GoogleMapProps extends MapProps, IUseGoogleMaps { 269 | /** 270 | * The props to pass to the `div` container element. 271 | */ 272 | containerProps?: React.HTMLAttributes 273 | /** 274 | * The content to be rendered when the script fails to load. 275 | * @default 'Google Maps is on error' 276 | */ 277 | errorContent?: React.ReactNode 278 | /** 279 | * The content to be rendered when the script is on idle. 280 | * @default 'Google Maps is on idle' 281 | */ 282 | idleContent?: React.ReactNode 283 | /** 284 | * The content to be rendered while the script is loading. 285 | * @default 'Google Maps is loading' 286 | */ 287 | loadingContent?: React.ReactNode 288 | /** 289 | * The minimum height of the map container. 290 | * @default 'unset' 291 | */ 292 | mapMinHeight?: string 293 | /** 294 | * The Google Maps API script callback 295 | */ 296 | scriptCallback?: () => void 297 | } 298 | -------------------------------------------------------------------------------- /dist/index.d.cts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import React__default from 'react'; 3 | 4 | declare global { 5 | interface Window { 6 | google?: any; 7 | googleMapsCallback?: () => void; 8 | } 9 | } 10 | type MapMouseEvent = google.maps.MapMouseEvent; 11 | type Map = google.maps.Map; 12 | type MapsLibrary = typeof google.maps; 13 | type MapPanes = google.maps.MapPanes; 14 | type MapOptions = google.maps.MapOptions; 15 | type LatLng = google.maps.LatLng; 16 | type LatLngLiteral = google.maps.LatLngLiteral; 17 | type LatLngBounds = google.maps.LatLngBounds; 18 | type LatLngBoundsLiteral = google.maps.LatLngBoundsLiteral; 19 | /** 20 | * The drag configuration of the overlay. 21 | */ 22 | type Drag = { 23 | /** 24 | * Whether the overlay is draggable. 25 | * @default false 26 | */ 27 | draggable: boolean; 28 | /** 29 | * Callback fired repeatedly when the overlay is dragged. 30 | * @param e The event. 31 | * @param props The props. 32 | */ 33 | onDrag: (e: MouseEvent, props: {}) => void; 34 | /** 35 | * Callback fired when the drag has ended. 36 | * @param e The event. 37 | * @param props The props. 38 | */ 39 | onDragEnd: (e: MouseEvent, props: {}) => void; 40 | /** 41 | * Callback fired when the drag has started. 42 | * @param e The event. 43 | * @param props The props. 44 | */ 45 | onDragStart: (e: MouseEvent, props: {}) => void; 46 | }; 47 | type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'; 48 | type Pane = 'floatPane' | 'mapPane' | 'markerLayer' | 'overlayLayer' | 'overlayMouseTarget'; 49 | interface IUseGoogleMaps { 50 | /** 51 | * The Google Maps API key. 52 | */ 53 | apiKey?: string; 54 | /** 55 | * The Google Maps API callback. 56 | */ 57 | callback?: () => void; 58 | /** 59 | * The Google Maps API params to be appended to the script URL. 60 | * @example 61 | * { 62 | * language: 'en', 63 | * region: 'GB', 64 | * } 65 | */ 66 | externalApiParams?: { 67 | [key: string]: any; 68 | }; 69 | /** 70 | * The Google Maps API libraries to be loaded. 71 | * @default ['places', 'geometry'] 72 | */ 73 | libraries?: string[]; 74 | /** 75 | * Whether to manage the script loading externally. 76 | * @default false 77 | */ 78 | loadScriptExternally?: boolean; 79 | /** 80 | * The status of the script loading. 81 | * To be used only if `loadScriptExternally` is `true`. 82 | */ 83 | status?: UseScriptStatus; 84 | } 85 | interface ScriptProps { 86 | /** 87 | * The attributes to be passed to the script element. 88 | */ 89 | attributes?: { 90 | [key: string]: string; 91 | }; 92 | /** 93 | * The callback to be called when the script has loaded 94 | * successfully or has failed to load. 95 | */ 96 | callbacks?: { 97 | onErrorCallback?: () => void; 98 | onLoadCallback?: () => void; 99 | }; 100 | /** 101 | * The ID of the HTML element where the script will be appended. 102 | * If not provided, the script will be appended to the `body` element. 103 | */ 104 | elementIdToAppend?: string; 105 | /** 106 | * The URL of the script to be loaded. 107 | */ 108 | src: string; 109 | } 110 | interface UseScriptOptions { 111 | /** 112 | * Whether to remove the script when the component unmounts. 113 | */ 114 | removeOnUnmount?: boolean; 115 | /** 116 | * Whether to prevent the script from loading. 117 | * @default false 118 | */ 119 | shouldPreventLoad?: boolean; 120 | } 121 | interface MapContextProps { 122 | /** 123 | * The Google Maps instance. 124 | */ 125 | map: Map; 126 | /** 127 | * The Google Maps API object. 128 | */ 129 | maps: MapsLibrary; 130 | } 131 | interface OverlayViewProps extends MapContextProps { 132 | /** 133 | * The children to be rendered within the overlay. 134 | */ 135 | children?: React__default.ReactElement; 136 | /** 137 | * The drag configuration of the overlay. 138 | * @default { draggable: false } 139 | */ 140 | drag?: Drag; 141 | /** 142 | * The map pane in which to render the overlay. 143 | * @default 'floatPane' 144 | */ 145 | pane?: Pane | undefined; 146 | /** 147 | * The geographical coordinates of the overlay. 148 | */ 149 | position: LatLng; 150 | /** 151 | * The z-index of the overlay. 152 | * @default 0 153 | */ 154 | zIndex?: number | 0; 155 | } 156 | interface createOverlayProps { 157 | /** 158 | * The HTML container element in which to render the overlay. 159 | */ 160 | container: HTMLDivElement; 161 | /** 162 | * The drag configuration of the overlay. 163 | * @default { draggable: false } 164 | */ 165 | drag?: Drag; 166 | /** 167 | * The Google Maps API object. 168 | */ 169 | maps: MapContextProps['maps']; 170 | /** 171 | * The map pane in which to render the overlay. 172 | * @default 'floatPane' 173 | */ 174 | pane: Pane; 175 | /** 176 | * The geographical coordinates of the overlay. 177 | */ 178 | position: LatLng; 179 | } 180 | interface EventProps { 181 | /** 182 | * The event handler. 183 | * @param e The event. 184 | */ 185 | handler: (e: any) => void; 186 | /** 187 | * The HTML Event attribute name. 188 | * @reference https://www.w3schools.com/tags/ref_eventattributes.asp 189 | * @example 190 | * 'onClick' 191 | */ 192 | name: string; 193 | } 194 | interface onGoogleApiLoadedProps extends MapContextProps { 195 | /** 196 | * The ref of the Map. 197 | */ 198 | ref: HTMLDivElement | null; 199 | } 200 | interface MapMarkersProps extends MapContextProps { 201 | /** 202 | * The Markers to be rendered on the map. 203 | */ 204 | children: React__default.ReactNode; 205 | } 206 | interface MapProps { 207 | /** 208 | * The Markers to be rendered on the map 209 | */ 210 | children?: React__default.ReactNode; 211 | /** 212 | * The default center of the map. 213 | */ 214 | defaultCenter: LatLngLiteral; 215 | /** 216 | * The default zoom of the map. 217 | */ 218 | defaultZoom: number; 219 | /** 220 | * The events to pass to the Google Maps instance (`div`). 221 | * @type {Array} 222 | * @example 223 | * [ 224 | * { 225 | * name: 'onClick', 226 | * handler: (event) => { ... } 227 | * } 228 | * ] 229 | */ 230 | events?: EventProps[]; 231 | /** 232 | * The callback fired when the map changes (center, zoom, bounds). 233 | */ 234 | onChange?: (options: { 235 | bounds: LatLngBounds; 236 | center: (number | undefined)[]; 237 | zoom: number; 238 | }) => void; 239 | /** 240 | * The callback fired when the map is loaded. 241 | */ 242 | onGoogleApiLoaded?: (props: onGoogleApiLoadedProps) => void; 243 | /** 244 | * The options to pass to the Google Maps instance. 245 | * @reference https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions 246 | */ 247 | options?: MapOptions; 248 | /** 249 | * The style of the map container. 250 | * @default { 251 | * width: '100%', 252 | * height: '100%', 253 | * left: 0, 254 | * top: 0, 255 | * margin: 0, 256 | * padding: 0, 257 | * position: 'absolute' 258 | * } 259 | */ 260 | style?: React__default.CSSProperties; 261 | } 262 | interface GoogleMapProps extends MapProps, IUseGoogleMaps { 263 | /** 264 | * The props to pass to the `div` container element. 265 | */ 266 | containerProps?: React__default.HTMLAttributes; 267 | /** 268 | * The content to be rendered when the script fails to load. 269 | * @default 'Google Maps is on error' 270 | */ 271 | errorContent?: React__default.ReactNode; 272 | /** 273 | * The content to be rendered when the script is on idle. 274 | * @default 'Google Maps is on idle' 275 | */ 276 | idleContent?: React__default.ReactNode; 277 | /** 278 | * The content to be rendered while the script is loading. 279 | * @default 'Google Maps is loading' 280 | */ 281 | loadingContent?: React__default.ReactNode; 282 | /** 283 | * The minimum height of the map container. 284 | * @default 'unset' 285 | */ 286 | mapMinHeight?: string; 287 | /** 288 | * The Google Maps API script callback 289 | */ 290 | scriptCallback?: () => void; 291 | } 292 | 293 | declare const GoogleMap: React.ForwardRefExoticComponent>; 294 | 295 | export { type Drag, type EventProps, type GoogleMapProps, type IUseGoogleMaps, type LatLng, type LatLngBounds, type LatLngBoundsLiteral, type LatLngLiteral, type Map, type MapContextProps, type MapMarkersProps, type MapMouseEvent, type MapOptions, type MapPanes, type MapProps, type MapsLibrary, type OverlayViewProps, type Pane, type ScriptProps, type UseScriptOptions, type UseScriptStatus, type createOverlayProps, GoogleMap as default, type onGoogleApiLoadedProps }; 296 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import React__default from 'react'; 3 | 4 | declare global { 5 | interface Window { 6 | google?: any; 7 | googleMapsCallback?: () => void; 8 | } 9 | } 10 | type MapMouseEvent = google.maps.MapMouseEvent; 11 | type Map = google.maps.Map; 12 | type MapsLibrary = typeof google.maps; 13 | type MapPanes = google.maps.MapPanes; 14 | type MapOptions = google.maps.MapOptions; 15 | type LatLng = google.maps.LatLng; 16 | type LatLngLiteral = google.maps.LatLngLiteral; 17 | type LatLngBounds = google.maps.LatLngBounds; 18 | type LatLngBoundsLiteral = google.maps.LatLngBoundsLiteral; 19 | /** 20 | * The drag configuration of the overlay. 21 | */ 22 | type Drag = { 23 | /** 24 | * Whether the overlay is draggable. 25 | * @default false 26 | */ 27 | draggable: boolean; 28 | /** 29 | * Callback fired repeatedly when the overlay is dragged. 30 | * @param e The event. 31 | * @param props The props. 32 | */ 33 | onDrag: (e: MouseEvent, props: {}) => void; 34 | /** 35 | * Callback fired when the drag has ended. 36 | * @param e The event. 37 | * @param props The props. 38 | */ 39 | onDragEnd: (e: MouseEvent, props: {}) => void; 40 | /** 41 | * Callback fired when the drag has started. 42 | * @param e The event. 43 | * @param props The props. 44 | */ 45 | onDragStart: (e: MouseEvent, props: {}) => void; 46 | }; 47 | type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error'; 48 | type Pane = 'floatPane' | 'mapPane' | 'markerLayer' | 'overlayLayer' | 'overlayMouseTarget'; 49 | interface IUseGoogleMaps { 50 | /** 51 | * The Google Maps API key. 52 | */ 53 | apiKey?: string; 54 | /** 55 | * The Google Maps API callback. 56 | */ 57 | callback?: () => void; 58 | /** 59 | * The Google Maps API params to be appended to the script URL. 60 | * @example 61 | * { 62 | * language: 'en', 63 | * region: 'GB', 64 | * } 65 | */ 66 | externalApiParams?: { 67 | [key: string]: any; 68 | }; 69 | /** 70 | * The Google Maps API libraries to be loaded. 71 | * @default ['places', 'geometry'] 72 | */ 73 | libraries?: string[]; 74 | /** 75 | * Whether to manage the script loading externally. 76 | * @default false 77 | */ 78 | loadScriptExternally?: boolean; 79 | /** 80 | * The status of the script loading. 81 | * To be used only if `loadScriptExternally` is `true`. 82 | */ 83 | status?: UseScriptStatus; 84 | } 85 | interface ScriptProps { 86 | /** 87 | * The attributes to be passed to the script element. 88 | */ 89 | attributes?: { 90 | [key: string]: string; 91 | }; 92 | /** 93 | * The callback to be called when the script has loaded 94 | * successfully or has failed to load. 95 | */ 96 | callbacks?: { 97 | onErrorCallback?: () => void; 98 | onLoadCallback?: () => void; 99 | }; 100 | /** 101 | * The ID of the HTML element where the script will be appended. 102 | * If not provided, the script will be appended to the `body` element. 103 | */ 104 | elementIdToAppend?: string; 105 | /** 106 | * The URL of the script to be loaded. 107 | */ 108 | src: string; 109 | } 110 | interface UseScriptOptions { 111 | /** 112 | * Whether to remove the script when the component unmounts. 113 | */ 114 | removeOnUnmount?: boolean; 115 | /** 116 | * Whether to prevent the script from loading. 117 | * @default false 118 | */ 119 | shouldPreventLoad?: boolean; 120 | } 121 | interface MapContextProps { 122 | /** 123 | * The Google Maps instance. 124 | */ 125 | map: Map; 126 | /** 127 | * The Google Maps API object. 128 | */ 129 | maps: MapsLibrary; 130 | } 131 | interface OverlayViewProps extends MapContextProps { 132 | /** 133 | * The children to be rendered within the overlay. 134 | */ 135 | children?: React__default.ReactElement; 136 | /** 137 | * The drag configuration of the overlay. 138 | * @default { draggable: false } 139 | */ 140 | drag?: Drag; 141 | /** 142 | * The map pane in which to render the overlay. 143 | * @default 'floatPane' 144 | */ 145 | pane?: Pane | undefined; 146 | /** 147 | * The geographical coordinates of the overlay. 148 | */ 149 | position: LatLng; 150 | /** 151 | * The z-index of the overlay. 152 | * @default 0 153 | */ 154 | zIndex?: number | 0; 155 | } 156 | interface createOverlayProps { 157 | /** 158 | * The HTML container element in which to render the overlay. 159 | */ 160 | container: HTMLDivElement; 161 | /** 162 | * The drag configuration of the overlay. 163 | * @default { draggable: false } 164 | */ 165 | drag?: Drag; 166 | /** 167 | * The Google Maps API object. 168 | */ 169 | maps: MapContextProps['maps']; 170 | /** 171 | * The map pane in which to render the overlay. 172 | * @default 'floatPane' 173 | */ 174 | pane: Pane; 175 | /** 176 | * The geographical coordinates of the overlay. 177 | */ 178 | position: LatLng; 179 | } 180 | interface EventProps { 181 | /** 182 | * The event handler. 183 | * @param e The event. 184 | */ 185 | handler: (e: any) => void; 186 | /** 187 | * The HTML Event attribute name. 188 | * @reference https://www.w3schools.com/tags/ref_eventattributes.asp 189 | * @example 190 | * 'onClick' 191 | */ 192 | name: string; 193 | } 194 | interface onGoogleApiLoadedProps extends MapContextProps { 195 | /** 196 | * The ref of the Map. 197 | */ 198 | ref: HTMLDivElement | null; 199 | } 200 | interface MapMarkersProps extends MapContextProps { 201 | /** 202 | * The Markers to be rendered on the map. 203 | */ 204 | children: React__default.ReactNode; 205 | } 206 | interface MapProps { 207 | /** 208 | * The Markers to be rendered on the map 209 | */ 210 | children?: React__default.ReactNode; 211 | /** 212 | * The default center of the map. 213 | */ 214 | defaultCenter: LatLngLiteral; 215 | /** 216 | * The default zoom of the map. 217 | */ 218 | defaultZoom: number; 219 | /** 220 | * The events to pass to the Google Maps instance (`div`). 221 | * @type {Array} 222 | * @example 223 | * [ 224 | * { 225 | * name: 'onClick', 226 | * handler: (event) => { ... } 227 | * } 228 | * ] 229 | */ 230 | events?: EventProps[]; 231 | /** 232 | * The callback fired when the map changes (center, zoom, bounds). 233 | */ 234 | onChange?: (options: { 235 | bounds: LatLngBounds; 236 | center: (number | undefined)[]; 237 | zoom: number; 238 | }) => void; 239 | /** 240 | * The callback fired when the map is loaded. 241 | */ 242 | onGoogleApiLoaded?: (props: onGoogleApiLoadedProps) => void; 243 | /** 244 | * The options to pass to the Google Maps instance. 245 | * @reference https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions 246 | */ 247 | options?: MapOptions; 248 | /** 249 | * The style of the map container. 250 | * @default { 251 | * width: '100%', 252 | * height: '100%', 253 | * left: 0, 254 | * top: 0, 255 | * margin: 0, 256 | * padding: 0, 257 | * position: 'absolute' 258 | * } 259 | */ 260 | style?: React__default.CSSProperties; 261 | } 262 | interface GoogleMapProps extends MapProps, IUseGoogleMaps { 263 | /** 264 | * The props to pass to the `div` container element. 265 | */ 266 | containerProps?: React__default.HTMLAttributes; 267 | /** 268 | * The content to be rendered when the script fails to load. 269 | * @default 'Google Maps is on error' 270 | */ 271 | errorContent?: React__default.ReactNode; 272 | /** 273 | * The content to be rendered when the script is on idle. 274 | * @default 'Google Maps is on idle' 275 | */ 276 | idleContent?: React__default.ReactNode; 277 | /** 278 | * The content to be rendered while the script is loading. 279 | * @default 'Google Maps is loading' 280 | */ 281 | loadingContent?: React__default.ReactNode; 282 | /** 283 | * The minimum height of the map container. 284 | * @default 'unset' 285 | */ 286 | mapMinHeight?: string; 287 | /** 288 | * The Google Maps API script callback 289 | */ 290 | scriptCallback?: () => void; 291 | } 292 | 293 | declare const GoogleMap: React.ForwardRefExoticComponent>; 294 | 295 | export { type Drag, type EventProps, type GoogleMapProps, type IUseGoogleMaps, type LatLng, type LatLngBounds, type LatLngBoundsLiteral, type LatLngLiteral, type Map, type MapContextProps, type MapMarkersProps, type MapMouseEvent, type MapOptions, type MapPanes, type MapProps, type MapsLibrary, type OverlayViewProps, type Pane, type ScriptProps, type UseScriptOptions, type UseScriptStatus, type createOverlayProps, GoogleMap as default, type onGoogleApiLoadedProps }; 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Google Maps React Markers

2 | 3 |

4 | 5 | NPM 6 | 7 | 8 | NPM total downloads 9 | 10 | Maintained 11 | 12 | GitHub license: MIT 13 | 14 | GitHub stars 15 | 16 | GitHub open issues 17 | 18 | 19 | PRs welcome 20 | 21 |

22 | 23 | Google Maps library that accepts markers as react components and works with React 18+. 24 | 25 | It supports a small set of the props of [Google Map React](https://github.com/google-map-react/google-map-react). Clustering also is possible. 26 | The library implements [Google Maps Custom Overlays](https://developers.google.com/maps/documentation/javascript/customoverlays) official library. 27 | 28 | **If you like this library, please consider supporting me ❤️** 29 | 30 | [![Buy me a Coffee](https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/giorgiabosello) 31 | [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/giorgiabosello) 32 | 33 | ## 💬 Discussion 34 | 35 | > **Released TypeScript version of the library!** 36 | 37 | Feedbacks are welcome in [this discussion](https://github.com/giorgiabosello/google-maps-react-markers/discussions/109). 38 | 39 | ## 🚀 Demo 40 | 41 | 42 | 43 | 44 | 45 | See it in action [here](https://giorgiabosello.github.io/google-maps-react-markers/) _(API KEY not provided)_. 46 | 47 | Demo source code is available [here](https://github.com/giorgiabosello/google-maps-react-markers/tree/master/docs/src). 48 | 49 | ## 🛠 Install 50 | 51 | ```bash 52 | pnpm add google-maps-react-markers 53 | ``` 54 | 55 | or 56 | 57 | ```bash 58 | yarn add google-maps-react-markers 59 | ``` 60 | 61 | or 62 | 63 | ```bash 64 | npm install --save google-maps-react-markers 65 | ``` 66 | 67 | ## 💻 Usage 68 | 69 | ```jsx 70 | import GoogleMap from 'google-maps-react-markers' 71 | 72 | const App = () => { 73 | const mapRef = useRef(null) 74 | const [mapReady, setMapReady] = useState(false) 75 | 76 | /** 77 | * @description This function is called when the map is ready 78 | * @param {Object} map - reference to the map instance 79 | * @param {Object} maps - reference to the maps library 80 | */ 81 | const onGoogleApiLoaded = ({ map, maps }) => { 82 | mapRef.current = map 83 | setMapReady(true) 84 | } 85 | 86 | const onMarkerClick = (e, { markerId, lat, lng }) => { 87 | console.log('This is ->', markerId) 88 | 89 | // inside the map instance you can call any google maps method 90 | mapRef.current.setCenter({ lat, lng }) 91 | // ref. https://developers.google.com/maps/documentation/javascript/reference?hl=it 92 | } 93 | 94 | return ( 95 | <> 96 | {mapReady &&
Map is ready. See for logs in developer console.
} 97 | console.log('Map moved', map)} 105 | > 106 | {coordinates.map(({ lat, lng, name }, index) => ( 107 | {}} 115 | // onDrag={(e, { latLng }) => {}} 116 | // onDragEnd={(e, { latLng }) => {}} 117 | /> 118 | ))} 119 | 120 | 121 | ) 122 | } 123 | 124 | export default App 125 | ``` 126 | 127 | ## 🧐 Props 128 | 129 | ### GoogleMap component 130 | 131 | | Prop | Type | Required | Default | Description | 132 | | -------------------- | -------- | -------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 133 | | **apiKey** | string | **yes** | `''` | API Key to load Google Maps | 134 | | **defaultCenter** | object | **yes** | `{ lat: 0, lng: 0 }` | Default center of the map | 135 | | **defaultZoom** | number | **yes** | `1-20` | Default zoom of the map | 136 | | libraries | array | no | `['places', 'geometry']` | Libraries to load | 137 | | options | object | no | `{}` | Options for the map | 138 | | onGoogleApiLoaded | function | no | `() => {}` | Callback when the map is loaded | 139 | | onChange | function | no | `() => {}` | Callback when the map has changed | 140 | | events | array | no | `[]` | Array of objects name/handler of [DOM events](https://en.wikipedia.org/wiki/DOM_event) to pass down to the `div` overlay. Example: `events: [{ name: 'onClick', handler: () => {} }]` | 141 | | children | node | no | `null` | Markers of the map | 142 | | loadScriptExternally | bool | no | `false` | Whether to load the Google Maps script externally.
If `true`, the `status` prop is required and it will be used to control the loading of the script | 143 | | status | string | no | `idle` | The forced status of the Google Maps script. Depends on `loadScriptExternally`.
It can be one of `idle`, `loading`, `ready`, `error` | 144 | | loadingContent | node | no | `'Google Maps is loading'` | Content to show while the map is loading | 145 | | idleContent | node | no | `'Google Maps is on idle'` | Content to show when the map is idle | 146 | | errorContent | node | no | `'Google Maps is on error'` | Content to show when the map has an error | 147 | | mapMinHeight | string | no | `'unset'` | Min height of the map | 148 | | containerProps | object | no | `{}` | Props for the div container of the map | 149 | | scriptCallback | function | no | `() => {}` | window global callback passed to the Google Script | 150 | | externalApiParams | object | no | `undefined` | Optional params to pass to the Google API script. Eg. `{region: 'IT', language: 'it'}` | 151 | 152 | ### Markers 153 | 154 | | Prop | Type | Required | Default | Description | 155 | | ----------- | ------ | -------- | ----------- | -------------------------------------------------------------------------------------------------------- | 156 | | **lat** | number | **yes** | `undefined` | Latitude of the marker | 157 | | **lng** | number | **yes** | `undefined` | Longitude of the marker | 158 | | draggable | bool | no | `false` | If true, the marker can be dragged | 159 | | onDragStart | func | no | `() => {}` | This event is fired when the user starts dragging the marker | 160 | | onDrag | func | no | `() => {}` | This event is repeatedly fired while the user drags the marker | 161 | | onDragEnd | func | no | `() => {}` | This event is fired when the user stops dragging the marker | 162 | | zIndex | number | no | 0 | The z-index of the marker. To bring the marker to the front, e.g. when it is active, set a higher value. | 163 | 164 | ## 📍 Clustering 165 | 166 | For clustering, follow this [guide](https://www.leighhalliday.com/google-maps-clustering) using [useSupercluster Hook](https://github.com/leighhalliday/use-supercluster), but use bounds in this way: 167 | 168 | ```jsx 169 | const [mapBounds, setMapBounds] = useState({ 170 | bounds: [0, 0, 0, 0], 171 | zoom: 0, 172 | }) 173 | const onMapChange = ({ bounds, zoom }) => { 174 | const ne = bounds.getNorthEast() 175 | const sw = bounds.getSouthWest() 176 | /** 177 | * useSupercluster accepts bounds in the form of [westLng, southLat, eastLng, northLat] 178 | * const { clusters, supercluster } = useSupercluster({ 179 | * points: points, 180 | * bounds: mapBounds.bounds, 181 | * zoom: mapBounds.zoom, 182 | * }) 183 | */ 184 | setMapBounds({ ...mapBounds, bounds: [sw.lng(), sw.lat(), ne.lng(), ne.lat()], zoom }) 185 | } 186 | ``` 187 | 188 | ## 👥 Contributing 189 | 190 | To run the project locally, clone the repo and run: 191 | 192 | ```bash 193 | # in the root directory 194 | yarn --frozen-install 195 | yarn dev 196 | ``` 197 | 198 | ```bash 199 | # in another tab (with NextJS and SSR) 200 | cd docs 201 | yarn install 202 | yarn dev 203 | ``` 204 | 205 | Do your changes to `src/` or `docs/src` directory, commits all files in the root directory (`yarn.lock` and ones in `dist` too) and open a PR. 206 | 207 | ## 💻 Built with 208 | 209 | - [React](https://reactjs.org/) 210 | - [TypeScript](https://www.typescriptlang.org/) 211 | - [Google Maps Custom Overlays](https://developers.google.com/maps/documentation/javascript/customoverlays) 212 | - [tsup](https://github.com/egoist/tsup): for bundling 213 | - [ESLint](https://eslint.org/): for linting 214 | - [Prettier](https://prettier.io/): for code formatting 215 | 216 | ## 🗒 License 217 | 218 | MIT © [giorgiabosello](https://github.com/giorgiabosello) 219 | 220 | ## 🙏 Support 221 | 222 | [![Buy me a Coffee](https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/giorgiabosello) 223 | [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/giorgiabosello) 224 | 225 |
226 |

227 | Developed with ❤️ in Italy 🇮🇹 228 |

229 | --------------------------------------------------------------------------------