diff --git a/app/client/package.json b/app/client/package.json index 06c4555eb6..77e17e12f6 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -142,6 +142,7 @@ "js-sha256": "^0.9.0", "jshint": "^2.13.4", "klona": "^2.0.5", + "leaflet": "^1.9.4", "libphonenumber-js": "^1.9.44", "linkedom": "^0.14.20", "localforage": "^1.7.3", @@ -185,6 +186,7 @@ "react-helmet": "^5.2.1", "react-hook-form": "^7.28.0", "react-json-view": "^1.21.3", + "react-leaflet": "3.2.5", "react-media-recorder": "^1.6.1", "react-modal": "^3.15.1", "react-page-visibility": "^7.0.0", diff --git a/app/client/src/widgets/OpenStreetMapWidget/component/index.tsx b/app/client/src/widgets/OpenStreetMapWidget/component/index.tsx new file mode 100644 index 0000000000..560e714f03 --- /dev/null +++ b/app/client/src/widgets/OpenStreetMapWidget/component/index.tsx @@ -0,0 +1,158 @@ +import React, { useEffect } from "react"; +import type { WidgetProps } from "widgets/BaseWidget"; +import styled from "styled-components"; + +import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"; +import "leaflet/dist/leaflet.css"; +import L from "leaflet"; +import { FullScreen, useFullScreenHandle } from "react-full-screen"; +import { Icon } from "@blueprintjs/core"; + +// Контейнер для карты с относительным позиционированием +const MapWrapper = styled.div` + position: relative; + width: 100%; + height: 100%; +`; + +// Кнопка для переключения полноэкранного режима +const FullscreenButton = styled.button` + position: absolute; + top: 10px; + right: 10px; + z-index: 1000; + background: white; + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: background-color 0.2s; + + &:hover { + background-color: #f0f0f0; + } + + &:active { + background-color: #e0e0e0; + } +`; + +// 🧩 фикс, чтобы маркеры отображались (иначе пустые иконки) +delete (L.Icon.Default.prototype as any)._getIconUrl; + +L.Icon.Default.mergeOptions({ + iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"), + iconUrl: require("leaflet/dist/images/marker-icon.png"), + shadowUrl: require("leaflet/dist/images/marker-shadow.png"), +}); + +// Компонент для обновления размера карты при изменении размера контейнера +function MapResizer() { + const map = useMap(); + + useEffect(() => { + // Обновляем размер карты при монтировании + const timer = setTimeout(() => { + map.invalidateSize(); + }, 100); + + return () => clearTimeout(timer); + }, [map]); + + // Обновляем размер при изменении размера окна + useEffect(() => { + const handleResize = () => { + map.invalidateSize(); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [map]); + + // Обновляем размер при переключении полноэкранного режима + useEffect(() => { + const handleFullscreenChange = () => { + // Небольшая задержка, чтобы контейнер успел изменить размер + setTimeout(() => { + map.invalidateSize(); + }, 200); + }; + + document.addEventListener("fullscreenchange", handleFullscreenChange); + document.addEventListener("webkitfullscreenchange", handleFullscreenChange); + document.addEventListener("mozfullscreenchange", handleFullscreenChange); + document.addEventListener("MSFullscreenChange", handleFullscreenChange); + + return () => { + document.removeEventListener("fullscreenchange", handleFullscreenChange); + document.removeEventListener("webkitfullscreenchange", handleFullscreenChange); + document.removeEventListener("mozfullscreenchange", handleFullscreenChange); + document.removeEventListener("MSFullscreenChange", handleFullscreenChange); + }; + }, [map]); + + return null; +} + +function OpenStreetMapComponent(props: OpenStreetMapComponentProps) { + // Получаем значения из props или используем значения по умолчанию + const centerLat = props.centerLat ?? 55.751244; + const centerLng = props.centerLng ?? 37.618423; + const zoom = props.zoom ?? 7; + const markerLat = props.markerLat ?? centerLat; + const markerLng = props.markerLng ?? centerLng; + const markerText = props.markerText ?? "Привет! Это Москва 🏙️"; + + const center: [number, number] = [centerLat, centerLng]; + const markerPosition: [number, number] = [markerLat, markerLng]; + + // Хук для управления полноэкранным режимом + const fullScreenHandle = useFullScreenHandle(); + + return ( + + + + + + + {markerText} + + + + + {/* Кнопка для переключения полноэкранного режима */} + + + + + ); +} + +export interface OpenStreetMapComponentProps extends WidgetProps { + centerLat?: number; + centerLng?: number; + zoom?: number; + markerLat?: number; + markerLng?: number; + markerText?: string; +} + +export default OpenStreetMapComponent; diff --git a/app/client/src/widgets/OpenStreetMapWidget/constants.ts b/app/client/src/widgets/OpenStreetMapWidget/constants.ts new file mode 100644 index 0000000000..6dec3a3f08 --- /dev/null +++ b/app/client/src/widgets/OpenStreetMapWidget/constants.ts @@ -0,0 +1,9 @@ +// This file contains common constants which can be used across the widget configuration file (index.ts), widget and component folders. +export const OPENSTREETMAP_WIDGET_CONSTANT = ""; + +export enum OverflowTypes { + SCROLL = "SCROLL", + TRUNCATE = "TRUNCATE", + NONE = "NONE", + } + \ No newline at end of file diff --git a/app/client/src/widgets/OpenStreetMapWidget/icon.svg b/app/client/src/widgets/OpenStreetMapWidget/icon.svg new file mode 100644 index 0000000000..19a4df7cd7 --- /dev/null +++ b/app/client/src/widgets/OpenStreetMapWidget/icon.svg @@ -0,0 +1,2 @@ + +OpenStreetMap icon \ No newline at end of file diff --git a/app/client/src/widgets/OpenStreetMapWidget/index.ts b/app/client/src/widgets/OpenStreetMapWidget/index.ts new file mode 100644 index 0000000000..b668e51d06 --- /dev/null +++ b/app/client/src/widgets/OpenStreetMapWidget/index.ts @@ -0,0 +1,3 @@ +import Widget from "./widget"; + +export default Widget; diff --git a/app/client/src/widgets/OpenStreetMapWidget/widget/index.tsx b/app/client/src/widgets/OpenStreetMapWidget/widget/index.tsx new file mode 100644 index 0000000000..dc0905a6ba --- /dev/null +++ b/app/client/src/widgets/OpenStreetMapWidget/widget/index.tsx @@ -0,0 +1,203 @@ +import React from "react"; + +import type { DerivedPropertiesMap } from "WidgetProvider/factory/types"; + +import BaseWidget from "widgets/BaseWidget"; +import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; + +import OpenStreetMapComponent from "../component"; +import { ValidationTypes } from "constants/WidgetValidation"; + +import IconSVG from "../icon.svg"; +import { WIDGET_TAGS } from "constants/WidgetConstants"; + +class OpenStreetMapWidget extends BaseWidget { + static type = "OPENSTREETMAP_WIDGET"; + + //Метаданные + static getConfig() { + return { + name: "OpenStreetMap", + iconSVG: IconSVG, + needsMeta: false, + isCanvas: false, + tags: [WIDGET_TAGS.CONTENT], + searchTags: ["map", "openstreet", "open"], + }; + } + + static getFeatures() { + return { + dynamicHeight: { + sectionIndex: 0, + active: false, + }, + }; + } + //Значения по умолчанию + static getDefaults() { + return { + widgetName: "OpenStreetMap", + rows: 10, + columns: 10, + version: 1, + // Значения по умолчанию для карты + centerLat: 55.751244, + centerLng: 37.618423, + zoom: 7, + markerLat: 55.751244, + markerLng: 37.618423, + markerText: "Привет! Это Москва 🏙️", + }; + } + + static getPropertyPaneContentConfig() { + return [ + { + sectionName: "General", + children: [ + { + propertyName: "centerLat", + helpText: "Широта центра карты", + label: "Широта центра", + controlType: "INPUT_TEXT", + placeholderText: "55.751244", + defaultValue: 55.751244, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.NUMBER, + params: { + min: -90, + max: 90, + }, + }, + }, + { + propertyName: "centerLng", + helpText: "Долгота центра карты", + label: "Долгота центра", + controlType: "INPUT_TEXT", + placeholderText: "37.618423", + defaultValue: 37.618423, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.NUMBER, + params: { + min: -180, + max: 180, + }, + }, + }, + { + propertyName: "zoom", + helpText: "Уровень масштабирования карты (1-18)", + label: "Масштаб", + controlType: "INPUT_TEXT", + placeholderText: "7", + defaultValue: 7, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.NUMBER, + params: { + min: 1, + max: 18, + }, + }, + }, + { + propertyName: "markerLat", + helpText: "Широта маркера", + label: "Широта маркера", + controlType: "INPUT_TEXT", + placeholderText: "55.751244", + defaultValue: 55.751244, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.NUMBER, + params: { + min: -90, + max: 90, + }, + }, + }, + { + propertyName: "markerLng", + helpText: "Долгота маркера", + label: "Долгота маркера", + controlType: "INPUT_TEXT", + placeholderText: "37.618423", + defaultValue: 37.618423, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.NUMBER, + params: { + min: -180, + max: 180, + }, + }, + }, + { + propertyName: "markerText", + helpText: "Текст во всплывающем окне маркера", + label: "Текст маркера", + controlType: "INPUT_TEXT", + placeholderText: "Привет! Это Москва 🏙️", + defaultValue: "Привет! Это Москва 🏙️", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.TEXT, + }, + }, + { + propertyName: "isVisible", + helpText: "Управляет видимостью виджета", + label: "Видимый", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + ], + }, + ]; + } + + static getPropertyPaneStyleConfig() { + return []; + } + + static getDerivedPropertiesMap(): DerivedPropertiesMap { + return {}; + } + + static getDefaultPropertiesMap(): Record { + return {}; + } + + static getMetaPropertiesMap(): Record { + return {}; + } + + //Рендерит компонент + getWidgetView() { + return ; + } +} + +export interface OpenStreetMapWidgetProps extends WidgetProps { + centerLat?: number; + centerLng?: number; + zoom?: number; + markerLat?: number; + markerLng?: number; + markerText?: string; +} + +export default OpenStreetMapWidget; \ No newline at end of file diff --git a/app/client/src/widgets/index.ts b/app/client/src/widgets/index.ts index fc83e4e27a..274d75af0f 100644 --- a/app/client/src/widgets/index.ts +++ b/app/client/src/widgets/index.ts @@ -368,6 +368,10 @@ const WidgetLoaders = new Map Promise>([ "EXTERNAL_WIDGET", async () => import("./ExternalWidget").then((m) => m.default), ], + [ + "OPENSTREETMAP_WIDGET", + async () => import("./OpenStreetMapWidget").then((m) => m.default), + ], // Deprecated Widgets [ diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 04dcebce9e..d79cee12ac 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -7436,6 +7436,17 @@ __metadata: languageName: node linkType: hard +"@react-leaflet/core@npm:^1.1.1": + version: 1.1.1 + resolution: "@react-leaflet/core@npm:1.1.1" + peerDependencies: + leaflet: ^1.7.1 + react: ^17.0.1 + react-dom: ^17.0.1 + checksum: 2fc4a80e5524f9437ac6cef0f95e63388f2df6ecc5107fef85fd097eb2455436e796d41a4c43cdcbb983a4132403646823ba1dc0d953e866eb80808cbfaf0232 + languageName: node + linkType: hard + "@react-spectrum/utils@npm:^3.11.10, @react-spectrum/utils@npm:^3.9.0": version: 3.11.10 resolution: "@react-spectrum/utils@npm:3.11.10" @@ -13837,6 +13848,7 @@ __metadata: json5: ^2.2.3 klona: ^2.0.5 knip: ^5.30.2 + leaflet: ^1.9.4 libphonenumber-js: ^1.9.44 linkedom: ^0.14.20 lint-staged: ^14.0.1 @@ -13900,6 +13912,7 @@ __metadata: react-hook-form: ^7.28.0 react-is: ^16.12.0 react-json-view: ^1.21.3 + react-leaflet: 3.2.5 react-media-recorder: ^1.6.1 react-modal: ^3.15.1 react-page-visibility: ^7.0.0 @@ -24138,6 +24151,13 @@ __metadata: languageName: node linkType: hard +"leaflet@npm:^1.9.4": + version: 1.9.4 + resolution: "leaflet@npm:1.9.4" + checksum: bfc79f17a247b37b92d84b3c78702501603392d6589fde606de4a825d11f1609d90225388834f2e0709dac327e52dcd4b4b9cc9fd3d590060c5b1e53b84fa6c6 + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -29956,6 +29976,19 @@ __metadata: languageName: node linkType: hard +"react-leaflet@npm:3.2.5": + version: 3.2.5 + resolution: "react-leaflet@npm:3.2.5" + dependencies: + "@react-leaflet/core": ^1.1.1 + peerDependencies: + leaflet: ^1.7.1 + react: ^17.0.1 + react-dom: ^17.0.1 + checksum: 503b7cee8acc12e0e2c5e7675432e7ef5742463e3e5420282ce60a9efd306430caefae3cb282c976e3df7665f19aef5b49cdce44a034979b4ea3ee968c2621d2 + languageName: node + linkType: hard + "react-lifecycles-compat@npm:^3.0.0, react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4"