├── .gitignore ├── .DS_Store ├── cover.png ├── logo.png ├── .babelrc ├── webpack.config.js ├── package.json ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .idea -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodHeK/qsearch/HEAD/.DS_Store -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodHeK/qsearch/HEAD/cover.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodHeK/qsearch/HEAD/logo.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "transform-react-jsx" 6 | ] 7 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'); 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: path.resolve(__dirname, 'build'), 7 | filename: 'index.js', 8 | libraryTarget: 'commonjs2' 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | include: path.resolve(__dirname, 'src'), 15 | exclude: /(node_modules|bower_components|build)/, 16 | use: { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: ['env'] 20 | } 21 | } 22 | } 23 | ] 24 | }, 25 | externals: { 26 | 'react': 'commonjs react' 27 | } 28 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qsearch", 3 | "version": "1.0.4", 4 | "description": "", 5 | "main": "build/index.js", 6 | "keywords": ["search", "trie", "suggestions", "filtering", "searching"], 7 | "peerDependencies": { 8 | "react": "^16.8.6" 9 | }, 10 | "scripts": { 11 | "start": "webpack --watch", 12 | "build": "webpack" 13 | }, 14 | "author": { 15 | "name": "codhek" 16 | }, 17 | "license": "ISC", 18 | "dependencies": { 19 | "react": "^16.8.6", 20 | "webpack": "^4.12.0" 21 | }, 22 | "devDependencies": { 23 | "babel-cli": "^6.26.0", 24 | "babel-core": "^6.26.3", 25 | "babel-loader": "^7.1.4", 26 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 27 | "babel-plugin-transform-react-jsx": "^6.24.1", 28 | "babel-preset-env": "^1.7.0", 29 | "webpack-cli": "^3.0.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](cover.png) 2 | 3 | ![voila](https://img.shields.io/badge/npm-v1.0.4-blue.svg) ![build](https://img.shields.io/badge/build-passing-green.svg) 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install --save qsearch 9 | ``` 10 | 11 | ### Demo 12 | 13 | Try out the demo [App](https://nifty-elion-04a803.netlify.com/). 14 | 15 | Link to the Demo [App repository](https://github.com/CodHeK/qsearch-demo-app). 16 | 17 | ### How To Use 18 | 19 | First import this component where you want to use it 20 | 21 | ``` 22 | import Search from 'qsearch'; 23 | ``` 24 | 25 | Then just render it as : 26 | 27 | ``` 28 | 29 | ``` 30 | 31 | ### Props 32 | 33 | ``` 34 | /* 35 | CONFIG PASSED AS PROPS: 36 | 37 | data: The data that needs to be searched upon. 38 | styles: Add custom styles to your search bar Component 39 | onEnter: Enable search on ENTER or on the fly! 40 | callback: mention a callback function to 41 | receive your search data 42 | 43 | */ 44 | ``` 45 | 46 | 47 | #### Example data : 48 | ``` 49 | [ 50 | { 51 | "id": 1, 52 | "name": "Leanne Graham", 53 | "username": "Bret", 54 | "email": "Sincere@april.biz", 55 | "address": { 56 | "street": "Kulas Light", 57 | "suite": "Apt. 556", 58 | "city": "Gwenborough", 59 | "zipcode": "92998-3874", 60 | "geo": { 61 | "lat": "-37.3159", 62 | "lng": "81.1496" 63 | } 64 | }, 65 | "phone": "1-770-736-8031 x56442", 66 | "website": "hildegard.org", 67 | "company": { 68 | "name": "Romaguera-Crona", 69 | "catchPhrase": "Multi-layered client-server neural-net", 70 | "bs": "harness real-time e-markets" 71 | } 72 | }, 73 | ] 74 | 75 | ``` 76 | 77 | #### Example use : 78 | 79 | ``` 80 | import React, { Component } from 'react'; 81 | import Search from 'qsearch'; 82 | 83 | class App extends Component { 84 | constructor(props) { 85 | super(props); 86 | this.state = { 87 | data: null, 88 | } 89 | } 90 | 91 | componentWillMount() { 92 | fetch('https://jsonplaceholder.typicode.com/users') 93 | .then(resp => resp.json()) 94 | .then(data => this.setState({ data, })); 95 | } 96 | 97 | /* Specify a callback to receive the 98 | filtered entries and suggested words back from the Component */ 99 | 100 | getSearchData = (data) => { 101 | const { filtered, suggested } = data; 102 | this.setState({ filtered, suggested }); 103 | }; 104 | 105 | render() { 106 | const { data } = this.state; 107 | 108 | const SearchBarStyles = { 109 | width: '300px', 110 | height: '50px', 111 | margin: '2%', 112 | borderRadius: '10px', 113 | paddingLeft: '5px' 114 | }; 115 | 116 | const config = { 117 | data: data, 118 | styles: SearchBarStyles, 119 | onEnter: true, 120 | callback: this.getSearchData 121 | }; 122 | 123 | return ( 124 | data && 125 | ); 126 | } 127 | } 128 | 129 | export default App; 130 | ``` 131 | ### Also ... 132 | 133 | If you find any bugs/edge-cases not taken care of :see_no_evil:, feel free to open an [issue](https://github.com/CodHeK/qsearch/issues). :smiley: -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Search = props => { 4 | let { data, styles, onEnter, callback } = props.config; 5 | 6 | let Trie = null; 7 | 8 | const check = (obj) => { 9 | for(const [_, v] of Object.entries(obj)) { 10 | if(JSON.stringify(v)[0] === '{') 11 | return false; 12 | } 13 | return true; 14 | }; 15 | 16 | const add = (v, index) => { 17 | let words = String(v).replace(/[^a-z0-9]/gi,' ').split(" "); 18 | for(const word of words) { 19 | if(word.length > 0) { 20 | insert(Trie, word.toLowerCase(), index); 21 | } 22 | } 23 | } 24 | 25 | const dfs = (obj, index) => { 26 | if(check(obj)) { 27 | for(const [_, v] of Object.entries(obj)) 28 | add(v, index); 29 | return; 30 | } 31 | 32 | for(const [_, v] of Object.entries(obj)) { 33 | if(JSON.stringify(v)[0] === '{') 34 | dfs(v, index); 35 | else 36 | add(v, index); 37 | } 38 | }; 39 | 40 | const genNode = () => { 41 | let temp = { 42 | 'isLeaf': false, 43 | 'map': new Map(), 44 | 'indexes': new Map() 45 | }; 46 | return temp; 47 | }; 48 | 49 | const insert = (root, str, index) => { 50 | if(root === null) root = genNode(); 51 | 52 | let temp = root; 53 | for(const x of str) { 54 | if(!temp.map.has(x)) 55 | temp.map.set(x, genNode()); 56 | 57 | temp.indexes.set(index, true); 58 | temp = temp.map.get(x); 59 | } 60 | temp.isLeaf = true; 61 | temp.indexes.set(index, true) 62 | Trie = root; 63 | }; 64 | 65 | const search = (root, str) => { 66 | if(root === null) return false; 67 | 68 | let temp = root; 69 | for(const x of str) { 70 | temp = temp.map.get(x); 71 | 72 | if(!temp) return []; 73 | } 74 | 75 | return temp.indexes; 76 | }; 77 | 78 | let suggested = []; 79 | 80 | const traverse = (root, str) => { 81 | // console.log(root, str, "81"); 82 | if(root.isLeaf) { 83 | suggested.push(str); 84 | return; 85 | } 86 | for(const [k, v] of root.map) { 87 | traverse(v, str+String(k)); 88 | } 89 | }; 90 | 91 | const suggestions = (root, str) => { 92 | if(root === null) return false; 93 | 94 | let temp = root; 95 | for(let i = 0; i < str.length; i++) { 96 | temp = temp.map.get(str[i]); 97 | 98 | if(!temp) return []; 99 | } 100 | 101 | console.log(temp); 102 | if(!temp.isLeaf) { 103 | for(const [k, v] of temp.map) { 104 | traverse(v, str+String(k)); 105 | } 106 | } 107 | }; 108 | 109 | for(let i = 0; i < data.length; i++) { 110 | dfs(data[i], i); 111 | }; 112 | 113 | const filteredData = (e, onEnter) => { 114 | let searchedVal = search(Trie, e.target.value.toLowerCase()); 115 | let filtered = []; 116 | suggested = []; 117 | for (const [idx, _] of searchedVal) 118 | filtered.push(data[idx]); 119 | 120 | if (!onEnter) 121 | suggestions(Trie, e.target.value.toLowerCase(), suggested); 122 | 123 | //console.log(filtered, suggested, "120") 124 | return { 125 | filtered, 126 | suggested, 127 | }; 128 | }; 129 | 130 | const inputSearch = (e) => { 131 | if(e.target.value !== "") 132 | callback(filteredData(e, false)); 133 | else 134 | callback({ filtered: [], suggested: [] }); 135 | }; 136 | 137 | const inputSearchOnEnter = (e) => { 138 | if(e.which === 13) 139 | if(e.target.value !== "") 140 | callback(filteredData(e, true)); 141 | else 142 | callback({ filtered: [], suggested: [] }); 143 | 144 | }; 145 | 146 | let SearchBar; 147 | if(onEnter === false) 148 | SearchBar = inputSearch(e) } />; 150 | else 151 | SearchBar = inputSearchOnEnter(e) } />; 153 | 154 | return ( 155 |
156 | {SearchBar} 157 |
158 | ); 159 | }; 160 | 161 | export default Search; 162 | --------------------------------------------------------------------------------