Add ability to pass in an Item component

Create SplitMatch component

Create first (WIP) example
This commit is contained in:
Tom Southall
2022-02-23 21:41:55 +00:00
parent f9467a8ede
commit a1242eec5d
10 changed files with 235 additions and 26 deletions

View File

@@ -3,6 +3,7 @@ import Turnstone from '../../src/lib'
import styles from './styles/App.module.css'
import autocompleteStyles from './styles/autocomplete.module.css'
import defaultListbox from '../_shared/defaultListbox'
import Item from './components/item/item'
const maxItems = 10
const placeholder = 'Enter a city or airport'
@@ -33,29 +34,17 @@ const listbox = [
const App = () => {
const [selected, setSelected] = useState()
const onChange = useCallback(
(text) => {
//console.log('Changed to:', text)
}, []
)
const onSelect = sel => setSelected(sel)
// const onSelect = useCallback(
// (selectedResult) => {
// console.log('Selected Result:', selectedResult)
// }, []
// )
const onEnter = useCallback(
(query, selectedResult) => {
//console.log('Enter Pressed. Selected Result:', selectedResult, query)
console.log('Enter Pressed. Selected Result:', selectedResult, query)
}, []
)
const onTab = useCallback(
(query, selectedResult) => {
//console.log('Tab Pressed. Selected Result:', selectedResult, query)
console.log('Tab Pressed. Selected Result:', selectedResult, query)
}, []
)
@@ -71,12 +60,12 @@ const App = () => {
defaultListbox={defaultListbox}
defaultListboxIsImmutable={false}
id='autocomplete'
itemComponent={Item}
listbox={listbox}
listboxIsImmutable={true}
maxItems={maxItems}
minQueryLength={1}
noItemsMessage={noItemsMessage}
onChange={onChange}
onSelect={onSelect}
onEnter={onEnter}
onTab={onTab}

View File

@@ -0,0 +1,74 @@
import React from 'react'
import SplitMatch from '../splitMatch/splitMatch'
import imgNewYork from'../../images/newyork.jpg'
import styles from './item.module.css'
const SplitComponent = (props) => {
const {
children,
index
} = props
const className = `split${index}`
return <span className={styles[className]}>{children}</span>
}
const MatchComponent = (props) => {
const { children } = props
return <span className={styles.match}>{children}</span>
}
export default function Item(props) {
const {
appearsInDefaultListbox,
index,
item,
query,
searchType = 'startswith'
} = props
const globalMatch = searchType === 'contains'
const matchedText = (includeSeparator) => {
return (
<SplitMatch
searchText={query}
globalMatch={globalMatch}
globalSplit={false}
caseSensitiveMatch={false}
caseSensitiveSplit={false}
separator=", "
includeSeparator={includeSeparator}
MatchComponent={MatchComponent}
SplitComponent={SplitComponent}>{item.name}</SplitMatch>
)
}
const img = () => {
return item.name === 'New York City, New York, United States'
? <div><img src={imgNewYork} alt={item.name} /></div>
: void 0
}
const firstItem = () => {
return (
<div className={`${styles.container} ${styles.first}`}>
{img()}
<div>{matchedText(false)}</div>
</div>
)
}
const standardItem = () => {
return (
<div className={styles.container}>
{matchedText(true)}
</div>
)
}
return index === 0 && !appearsInDefaultListbox ? firstItem() : standardItem()
}

View File

@@ -0,0 +1,27 @@
.first > div {
display: inline-block;
height: 50px;
vertical-align: middle;
}
.first img {
display: block;
width: 50px;
height: 50px;
margin-right: 8px;
}
.first .split0 {
display: block;
margin-top: 4px;
font-size: 20px;
}
.container .split1 {
font-size: 15px;
color: #777;
}
.match {
font-weight: 600;
}

View File

@@ -0,0 +1,99 @@
import React from 'react'
import escapeStringRegExp from 'escape-string-regexp'
const getMatches = (str, searchString, global, caseSensitive) => {
const flags = (caseSensitive) ? 'g' : 'gi'
const regex = new RegExp(escapeStringRegExp(searchString), flags)
const matches = [...str.matchAll(regex)].map(a => [a.index, a.index + searchString.length])
return global
? matches
: (matches.length ? [matches[0]] : [])
}
const getDividers = (str, separator, global, caseSensitive) => {
const flags = (caseSensitive) ? 'g' : 'gi'
const regex = new RegExp(escapeStringRegExp(separator), flags)
const dividers = [...str.matchAll(regex)].map(a => a.index + separator.length)
return [...((global)
? dividers
: (dividers.length ? [dividers[0]] : [])
), str.length]
}
const isMatchingChar = (index, matches) => {
return matches.reduce((prev, match) => {
return prev || index >= match[0] && index < match[1]
}, false)
}
export default function SplitMatch(props) {
const {
caseSensitiveMatch = false,
caseSensitiveSplit = false,
globalMatch = true,
globalSplit = true,
includeSeparator = true,
searchText,
separator = ',',
children: text,
MatchComponent,
SplitComponent
} = props
if(!text) return null
const wrapMatch = (match, key) => {
if(MatchComponent) return <MatchComponent key={key}>{match}</MatchComponent>
return <strong key={key}>{match}</strong>
}
const wrapSplit = (children, key, index) => {
if(SplitComponent) return <SplitComponent key={key} index={index}>{children}</SplitComponent>
return <span key={key}>{children}</span>
}
let separatorRemoved = false
const dividers = getDividers(text, separator, globalSplit, caseSensitiveSplit)
const matches = getMatches(text, searchText, globalMatch, caseSensitiveMatch)
const parts = dividers.map((dividerIndex, i) => {
let tag = ''
let tagIsMatch = false
const parts = []
const prevIndex = dividers[i - 1] || 0
const chars = Array.from(text.substring(prevIndex, dividerIndex))
const addTag = (isMatch, finalTagInDivider) => {
const key = `part-${i}-${parts.length}`
if(tag.length && tagIsMatch !== isMatch) {
console.log({tag : `"${tag}"`})
if(!includeSeparator && finalTagInDivider && (globalSplit || !separatorRemoved)) {
tag = tag.replace(separator, '')
separatorRemoved = true
}
parts.push(
tagIsMatch
? wrapMatch(tag, key)
: <React.Fragment key={key}>{tag}</React.Fragment>
)
tag = ''
}
}
chars.forEach((char, index) => {
const isMatch = isMatchingChar(prevIndex + index, matches)
addTag(isMatch)
tagIsMatch = isMatch
tag = `${tag}${char}`
})
addTag(!tagIsMatch, true)
return wrapSplit(parts, `part-${i}`, i)
})
console.log({parts})
return parts
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -5,7 +5,7 @@
.query,
.typeahead {
box-sizing: border-box;
width: 700px;
width: 500px;
height: 2.5em;
line-height: normal;
font-size: 1.4em;
@@ -32,7 +32,7 @@
.listbox {
box-sizing: border-box;
width: 700px;
width: 500px;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
@@ -69,10 +69,12 @@
background-color: #f0f0f0;
}
/* Get rid */
.topItem {
font-size: 18px;
}
/* Get rid */
.split:not(:first-child) {
font-size: 15px;
}