-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add grouped search results panel
- Loading branch information
Showing
4 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters