Multi word twitter-typeahead.
This example adds multi-word ability to typeahead. The dropdown also follows the caret position thanks to Caret.js. Unfortunately, hinting and highlighting do not work, but I am out of time to play with it.
This example adds multi-word ability to typeahead. The dropdown also follows the caret position thanks to Caret.js. Unfortunately, hinting and highlighting do not work, but I am out of time to play with it.
| <!DOCTYPE html> | |
| <meta charset="utf-8"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.min.css" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-tokenfield/0.12.0/css/bootstrap-tokenfield.min.css" rel="stylesheet"> | |
| <style> | |
| body | |
| { | |
| min-width: 800px; | |
| padding: 2em; | |
| } | |
| .typeahead, | |
| .tt-query, | |
| .tt-hint { | |
| width: 396px; | |
| height: 30px; | |
| padding: 8px 12px; | |
| font-size: 24px; | |
| line-height: 30px; | |
| border: 2px solid #ccc; | |
| -webkit-border-radius: 8px; | |
| -moz-border-radius: 8px; | |
| border-radius: 8px; | |
| outline: none; | |
| height: 2em; | |
| } | |
| .typeahead { | |
| background-color: #fff; | |
| } | |
| .typeahead:focus { | |
| border: 2px solid #0097cf; | |
| } | |
| .tt-query { | |
| -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); | |
| -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); | |
| box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); | |
| } | |
| .tt-hint { | |
| color: #999 | |
| } | |
| .tt-menu { | |
| /*width: 422px;*/ | |
| width: auto; | |
| margin: 12px 0; | |
| padding: 8px 0; | |
| background-color: #fff; | |
| border: 1px solid #ccc; | |
| border: 1px solid rgba(0, 0, 0, 0.2); | |
| -webkit-border-radius: 8px; | |
| -moz-border-radius: 8px; | |
| border-radius: 8px; | |
| -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); | |
| -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); | |
| box-shadow: 0 5px 10px rgba(0,0,0,.2); | |
| } | |
| .tt-suggestion { | |
| padding: 3px 20px; | |
| font-size: 18px; | |
| line-height: 24px; | |
| } | |
| .tt-suggestion:hover { | |
| cursor: pointer; | |
| color: #fff; | |
| background-color: #0097cf; | |
| } | |
| .tt-suggestion.tt-cursor { | |
| color: #fff; | |
| background-color: #0097cf; | |
| } | |
| .tt-suggestion p { | |
| margin: 0; | |
| } | |
| .gist { | |
| font-size: 14px; | |
| } | |
| .twitter-typeahead, .tt-hint, .tt-input | |
| { | |
| width: 100%; | |
| } | |
| .tt-menu | |
| { | |
| width: auto; | |
| } | |
| .outerbox | |
| { | |
| border: 2px dashed #ccc; | |
| padding: 1em; | |
| margin: 1em; | |
| } | |
| #textarea | |
| { | |
| height: 300px; | |
| } | |
| </style> | |
| <body> | |
| <div class="outerbox"> | |
| <input id="textbox" class="typeahead" type="text" placeholder="Enter dinosaur names"/> | |
| </div> | |
| <div class="outerbox"> | |
| <textarea id="textarea" class="typeahead" placeholder="Type @ to trigger dinosaur name lookup"></textarea> | |
| </div> | |
| </body> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/js/bootstrap.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.11.1/typeahead.bundle.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Caret.js/0.3.1/jquery.caret.min.js"></script> | |
| <script> | |
| var dinos = [ | |
| "aardonyx", "allosaurus", "anchiceratops", "ankylosaurus", "apatosaurus", | |
| "arrhinoceratops", "atlascopcosaurus", "avalonia", "azendohsaurus", | |
| "bactrosaurus", "bagaceratops", "bambiraptor", "baryonyx", "becklespinax", | |
| "bellusaurus", "brachiosaurus", "brachyceratops", "buitreraptor", | |
| "camarasaurus", "carnotaurus", "cedarpelta", "centrosaurus", "coelophysis", | |
| "compsognathus", "conchoraptor", "confuciusornis", "corythosaurus", | |
| "deinonychus", "diplodocus", "edmontosaurus", "euoplocephalus", "fukuiraptor", | |
| "fukuisaurus", "gallimimus", "gigantosaurus", "giraffatitan", "hypsilophodon", | |
| "iguanodon", "irritator", "ichthyosaurus", "janenschia", "jaxartosaurus", | |
| "jinzhousaurus", "jobaria", "juravenator", "kentrosaurus", "khaan", | |
| "kotasaurus", "kritosaurus", "lamaceratops", "lambeosaurus", "leaellynasaura", | |
| "lesothosaurus", "lexovisaurus", "liaoxiornis", "lycorhinus", "macrurosaurus", | |
| "maiasaura", "mamenchisaurus", "megalosaurus", "minmi", "nanotyrannus", | |
| "nipponosaurus", "notoceratops", "nqwebasaurus", "omeisaurus", "ornitholestes", | |
| "omithomimus", "ornithomimus", "orodromeus", "oryctodromeus", "othnielia", | |
| "ouranosaurus", "oviraptor", "pachycephalosaurus", "panoplosaurus", | |
| "parasaurolophus", "pentaceratops", "plateosaurus", "plesiosaurus", | |
| "protoceratops", "psittacosaurus", "quaesitosaurus", "rebbachisaurus", | |
| "rhabdodon", "rinchenia", "riojasaurus", "rugops", "saurolophus", | |
| "saurophaganax", "seismosaurus", "spinosaurus", "stegosaurus", "stegoceras", | |
| "styracosaurus", "triceratops", "troodon", "tyrannosaurus", "tyrannotitan", | |
| "udanoceratops", "unenlagia", "utahraptor", "valdosaurus", "velociraptor", | |
| "vulcanodon", "wannanosaurus", "wuerhosaurus", "xiaosaurus", "yadusaurus", | |
| "yangchuanosaurus", "yimenosaurus", "yingshanosaurus", "yinlong", | |
| "yuanmousaurus", "yunnanosaurus", "zalmoxes", "zephyrosaurus", "zigongosaurus", | |
| "zuniceratops" | |
| ]; | |
| function MyTypeahead(id, data, opts) | |
| { | |
| var lookUp = data.reduce(function(p, c){ p[c]=1; return p}, {}); | |
| opts = opts ? opts : {}; | |
| opts.levenshteinDistance = (undefined !== opts.levenshteinDistance) ? opts.levenshteinDistance : 3; | |
| opts.validChars = (undefined !== opts.validChars) ? opts.validChars : /^[a-zA-Z]+$/; | |
| opts.tokenfield = (undefined !== opts.tokenfield) ? opts.tokenfield : false; | |
| opts.vertAdjustMenu = (undefined !== opts.vertAdjustMenu) ? opts.vertAdjustMenu : false; | |
| opts.trigger = (undefined !== opts.trigger) ? opts.trigger : ''; | |
| opts.delimiters = (undefined !== opts.delimiters) ? opts.delimiters : ',; \r\n'; | |
| function extractor(query) | |
| { | |
| var result = (new RegExp('([^,; \r\n' + opts.delimiters + ']+)$')).exec(query); | |
| if(result && result[1]) | |
| return result[1].trim(); | |
| return ''; | |
| } | |
| function charMatches(a, b) | |
| { | |
| var i; | |
| for (i = 0; i < a.length && i < b.length && a[i] == b[i]; i++) | |
| ; | |
| return i; | |
| } | |
| function levDist(a, b) | |
| { | |
| if(!a || !b) | |
| return (a || b).length; | |
| var m = []; | |
| for(var i = 0; i <= b.length; i++) | |
| { | |
| m[i] = [i]; | |
| if(!i) | |
| continue; | |
| for(var j = 0; j <= a.length; j++) | |
| { | |
| m[0][j] = j; | |
| if(!j) | |
| continue; | |
| m[i][j] = (b.charAt(i - 1) == a.charAt(j - 1)) | |
| ? m[i - 1][j - 1] | |
| : Math.min( m[i - 1][j - 1] + 1, m[i][j - 1] + 1, m[i - 1][j] + 1 ); | |
| } | |
| } | |
| return m[b.length][a.length]; | |
| }; | |
| var lastUpper = false; | |
| function strMatcher(id, strs) | |
| { | |
| return function findMatches(q, sync, async) | |
| { | |
| var pos = $(id).caret('pos'); | |
| q = (0 < pos) ? extractor(q.substring(0, pos)) : ''; | |
| if (!q.length) | |
| return; | |
| if (opts.trigger.length) | |
| { | |
| if(opts.trigger != q.substr(0, opts.trigger.length)) | |
| return; | |
| q = q.substr(opts.trigger.length); | |
| } | |
| if (!q.length || lookUp[q]) | |
| return; | |
| if (opts.validChars && opts.validChars instanceof RegExp) | |
| if (!q.match(opts.validChars)) | |
| return; | |
| var firstChar = q.substr(0, 1); | |
| lastUpper = (firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase()); | |
| var cpos = $(id).caret('position'); | |
| $(id).parent().find('.tt-menu').css('left', cpos.left + 'px'); | |
| if (opts.vertAdjustMenu) | |
| $(id).parent().find('.tt-menu').css('top', (cpos.top + cpos.height) + 'px'); | |
| var matches = []; | |
| if (opts.levenshteinDistance > q.length) | |
| { | |
| var matches = [], substrRegex = new RegExp(q, 'i'); | |
| $.each(strs, function(i, str) | |
| { | |
| if (str.length > q.length && substrRegex.test(str)) | |
| matches.push(str); | |
| }); | |
| } | |
| if (opts.levenshteinDistance && !matches.length) | |
| matches = strs; | |
| var ld = {}; | |
| matches.sort(function(a, b) | |
| { | |
| if (opts.levenshteinDistance >= q.length) | |
| { var d = charMatches(b, q) - charMatches(a, q); | |
| if (d) | |
| return d; | |
| } | |
| if (!opts.levenshteinDistance) | |
| return 0; | |
| if (!ld[a]) | |
| ld[a] = levDist(a, q); | |
| if (!ld[b]) | |
| ld[b] = levDist(b, q); | |
| return ld[a] - ld[b]; | |
| }); | |
| sync(matches); | |
| }; | |
| }; | |
| var lastVal = ''; | |
| var lastPos = 0; | |
| function beforeReplace(event, data) | |
| { | |
| lastVal = $(id).val(); | |
| lastPos = $(id).caret('pos'); | |
| return true; | |
| } | |
| function onReplace(event, data) | |
| { | |
| if (!data || !data.length) | |
| return; | |
| if (!lastVal.length) | |
| return; | |
| var root = lastVal.substr(0, lastPos); | |
| var post = lastVal.substr(lastPos); | |
| var typed = extractor(root); | |
| if (!lastUpper && typed.length >= root.length && 0 >= post.length) | |
| return; | |
| var str = root.substr(0, root.length - typed.length); | |
| str += lastUpper ? (data.substr(0, 1).toUpperCase() + data.substr(1)) : data; | |
| var cursorPos = str.length; | |
| str += post; | |
| $(id).val(str); | |
| $(id).caret('pos', cursorPos); | |
| } | |
| this.typeahead = | |
| $(id).typeahead({hint: false, highlight: false}, {'limit': 5, 'source': strMatcher(id, data)}) | |
| .on('typeahead:beforeselect', beforeReplace) | |
| .on('typeahead:beforeautocomplete', beforeReplace) | |
| .on('typeahead:beforecursorchange', beforeReplace) | |
| .on('typeahead:selected', function(event,data){setTimeout(function(){ onReplace(event, data); }, 0);}) | |
| .on('typeahead:autocompleted', onReplace) | |
| .on('typeahead:cursorchange', onReplace) | |
| ; | |
| } | |
| var myTypeahead = new MyTypeahead('#textbox', dinos, {levenshteinDistance: 3}); | |
| var myTypeahead = new MyTypeahead('#textarea', dinos, {levenshteinDistance: 3, vertAdjustMenu: true, trigger: '@'}); | |
| </script> |