Skip to content

Commit

Permalink
feat: add grouped search results panel
Browse files Browse the repository at this point in the history
  • Loading branch information
hwbllmnn committed Jul 1, 2022
1 parent 896d0ca commit de36077
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
63 changes: 63 additions & 0 deletions src/Panel/SearchResultsPanel/SearchResultsPanel.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.search-wrapper {
flex: 1;
display: flex;
flex-direction: column;

.ant-input {
background-color: white;
}

}

.search-result-div {
width: 50vw;
position: absolute;
top: 47px;
z-index: 9999;

.ant-collapse {
max-height: 400px;
overflow-y: auto;

.ant-list div.result-text {
padding: 0 10px;
}

.ant-collapse-header {
padding: 5px 0 5px 40px;

i.arrow {
line-height: 40px;
}

.search-result-panel-header {
display: flex;
align-items: center;

>span {
flex: 1;
}

.search-option-avatar {
max-width: 32px;
margin-right: 15px;
border-radius: 0;
background: transparent;
}
}
}

.ant-collapse-content {
background: transparent;
padding: 0;

>.ant-collapse-content-box {
padding: 0;

.ant-list-item:hover {
cursor: pointer;
}
}
}
}
}
149 changes: 149 additions & 0 deletions src/Panel/SearchResultsPanel/SearchResultsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useEffect, useState } from 'react';
import OlLayerVector from 'ol/layer/Vector';
import OlFeature from 'ol/Feature';
import OlSourceVector from 'ol/source/Vector';
import OlMap from 'ol/Map';

import {
Collapse,
List
} from 'antd';

import _isEmpty from 'lodash';
import './SearchResultsPanel.less';
import useMap from '../../Hook/useMap';
import BaseLayer from 'ol/layer/Base';

const Panel = Collapse.Panel;
const ListItem = List.Item;

interface SearchResultsPanelProps {
features: {
[title: string]: OlFeature[];
};
numTotal: number;
searchTerms: string[];
}

const SearchResultsPanel = (props: SearchResultsPanelProps) => {
const [highlightLayer, setHighlightLayer] = useState<OlLayerVector<OlSourceVector> | null>(null);
const map = useMap() as OlMap;
const {
features,
numTotal,
searchTerms
} = props;

useEffect(() => {
const layer = new OlLayerVector({
source: new OlSourceVector()
});
setHighlightLayer(layer);
map.addLayer(layer);
}, []);

useEffect(() => {
return () => {
map.removeLayer(highlightLayer as BaseLayer);
}
}, [highlightLayer]);

const highlightSearchTerms = (text: string) => {
searchTerms.forEach(searchTerm => {
const term = searchTerm.toLowerCase();
if (term === '') {
return;
}
let start = text.toLowerCase().indexOf(term);
while (start >= 0) {
const startPart = text.substring(0, start);
const matchedPart = text.substring(start, start + term.length);
const endPart = text.substring(start + term.length, text.length);
text = `${startPart}<b>${matchedPart}</b>${endPart}`;
start = text.toLowerCase().indexOf(term, start + 8);
}
});
return text;
};

const onMouseOver = (feature: OlFeature) => {
return () => {
highlightLayer?.getSource()?.clear();
highlightLayer?.getSource()?.addFeature(feature);
};
};

/**
* Renders content panel of related collapse element for each feature type and
* its features.
*
* @param title Title of the group
* @param list The list of features
*/
const renderPanelForFeatureType = (title: string, list: OlFeature[]) => {
if (!list || _isEmpty(list)) {
return;
}

const header = (
<div className="search-result-panel-header">
<span>{`${title} (${list.length})`}</span>
</div>
);

return (
<Panel
header={header}
key={title}
>
<List
size="small"
dataSource={list.map((feat, idx) => {
let text: string = highlightSearchTerms(feat.get('title'));
return {
text,
idx,
feature: feat
};
})}
renderItem={(item: any) => (
<ListItem
className="result-list-item"
key={item.idx}
onMouseOver={onMouseOver(item.feature)}
onMouseOut={() => highlightLayer?.getSource()?.clear()}
onClick={() => map.getView().fit(item.feature.getGeometry(), {
nearest: true
})}
>
<div
className="result-text"
dangerouslySetInnerHTML={{ __html: item.text }}
/>
</ListItem>
)}
/>
</Panel>
);
};

if (numTotal === 0) {
return null;
}

return (
<div className="search-result-div">
<Collapse
defaultActiveKey={Object.keys(features)[0]}
>
{
Object.keys(features).map((title: string) => {
return renderPanelForFeatureType(title, features[title]);
})
}
</Collapse>
</div>
);
};

export default SearchResultsPanel;
37 changes: 37 additions & 0 deletions src/Util/ClickAwayListener/ClickAwayListener.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { useEffect, useRef } from 'react';

interface ClickAwayProps {
onClickAway: VoidFunction;
children: React.ReactNode[] | React.ReactNode;
}

const ClickAwayListener = (props: ClickAwayProps) => {
const ref = useRef(null);

const handleClickAway = (e: Event) => {
if (ref.current && (ref.current as Element).contains(e.target as Node)) {
return;
}

props.onClickAway();
};

useEffect(() => {
window.addEventListener('click', handleClickAway);
}, []);

useEffect(() => {
return () => {
window.removeEventListener('click', handleClickAway);
}
}, []);

return (
<div ref={ref}>
{props.children}
</div>
);

};

export default ClickAwayListener;
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ import TimeSlider from './Slider/TimeSlider/TimeSlider';
import Toolbar from './Toolbar/Toolbar';
import UserChip from './UserChip/UserChip';
import Window from './Window/Window';
import SearchResultsPanel from './Panel/SearchResultsPanel/SearchResultsPanel';
import ClickAwayListener from './Util/ClickAwayListener/ClickAwayListener';

export {
AddWmsLayerEntry,
AddWmsPanel,
AgFeatureGrid,
CircleMenu,
ClickAwayListener,
CoordinateInfo,
CoordinateReferenceSystemCombo,
CopyButton,
Expand Down Expand Up @@ -83,6 +86,7 @@ export {
Panel,
PropertyGrid,
ScaleCombo,
SearchResultsPanel,
SelectFeaturesButton,
SimpleButton,
timeLayerAware,
Expand Down

0 comments on commit de36077

Please sign in to comment.