From a1242eec5d85100995f9c045d7d94e024f93fc03 Mon Sep 17 00:00:00 2001 From: Tom Southall Date: Wed, 23 Feb 2022 21:41:55 +0000 Subject: [PATCH] Add ability to pass in an Item component Create SplitMatch component Create first (WIP) example --- examples/geo/App.jsx | 19 +--- examples/geo/components/item/item.jsx | 74 +++++++++++++ examples/geo/components/item/item.module.css | 27 +++++ .../geo/components/splitMatch/splitMatch.jsx | 99 ++++++++++++++++++ examples/geo/images/newyork.jpg | Bin 0 -> 2810 bytes examples/geo/styles/autocomplete.module.css | 6 +- src/lib/components/hooks/useData.js | 8 +- src/lib/components/item.jsx | 17 ++- src/lib/context/state.jsx | 2 +- src/lib/index.jsx | 9 +- 10 files changed, 235 insertions(+), 26 deletions(-) create mode 100644 examples/geo/components/item/item.jsx create mode 100644 examples/geo/components/item/item.module.css create mode 100644 examples/geo/components/splitMatch/splitMatch.jsx create mode 100644 examples/geo/images/newyork.jpg diff --git a/examples/geo/App.jsx b/examples/geo/App.jsx index 697fff9..b7bcc45 100644 --- a/examples/geo/App.jsx +++ b/examples/geo/App.jsx @@ -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} diff --git a/examples/geo/components/item/item.jsx b/examples/geo/components/item/item.jsx new file mode 100644 index 0000000..7678ae8 --- /dev/null +++ b/examples/geo/components/item/item.jsx @@ -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 {children} +} + +const MatchComponent = (props) => { + const { children } = props + + return {children} +} + +export default function Item(props) { + const { + appearsInDefaultListbox, + index, + item, + query, + searchType = 'startswith' + } = props + + const globalMatch = searchType === 'contains' + + const matchedText = (includeSeparator) => { + return ( + {item.name} + ) + } + + const img = () => { + return item.name === 'New York City, New York, United States' + ?
{item.name}
+ : void 0 + } + + const firstItem = () => { + return ( +
+ {img()} +
{matchedText(false)}
+
+ ) + } + + const standardItem = () => { + return ( +
+ {matchedText(true)} +
+ ) + } + + return index === 0 && !appearsInDefaultListbox ? firstItem() : standardItem() + +} diff --git a/examples/geo/components/item/item.module.css b/examples/geo/components/item/item.module.css new file mode 100644 index 0000000..3a47796 --- /dev/null +++ b/examples/geo/components/item/item.module.css @@ -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; +} \ No newline at end of file diff --git a/examples/geo/components/splitMatch/splitMatch.jsx b/examples/geo/components/splitMatch/splitMatch.jsx new file mode 100644 index 0000000..ce69a13 --- /dev/null +++ b/examples/geo/components/splitMatch/splitMatch.jsx @@ -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 {match} + return {match} + } + + const wrapSplit = (children, key, index) => { + if(SplitComponent) return {children} + return {children} + } + + 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) + : {tag} + ) + 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 +} \ No newline at end of file diff --git a/examples/geo/images/newyork.jpg b/examples/geo/images/newyork.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f51dd3e5c5a11598fc5a030947ae1eb6e69de38 GIT binary patch literal 2810 zcmb`F3pCW*9>@2*k-QV*ZD_`;gOWGJX&A4;8DZS&c)yKz3W*L^4)Q3c92v@EW@Pe6 zip$lJG8BoBN9oLHq;OP-WbWTRx7E38oxAS3Yu)c&d+*QsJ@@|azhGQ210)=XBqD&p zV89+4K=3P^K(e(x?&?k^lAI1h4glbC01{FkfXJx$Sa&-cRWEO<>bC8ReEs91zwn>z zP>t150IdKF>-~xUznO#s0^|Ilpx>Yw9}6`H02TnTOz`iVw9S6MbM7|BQrvB!m&2klI~70OYB?n#42Fo*zwKoyWc6z~UoAuz6e-7<1=yJci$u}YdMSOuJ-tgNc8DsB&6OIu4`MNeOk zpsz{LB5W6dVK5jGArWa25orQe7EAb_CFlZ@LckTcB4FwOE(t?O!UXrA3jjDAvHkyk z5fY8U02l%;1bO#M05}W*364Y|5!(?kI0A{11X5~fX#@2`z8E(dLz}`~xR~@tTlb90 zyF)LbEG!KCdr1ILP#O#k`E(?qe%mEs!vAUrg9C&lQc6wQ;1EjP&`rk1-8a3m@wa8c z1Q10)l_U|8z#25EwYnwVOJ(#))3I!`I)-oMMmomn@uW7duG}lJSHCgwnWOzPQ#??S z+pHxwJPgRNf80=5kZEngePFiU$r>7sOwQU<)7lX>Qi?3lIbeUxtK_q}4~NGmr`mj~ z8{d`9QOe)9e!9`Br0ty}NP557|LW?q3FHz+1uNQpmTO04;%gfxnc-F5;cB({rlpoW z-oKo&*W>V~-iW4@k_-@9Qn1+$&fe321c1Sf!ce zn+iaO&rr_*i_UJK410Jj-khu7x%1oa2886bbG_1N7U2&!nwa$@>m=f-AkEc&!$8Z2 z725IUxMukHKuy38`l7d0-w8ebT)lYdopJi@8=Y1gVT#H@Q2}Y;J_{l5(UvXz)@A03 z#({(F)Pd1lgY^0ctXaV`gaH?FcaEFA2)A=r&?q9po?u2Dh>YfbsRi?jAV4?4So6aA zC2CPQ^j0&+`4XR&C&`EPy|E_t_#m5h9w2X$7j9V2C)T;;u1jWy4u9_izgD*}x|r28 zUCJvQ>2X9@S}?fFY8Owa%5i83IEzi`>h|*^H*Yy+$3InEZFVCW>BwK~5s;Mksof z>4h~puW~KX(mEoTS3FRYmPL`vy;5^OCd1ThdH6_}kL51?iY?tGnef*|FZcJED1rf^ z^{wpk*&|P#r}`MCBe z5`Kv_IA81OJlsKD^$7^M*y45$d4=AWNpyKRvy8VW5Wc_fidCvS^(?Keo1m_7<(LsS zuZzFAb8l+&%?~R%@1y41HUjcn9z=Pc8;sxcBwN~4Hzh!{K*%C?cbLn@{S=#?yL+@g zOrM*WJZh9+X?ZxMc-YZXn=Rk!AjDbQ(W-cok0a4NPua|k<E5PXP8+KKj=P_2?L*xL% zKCeMll}Nk~n>3_!$YS5fQ&Kp^{yFFw>HSFPy`GBiRZB9($Pc}F>M4XTzm}6-Q6IxTZ^wGpR~WYlzQ>ZrP@_Hxe4<97y)>JU4Lvhyq6L-SS?0@ap^z-@;8SNK}Gh9rq#)ox-8Q(o6lRl`&dXK`2^Hwnz;ejH_Wg@2E4D?jqw&u&tyVSFB%1Sy zjQsfOta7GgZKk*9=`lOfn9A$pD#`CxCDi{RPqyH9Q3n0y`p;i1MlAZJvBj5|-z1%FxX0*ZkM$fZu$$_Mh^@*N zQDV!=$mz|f;FVM8k*4v}o$|T`V`IZ>$$i!8JIVs0$xBs#uRFq_(RkfxzQ5JS-!;#oxyyI_0ri9%!ezfP5k7jx31r2 z5d0IYA8Cm8#{IH!9HCfF&JC5WVz(S&vJNY-P8$>n3m;1?ip!%78#h}8%^sORIO1!h zsuKMAwJyG7R))?*lbJgz-o-46ofLrkrFGY$R78&Pit}1V y?22N_D6qAUBZIEGC7&WSQ;&I2i6JtIi@!9Q@@K-8N-`#Mr;i@)_>n^wJo^W-LO>+| literal 0 HcmV?d00001 diff --git a/examples/geo/styles/autocomplete.module.css b/examples/geo/styles/autocomplete.module.css index f304496..1a428c3 100644 --- a/examples/geo/styles/autocomplete.module.css +++ b/examples/geo/styles/autocomplete.module.css @@ -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; } diff --git a/src/lib/components/hooks/useData.js b/src/lib/components/hooks/useData.js index ce65bec..2836ba9 100644 --- a/src/lib/components/hooks/useData.js +++ b/src/lib/components/hooks/useData.js @@ -80,8 +80,9 @@ export const fetcher = (query, listbox, defaultListbox, minQueryLength, maxItems return [] else if (!defaultListbox && query.length < minQueryLength) return [] - const listboxProp = - defaultListbox && !query.length ? defaultListbox : listbox + const isDefaultListbox = (defaultListbox && !query.length) + + const listboxProp = isDefaultListbox ? defaultListbox : listbox const promises = listboxProp.map((group) => { if (typeof group.data === 'function') { @@ -100,7 +101,8 @@ export const fetcher = (query, listbox, defaultListbox, minQueryLength, maxItems text: itemText(item, listboxProp[groupIndex].displayField), groupIndex, groupName: listboxProp[groupIndex].name, - dataSearchType: listboxProp[groupIndex].dataSearchType + dataSearchType: listboxProp[groupIndex].dataSearchType, + defaultListbox: isDefaultListbox })) ] }, []) diff --git a/src/lib/components/item.jsx b/src/lib/components/item.jsx index de44d35..de8bc69 100644 --- a/src/lib/components/item.jsx +++ b/src/lib/components/item.jsx @@ -14,6 +14,7 @@ export default function Item(props) { } = useContext(StateContext) const { customStyles, highlighted, separator, query } = state + const CustomItem = state.props.itemComponent const startsWith = item.dataSearchType !== 'contains' @@ -48,11 +49,23 @@ export default function Item(props) { dispatch(setSelected(index)) } - const contents = splitText.map((part, index) => { + const matchedText = splitText.map((part, index) => { const match = startsWith ? (splitQuery[index] || '') : query return }) + const itemContents = (CustomItem) + ? + : matchedText + return (
- {contents} + {itemContents}
) } diff --git a/src/lib/context/state.jsx b/src/lib/context/state.jsx index 647b7d8..980011f 100644 --- a/src/lib/context/state.jsx +++ b/src/lib/context/state.jsx @@ -3,7 +3,7 @@ import reducer from '../reducers/reducer' import {setQuery} from '../actions/actions' import undef from '../utils/undef' -const StateContext = createContext() //TODO: Rename GlobalStateContext +const StateContext = createContext() const StateContextProvider = (props) => { const { separator, styles = {}, text = '', items = [] } = props diff --git a/src/lib/index.jsx b/src/lib/index.jsx index b2d8959..1b026e4 100644 --- a/src/lib/index.jsx +++ b/src/lib/index.jsx @@ -27,9 +27,11 @@ export default function Turnstone(props) { const newProps = {...propDefaults, ...props} return ( - - - + + + + + ) } @@ -71,6 +73,7 @@ Turnstone.propTypes = { disabled: PropTypes.bool, displayField: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), id: PropTypes.string, + itemComponent: PropTypes.elementType, listbox: listboxRules.isRequired, listboxIsImmutable: PropTypes.bool, minQueryLength: (props) => {