Merge pull request #2 from SPFxAppDev/feature/searchbox
Search Box Implemtation
This commit is contained in:
commit
574e807246
|
@ -23,3 +23,5 @@ gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship;
|
||||||
npm run serve
|
npm run serve
|
||||||
exit
|
exit
|
||||||
npm run serve
|
npm run serve
|
||||||
|
gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship;
|
||||||
|
npm run serve
|
||||||
|
|
|
@ -8,7 +8,7 @@ export interface IAutocompleteProps extends Omit<ITextFieldProps, "componentRef"
|
||||||
showSuggestionsOnFocus?: boolean;
|
showSuggestionsOnFocus?: boolean;
|
||||||
minValueLength?: number;
|
minValueLength?: number;
|
||||||
onLoadSuggestions?(newValue: string): void;
|
onLoadSuggestions?(newValue: string): void;
|
||||||
onRenderSuggestions?(): JSX.Element;
|
onRenderSuggestions?(inputValue: string): JSX.Element;
|
||||||
textFieldRef?(fluentUITextField: ITextField, autocompleteComponent: Autocomplete, htmlInput?: HTMLInputElement);
|
textFieldRef?(fluentUITextField: ITextField, autocompleteComponent: Autocomplete, htmlInput?: HTMLInputElement);
|
||||||
onUpdated?(newValue: string);
|
onUpdated?(newValue: string);
|
||||||
calloutProps?: Omit<ICalloutProps, "hidden" | "target" | "preventDismissOnScroll" | "directionalHint" | "directionalHintFixed" | "isBeakVisible">;
|
calloutProps?: Omit<ICalloutProps, "hidden" | "target" | "preventDismissOnScroll" | "directionalHint" | "directionalHintFixed" | "isBeakVisible">;
|
||||||
|
@ -128,7 +128,7 @@ export class Autocomplete extends React.Component<IAutocompleteProps, IAutocompl
|
||||||
}}
|
}}
|
||||||
preventDismissOnScroll={true}
|
preventDismissOnScroll={true}
|
||||||
directionalHint={DirectionalHint.bottomCenter}>
|
directionalHint={DirectionalHint.bottomCenter}>
|
||||||
{isFunction(this.props.onRenderSuggestions) && this.props.onRenderSuggestions()}
|
{isFunction(this.props.onRenderSuggestions) && this.props.onRenderSuggestions(this.state.currentValue)}
|
||||||
</Callout>
|
</Callout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps>
|
||||||
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
|
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
|
||||||
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
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))) {
|
if(reloadIfOneOfProps.Contains(p => p.Equals(propertyPath))) {
|
||||||
this.reload();
|
this.reload();
|
||||||
|
@ -214,9 +214,9 @@ export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps>
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
groupName: strings.WebPartPropertyGroupPlugins,
|
groupName: strings.WebPartPropertyGroupPlugins,
|
||||||
groupFields: [
|
groupFields: [
|
||||||
// PropertyPaneToggle('plugins.searchBox', {
|
PropertyPaneToggle('plugins.searchBox', {
|
||||||
// label: "searchBox"
|
label: strings.WebPartPropertyPluginSearchboxLabel
|
||||||
// }),
|
}),
|
||||||
PropertyPaneToggle('plugins.markercluster', {
|
PropertyPaneToggle('plugins.markercluster', {
|
||||||
label: strings.WebPartPropertyPluginMarkerClusterLabel,
|
label: strings.WebPartPropertyPluginMarkerClusterLabel,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { isFunction } from 'lodash';
|
||||||
import { MarkerIcon } from './MarkerIcon';
|
import { MarkerIcon } from './MarkerIcon';
|
||||||
import MarkerClusterGroup from 'react-leaflet-markercluster';
|
import MarkerClusterGroup from 'react-leaflet-markercluster';
|
||||||
import * as strings from 'MapWebPartStrings';
|
import * as strings from 'MapWebPartStrings';
|
||||||
|
import SearchPlugin from './plugin/SearchPlugin';
|
||||||
|
|
||||||
interface IMapState {
|
interface IMapState {
|
||||||
markerItems: IMarker[];
|
markerItems: IMarker[];
|
||||||
|
@ -128,9 +129,6 @@ export default class Map extends React.Component<IMapProps, IMapState> {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on("zoom", (ev: any) => {
|
|
||||||
console.log("SSC", this.props.maxZoom, ev);
|
|
||||||
})
|
|
||||||
this.map = map;
|
this.map = map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,6 +149,8 @@ export default class Map extends React.Component<IMapProps, IMapState> {
|
||||||
{!this.props.plugins.markercluster &&
|
{!this.props.plugins.markercluster &&
|
||||||
this.renderMarker()
|
this.renderMarker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{this.renderSearchBox()}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
{this.renderLegend()}
|
{this.renderLegend()}
|
||||||
|
@ -341,6 +341,42 @@ export default class Map extends React.Component<IMapProps, IMapState> {
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderSearchBox(): JSX.Element {
|
||||||
|
|
||||||
|
if(!this.props.plugins.searchBox) {
|
||||||
|
return (<></>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchPlugin onLocationSelected={(lat: number, lon: number) => {
|
||||||
|
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 {
|
private showClickContent(): JSX.Element {
|
||||||
if(!this.state.showClickContent || isNullOrEmpty(this.state.currentMarker)) {
|
if(!this.state.showClickContent || isNullOrEmpty(this.state.currentMarker)) {
|
||||||
return (<></>);
|
return (<></>);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ISearchResult>;
|
||||||
|
isSearchBoxVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SearchPlugin extends React.Component<ISearchPluginProps, ISearchPluginState> {
|
||||||
|
|
||||||
|
public state: ISearchPluginState = {
|
||||||
|
searchResult: [],
|
||||||
|
isSearchBoxVisible: false
|
||||||
|
};
|
||||||
|
|
||||||
|
public static defaultProps: ISearchPluginProps = {
|
||||||
|
nominatimUrl: "https://nominatim.openstreetmap.org/search",
|
||||||
|
resultLimit: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public render(): React.ReactElement<ISearchPluginProps> {
|
||||||
|
return (
|
||||||
|
<div className={styles["map-plugin-search"]}>
|
||||||
|
{this.state.isSearchBoxVisible &&
|
||||||
|
<Autocomplete
|
||||||
|
className={styles['textbox']}
|
||||||
|
onRenderSuggestions={(searchTerm: string) => {
|
||||||
|
return this.renderSuggesstionsFlyout(searchTerm);
|
||||||
|
}}
|
||||||
|
onChange={async (ev: any, searchTerm: string) => {
|
||||||
|
const result = await this.makeSearchRequest(searchTerm);
|
||||||
|
this.setState({
|
||||||
|
searchResult: result
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
}
|
||||||
|
|
||||||
|
<button type="button" onClick={() => {
|
||||||
|
const isVisible: boolean = this.state.isSearchBoxVisible ? false : true;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isSearchBoxVisible: isVisible
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<Icon iconName="Search" />
|
||||||
|
</button>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSuggesstionsFlyout(searchTerm: string): JSX.Element {
|
||||||
|
|
||||||
|
const results = this.state.searchResult;
|
||||||
|
|
||||||
|
if(isNullOrEmpty(results)) {
|
||||||
|
return (<>
|
||||||
|
No results
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<div className={styles["suggesstion"]}>
|
||||||
|
{results.map((location: ISearchResult, index: number): JSX.Element => {
|
||||||
|
return (<div
|
||||||
|
key={`Icon_${index}_${location.place_id}`}
|
||||||
|
onClick={() => {
|
||||||
|
|
||||||
|
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}
|
||||||
|
</div>);
|
||||||
|
})}
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeSearchRequest(searchTerm: string): Promise<ISearchResult[]> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue