From f97c4a8de64419e984b6a50c9c3bf1bb4a0ec738 Mon Sep 17 00:00:00 2001 From: Sergej Schwabauer Date: Fri, 4 Mar 2022 14:22:49 +0100 Subject: [PATCH] Search Box Implemtation --- .ash_history | 2 + src/components/autocomplete/Autocomplete.tsx | 4 +- src/webparts/map/MapWebPart.ts | 8 +- src/webparts/map/components/Map.tsx | 42 ++++++- .../plugin/SearchPlugin.module.scss | 61 ++++++++++ .../map/components/plugin/SearchPlugin.tsx | 106 ++++++++++++++++++ 6 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 src/webparts/map/components/plugin/SearchPlugin.module.scss create mode 100644 src/webparts/map/components/plugin/SearchPlugin.tsx diff --git a/.ash_history b/.ash_history index b2fa28548..265e46ecf 100644 --- a/.ash_history +++ b/.ash_history @@ -23,3 +23,5 @@ gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship; npm run serve exit npm run serve +gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship; +npm run serve diff --git a/src/components/autocomplete/Autocomplete.tsx b/src/components/autocomplete/Autocomplete.tsx index 1f49d8331..b5c68273b 100644 --- a/src/components/autocomplete/Autocomplete.tsx +++ b/src/components/autocomplete/Autocomplete.tsx @@ -8,7 +8,7 @@ export interface IAutocompleteProps extends Omit; @@ -128,7 +128,7 @@ export class Autocomplete extends React.Component - {isFunction(this.props.onRenderSuggestions) && this.props.onRenderSuggestions()} + {isFunction(this.props.onRenderSuggestions) && this.props.onRenderSuggestions(this.state.currentValue)} ); } diff --git a/src/webparts/map/MapWebPart.ts b/src/webparts/map/MapWebPart.ts index 72d85bbc5..f9e222ccf 100644 --- a/src/webparts/map/MapWebPart.ts +++ b/src/webparts/map/MapWebPart.ts @@ -99,7 +99,7 @@ export default class MapWebPart extends BaseClientSideWebPart protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue); - const reloadIfOneOfProps = ["height", "tileLayerUrl", "minZoom", "maxZoom", "tileLayerAttribution", "plugins.zoomControl"] + const reloadIfOneOfProps = ["height", "tileLayerUrl", "minZoom", "maxZoom", "tileLayerAttribution", "plugins.zoomControl"]; if(reloadIfOneOfProps.Contains(p => p.Equals(propertyPath))) { this.reload(); @@ -214,9 +214,9 @@ export default class MapWebPart extends BaseClientSideWebPart isCollapsed: true, groupName: strings.WebPartPropertyGroupPlugins, groupFields: [ - // PropertyPaneToggle('plugins.searchBox', { - // label: "searchBox" - // }), + PropertyPaneToggle('plugins.searchBox', { + label: strings.WebPartPropertyPluginSearchboxLabel + }), PropertyPaneToggle('plugins.markercluster', { label: strings.WebPartPropertyPluginMarkerClusterLabel, }), diff --git a/src/webparts/map/components/Map.tsx b/src/webparts/map/components/Map.tsx index 6fa6d0af7..12e7e32f8 100644 --- a/src/webparts/map/components/Map.tsx +++ b/src/webparts/map/components/Map.tsx @@ -19,6 +19,7 @@ import { isFunction } from 'lodash'; import { MarkerIcon } from './MarkerIcon'; import MarkerClusterGroup from 'react-leaflet-markercluster'; import * as strings from 'MapWebPartStrings'; +import SearchPlugin from './plugin/SearchPlugin'; interface IMapState { markerItems: IMarker[]; @@ -128,9 +129,6 @@ export default class Map extends React.Component { }); - map.on("zoom", (ev: any) => { - console.log("SSC", this.props.maxZoom, ev); - }) this.map = map; } } @@ -151,6 +149,8 @@ export default class Map extends React.Component { {!this.props.plugins.markercluster && this.renderMarker() } + + {this.renderSearchBox()} {this.renderLegend()} @@ -341,6 +341,42 @@ export default class Map extends React.Component { ); } + private renderSearchBox(): JSX.Element { + + if(!this.props.plugins.searchBox) { + return (<>); + } + + return ( + { + this.map.setView([lat, lon], this.props.maxZoom > 18 ? 18 : this.props.maxZoom); + + const defaultRadius = 12; + const circleOptions = { + inner: { + color: '#136AEC', + fillColor: '#2A93EE', + fillOpacity: 1, + weight: 1.5, + opacity: 0.7, + radius: defaultRadius / 4 + }, + outer: { + color: "#136AEC", + fillColor: "#136AEC", + fillOpacity: 0.15, + opacity: 0.3, + weight: 1, + radius: defaultRadius + } + }; + + L.circle([lat, lon], circleOptions.outer).addTo(this.map); + L.circle([lat, lon], circleOptions.inner).addTo(this.map); + }} /> + ); + } + private showClickContent(): JSX.Element { if(!this.state.showClickContent || isNullOrEmpty(this.state.currentMarker)) { return (<>); diff --git a/src/webparts/map/components/plugin/SearchPlugin.module.scss b/src/webparts/map/components/plugin/SearchPlugin.module.scss new file mode 100644 index 000000000..bfde50bc9 --- /dev/null +++ b/src/webparts/map/components/plugin/SearchPlugin.module.scss @@ -0,0 +1,61 @@ +@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss'; + + +.map-plugin-search { + display: block; + top: 20px; + right: 20px; + z-index: 1000; + position: absolute; + + button { + outline: none; + border-radius: 2px; + height: 32px; + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + font-size: 1.4em; + background: #fff; + cursor: pointer; + } + + .textbox { + display: inline-block !important; + border-width: 0px; + padding: 0; + + div { + border-width: 0px; + } + } +} + +.suggesstion { + display: block; + + &-item { + display: flex; + padding: 5px 20px; + border-bottom: solid 1px $ms-color-themeLighter; + border-top: solid 1px $ms-color-themeLighter; + align-items: center; + cursor: pointer; + + &:first-child { + border-top-width: 0; + } + + &:last-child { + border-bottom-width: 0; + } + + i { + padding-right: 20px; + font-size: 2em; + } + + span { + font-size: 1em; + } + } +} \ No newline at end of file diff --git a/src/webparts/map/components/plugin/SearchPlugin.tsx b/src/webparts/map/components/plugin/SearchPlugin.tsx new file mode 100644 index 000000000..4b45b38c5 --- /dev/null +++ b/src/webparts/map/components/plugin/SearchPlugin.tsx @@ -0,0 +1,106 @@ +import { isFunction, isNullOrEmpty } from '@spfxappdev/utility'; +import { Autocomplete } from '@src/components/autocomplete/Autocomplete'; +import { Icon } from 'office-ui-fabric-react'; +import * as React from 'react'; +import styles from './SearchPlugin.module.scss'; + +export interface ISearchPluginProps { + nominatimUrl?: string; + resultLimit?: number; + onLocationSelected?(latitude: number, longitude: number): void; +} + +interface ISearchResult { + place_id: number; + display_name: string; + lat: string; + lon: string; +} + +interface ISearchPluginState { + searchResult: Array; + isSearchBoxVisible: boolean; +} + +export default class SearchPlugin extends React.Component { + + public state: ISearchPluginState = { + searchResult: [], + isSearchBoxVisible: false + }; + + public static defaultProps: ISearchPluginProps = { + nominatimUrl: "https://nominatim.openstreetmap.org/search", + resultLimit: 3 + }; + + + public render(): React.ReactElement { + return ( +
+ {this.state.isSearchBoxVisible && + { + return this.renderSuggesstionsFlyout(searchTerm); + }} + onChange={async (ev: any, searchTerm: string) => { + const result = await this.makeSearchRequest(searchTerm); + this.setState({ + searchResult: result + }); + }} /> + } + + +
); + } + + private renderSuggesstionsFlyout(searchTerm: string): JSX.Element { + + const results = this.state.searchResult; + + if(isNullOrEmpty(results)) { + return (<> + No results + ); + } + + return (
+ {results.map((location: ISearchResult, index: number): JSX.Element => { + return (
{ + + if(isFunction(this.props.onLocationSelected)) { + this.props.onLocationSelected(parseFloat(location.lat), parseFloat(location.lon)); + } + + this.setState({ + isSearchBoxVisible: false + }); + }} + className={styles["suggesstion-item"]}> + {location.display_name} +
); + })} +
); + + } + + private async makeSearchRequest(searchTerm: string): Promise { + + const response = await fetch(`${this.props.nominatimUrl}?format=json&limit=${this.props.resultLimit}&q=${searchTerm}`); + const responseJson = await response.json(); + console.log("SSC", responseJson); + return responseJson; + } +} \ No newline at end of file