diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..6a9d9e29 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +14.18 diff --git a/package-lock.json b/package-lock.json index fde278fa..c863fe69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17780,11 +17780,6 @@ } } }, - "react-display-name": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz", - "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==" - }, "react-docgen": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.3.1.tgz", @@ -18272,16 +18267,6 @@ "resolved": "https://registry.npmjs.org/react-vk/-/react-vk-5.0.2.tgz", "integrity": "sha512-fjlznPH3o4lsOyKUzOXyWmfIELtd/CR6ZOeJsOhIH3TMI/T7QH8acdEWJH9Bz2Ivd9pa/MN9WMr00G9F25TDUg==" }, - "react-yandex-maps": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/react-yandex-maps/-/react-yandex-maps-4.6.0.tgz", - "integrity": "sha512-X4SC+SL4aByIaBOpis0FQYalqdXyvlj0D/LSznrYGUPadVsIuvHXP8GrFoN00oCOiiRwSQarOcVfN7V0XQxesA==", - "requires": { - "create-react-context": "^0.3.0", - "prop-types": "^15.7.2", - "react-display-name": "^0.2.5" - } - }, "react-youtube": { "version": "7.13.1", "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz", diff --git a/package.json b/package.json index 2acef8bb..bc2c16fc 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "react-redux": "^7.2.4", "react-text-loop": "^2.3.0", "react-vk": "^5.0.2", - "react-yandex-maps": "^4.6.0", "react-youtube": "^7.13.1", "redux": "^4.1.0", "redux-thunk": "^2.3.0", diff --git a/preview/docs/YandexMap.md b/preview/docs/YandexMap.md index 6fa1e778..cda4421e 100644 --- a/preview/docs/YandexMap.md +++ b/preview/docs/YandexMap.md @@ -27,10 +27,11 @@ To get the `API key`, follow the [instructions](https://yandex.ru/blog/mapsapi/n | Traffic | `Unchecked` | `Checked` | | Ruler | `Unchecked` | `Checked` | | Layers options | `Unchecked` | `Checked` | +| Localization | `ru_RU` | `en_US` | ### In the code (for developers) -| Prop name | Название в коде | Type | Default | Example | +| Prop name | Name in the code | Type | Default | Example | | :--------------- | :------------------: | :-------: | :----------: | :-------------------------------------------------------------------------------: | | API Key | `apikey` | `string` | `-` | [`Your API Key`](https://yandex.ru/blog/mapsapi/novye-pravila-dostupa-k-api-kart) | | Map scale | `zoomValue` | `string` | `9` | `5` | @@ -43,6 +44,7 @@ To get the `API key`, follow the [instructions](https://yandex.ru/blog/mapsapi/n | Traffic | `trafficControl` | `boolean` | `false` | `true` | | Ruler | `rulerControl` | `boolean` | `false` | `true` | | Layers options | `typeSelectorContol` | `boolean` | `false` | `true` | +| Localization | `lang` | `string` | `ru_RU` | `en_US` | ## 🗄 GitHub diff --git a/preview/docs/ru/YandexMap.md b/preview/docs/ru/YandexMap.md index c2b47a62..d166ebd7 100644 --- a/preview/docs/ru/YandexMap.md +++ b/preview/docs/ru/YandexMap.md @@ -27,6 +27,7 @@ | Пробки | `Не отмечен` | `Отмечен` | | Линейка | `Не отмечен` | `Отмечен` | | Варианты слоев | `Не отмечен` | `Отмечен` | +| Локализация | `ru_RU` | `en_US` | ### В коде (для разработчиков) @@ -43,6 +44,7 @@ | Пробки | `trafficControl` | `boolean` | `false` | `true` | | Линейка | `rulerControl` | `boolean` | `false` | `true` | | Варианты слоев | `typeSelectorContol` | `boolean` | `false` | `true` | +| Локализация | `lang` | `string` | `ru_RU` | `en_US` | ## 🗄 GitHub diff --git a/src/ScrollAnimationCustom/ScrollAnimationCustom.js b/src/ScrollAnimationCustom/ScrollAnimationCustom.js index 6cec73d2..5108c946 100644 --- a/src/ScrollAnimationCustom/ScrollAnimationCustom.js +++ b/src/ScrollAnimationCustom/ScrollAnimationCustom.js @@ -2,9 +2,8 @@ import React, { useMemo, useEffect, useRef } from 'react'; import { Box } from '@quarkly/widgets'; import { useOverrides } from '@quarkly/components'; import ComponentNotice from '../ComponentNotice'; -import { isEmptyChildren } from '../utils'; +import { isEmptyChildren, useScript } from '../utils'; import { propInfo, defaultProps, overrides } from './props'; -import useScript from './hooks/useScript'; import makeAnim from './utils/makeAnim'; const duration = 1000; // Totally arbitrary! diff --git a/src/YandexMap/Control.js b/src/YandexMap/Control.js new file mode 100644 index 00000000..3bf93c4c --- /dev/null +++ b/src/YandexMap/Control.js @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import { toggleControl } from './utils'; + +// hack to not break the react hooks rule +function Control({ map, control, enabled }) { + useEffect(() => { + toggleControl(map.current, control, enabled); + }, [map, control, enabled]); + + return null; +} + +export default Control; diff --git a/src/YandexMap/YandexMap.js b/src/YandexMap/YandexMap.js index a3a031f7..9cc5947b 100644 --- a/src/YandexMap/YandexMap.js +++ b/src/YandexMap/YandexMap.js @@ -1,20 +1,10 @@ -import React, { useRef } from 'react'; -import useResizeObserver from '@react-hook/resize-observer'; +import React, { useEffect, useRef } from 'react'; import { Box } from '@quarkly/widgets'; - -import { - YMaps, - Map, - ZoomControl, - RulerControl, - TrafficControl, - TypeSelector, - SearchControl, - GeolocationControl, - FullscreenControl, -} from 'react-yandex-maps'; -import { useDebounce } from '../utils'; +import { useDebounce, useScript } from '../utils'; import { propInfo, defaultProps } from './props'; +import withPropsTransformer from '../utils/withPropsTransformer'; +import { getInitialControls } from './utils'; +import Control from './Control'; const YandexMap = ({ apikey, @@ -24,59 +14,65 @@ const YandexMap = ({ longitudeCenter, trafficControl, rulerControl, - typeSelectorContol, + typeSelectorContol: typeSelector, searchControl, geolocationControl, fullscreenControl, + lang, ...props }) => { - const ymapRef = useRef({}); - const containerRef = useRef(null); + const map = useRef(null); + + const mapRef = useRef(null); const dapiKey = useDebounce(apikey, 2000); - const key = useDebounce( - `yandexmap${zoomValue}${latitudeCenter}${longitudeCenter}`, - 2000 - ); - useResizeObserver(containerRef, () => - ymapRef.current?.container?.fitToViewport() + const ns = `ymaps_${dapiKey}_${lang}`; + + const { ready } = useScript( + `https://api-maps.yandex.ru/2.1?apikey=${dapiKey}&lang=${lang}&ns=${ns}` ); + const controls = { + trafficControl, + rulerControl, + typeSelector, + searchControl, + geolocationControl, + fullscreenControl, + zoomControl, + }; + + useEffect(() => { + if (ready) { + window[ns].ready(() => { + const ymaps = window[ns]; + + if (!map.current) { + map.current = new ymaps.Map(mapRef.current, { + center: [latitudeCenter, longitudeCenter], + zoom: zoomValue, + controls: getInitialControls(controls), + }); + } + }); + } + + return () => { + map.current?.destroy(); + map.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ready, lang]); + + useEffect(() => { + map.current?.setCenter([latitudeCenter, longitudeCenter], zoomValue); + }, [latitudeCenter, longitudeCenter, zoomValue]); + return ( - - - - {fullscreenControl && } - {geolocationControl && } - {zoomControl && } - {trafficControl && } - {rulerControl && } - {typeSelectorContol && } - {searchControl && ( - - )} - - + + {Object.entries(controls).map(([key, value]) => ( + + ))} ); }; @@ -91,4 +87,4 @@ Object.assign(YandexMap, { defaultProps, }); -export default YandexMap; +export default withPropsTransformer(YandexMap); diff --git a/src/YandexMap/YandexMap.stories.js b/src/YandexMap/YandexMap.stories.js index 5e9a6bfd..12a0c05b 100644 --- a/src/YandexMap/YandexMap.stories.js +++ b/src/YandexMap/YandexMap.stories.js @@ -12,4 +12,16 @@ export default { export const StoryDefault = (props) => ; +export const StoryMultiple = (props) => ( + <> + + + + + + + + +); + StoryDefault.storyName = 'Default'; diff --git a/src/YandexMap/props/propsDefault.js b/src/YandexMap/props/propsDefault.js index 11746724..20e15b40 100644 --- a/src/YandexMap/props/propsDefault.js +++ b/src/YandexMap/props/propsDefault.js @@ -2,4 +2,15 @@ export default { latitudeCenter: 40.714599, longitudeCenter: -74.002791, zoomValue: 9, + searchControl: false, + fullscreenControl: false, + geolocationControl: false, + zoomControl: false, + trafficControl: false, + rulerControl: false, + typeSelectorContol: false, + lang: 'ru_RU', + // Style + height: '250px', + display: 'block', }; diff --git a/src/YandexMap/props/propsInfo.js b/src/YandexMap/props/propsInfo.js index df530c92..2f468f39 100644 --- a/src/YandexMap/props/propsInfo.js +++ b/src/YandexMap/props/propsInfo.js @@ -28,7 +28,7 @@ export default { ru: 'Широта', }, control: 'input', - type: 'text', + type: 'number', category: 'Center', weight: 0.5, }, @@ -38,7 +38,7 @@ export default { ru: 'Долгота', }, control: 'input', - type: 'text', + type: 'number', category: 'Center', weight: 0.5, }, @@ -105,4 +105,14 @@ export default { category: 'Controls', weight: 0.5, }, + lang: { + title: { + en: 'Localization', + ru: 'Локализация', + }, + control: 'input', + type: 'text', + variants: ['ru_RU', 'en_US', 'en_RU', 'ru_UA', 'uk_UA', 'tr_TR'], + weight: 0.5, + }, }; diff --git a/src/YandexMap/utils.js b/src/YandexMap/utils.js new file mode 100644 index 00000000..f803578b --- /dev/null +++ b/src/YandexMap/utils.js @@ -0,0 +1,23 @@ +export function getInitialControls(controlsObject) { + const controls = []; + + Object.entries(controlsObject).forEach(([key, value]) => { + if (value) controls.push(key); + }); + + return controls; +} + +export function toggleControls(map, controlsObject) { + Object.entries(controlsObject).forEach(([key, value]) => + toggleControl(map, key, value) + ); +} + +export function toggleControl(map, key, value) { + if (value) { + map?.controls?.add(key); + } else { + map?.controls?.remove(key); + } +} diff --git a/src/utils/index.js b/src/utils/index.js index 55606324..78c7905b 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -12,6 +12,7 @@ export { default as useDebounce } from './useDebounce'; export { default as useDebouncedCallback } from './useDebouncedCallback'; export { default as useUpdateEffect } from './useUpdateEffect'; export { default as useUniqueId } from './useUniqueId'; +export { default as useScript } from './useScript'; export { default as isString } from './isString'; diff --git a/src/ScrollAnimationCustom/hooks/useScript.js b/src/utils/useScript.js similarity index 85% rename from src/ScrollAnimationCustom/hooks/useScript.js rename to src/utils/useScript.js index 18f2bb01..3a582c94 100644 --- a/src/ScrollAnimationCustom/hooks/useScript.js +++ b/src/utils/useScript.js @@ -1,11 +1,13 @@ import { useState, useEffect } from 'react'; +const initialValue = { + ready: false, + error: false, +}; + export default function useScript(src) { - const [state, setState] = useState({ - ready: false, - loaded: false, - error: false, - }); + const [state, setState] = useState(initialValue); + const [prevSrc, setPrevSrc] = useState(src); useEffect( () => { @@ -31,7 +33,6 @@ export default function useScript(src) { const status = script.getAttribute('data-status'); setState({ - loading: status === 'loading', ready: status === 'ready', error: status === 'error', }); @@ -56,5 +57,12 @@ export default function useScript(src) { }, [src] // Only re-run effect if script src changes ); + + // Reset state immediately if src is changed + if (src !== prevSrc) { + setPrevSrc(src); + setState(initialValue); + } + return state; } diff --git a/src/utils/withPropsTransformer.js b/src/utils/withPropsTransformer.js new file mode 100644 index 00000000..8b245b70 --- /dev/null +++ b/src/utils/withPropsTransformer.js @@ -0,0 +1,46 @@ +import React from 'react'; +import getNumber from './getNumber'; + +const numberTransformer = (v, d) => getNumber(v, d); +const boolTransformer = (v) => { + return !!v; +}; + +const emptyTransformer = (v) => { + return v; +}; + +const getTransformer = (prop) => { + if (prop.type === 'number') { + return numberTransformer; + } + + if (prop.type === 'checkbox') { + return boolTransformer; + } + + return emptyTransformer; +}; + +const withPropsTransformer = (Component) => { + const { propInfo, defaultProps } = Component; + + function WrappedComponent(props) { + const newProps = {}; + + Object.keys(propInfo).forEach((p) => { + if (p in props) { + const transformer = getTransformer(propInfo[p]); + newProps[p] = transformer(props[p], defaultProps[p], { + propInfo: propInfo[p], + }); + } + }, []); + + return ; + } + + return Object.assign(WrappedComponent, Component); +}; + +export default withPropsTransformer;