├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── comment-issue.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── History.md ├── LICENSE ├── README.md ├── autocomplete-client.coffee ├── autocomplete-server.coffee ├── autocomplete.css ├── docs ├── mention1.png └── mention2.png ├── examples └── pubsublocal │ ├── .gitignore │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── identifier │ ├── packages │ ├── platforms │ ├── release │ └── versions │ ├── README.md │ ├── client │ ├── client.js │ ├── options.html │ ├── options.js │ ├── pubsublocal.html │ ├── single.html │ └── single.js │ ├── deploy.sh │ ├── lib │ └── collections.js │ ├── package-lock.json │ ├── package.json │ ├── packages │ └── autocomplete │ ├── server │ └── server.js │ └── upload-db.sh ├── inputs.html ├── package.js ├── templates.coffee └── tests ├── param_tests.coffee ├── regex_tests.coffee ├── rule_tests.coffee └── security_tests.coffee /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Reporting a bug or a problem 4 | title: '' 5 | labels: bug 6 | assignee: '' 7 | --- 8 | 9 | ## What 10 | 11 | 12 | ## Reproduction 13 | 14 | - [ ] Go to the sample application 15 | - [ ] Type... 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You would like to see something added to the project. 4 | title: '' 5 | labels: enhancement 6 | assignee: '' 7 | --- 8 | 9 | ## What 10 | 11 | 12 | ## Use case 13 | 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | ## What 13 | 14 | 15 | ## Why 16 | 17 | 18 | ## Checklist 19 | 20 | - [ ] Agreed upon solution from issue # 21 | - [ ] Stakeholders have approved the changes -------------------------------------------------------------------------------- /.github/workflows/comment-issue.yml: -------------------------------------------------------------------------------- 1 | name: Add immediate comment on new issues 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | createComment: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Create Comment 12 | uses: peter-evans/create-or-update-comment@v1.4.2 13 | with: 14 | issue-number: ${{ github.event.issue.number }} 15 | body: | 16 | Thank you for submitting this issue! 17 | 18 | We, the Members of Meteor Community Packages take every issue seriously. 19 | Our goal is to provide long-term lifecycles for packages and keep up 20 | with the newest changes in Meteor and the overall NodeJs/JavaScript ecosystem. 21 | 22 | However, we contribute to these packages mostly in our free time. 23 | Therefore, we can't guarantee your issues to be solved within certain time. 24 | 25 | If you think this issue is trivial to solve, don't hesitate to submit 26 | a pull request, too! We will accompany you in the process with reviews and hints 27 | on how to get development set up. 28 | 29 | Please also consider sponsoring the maintainers of the package. 30 | If you don't know who is currently maintaining this package, just leave a comment 31 | and we'll let you know 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .versions 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | .build* 18 | 19 | .idea/ 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: focal 3 | language: node_js 4 | node_js: 5 | - "14" 6 | - "12" 7 | before_install: 8 | - "curl -L http://git.io/ejPSng | /bin/sh" 9 | before_script: 10 | - "export PATH=$HOME/.meteor:$PATH" -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Meteor Community Code of Conduct 2 | 3 | ### Community 4 | 5 | We want to build a productive, happy and agile community that welcomes new ideas, constantly looks for areas to improve, and fosters collaboration. 6 | 7 | The project gains strength from a diversity of backgrounds and perspectives in our contributor community, and we actively seek participation from those who enhance it. This code of conduct exists to lay some ground rules that ensure we can collaborate and communicate effectively, despite our diversity. The code applies equally to founders, team members and those seeking help and guidance. 8 | 9 | ### Using This Code 10 | 11 | This isn’t an exhaustive list of things that you can’t do. Rather, it’s a guide for participation in the community that outlines how each of us can work to keep Meteor a positive, successful, and growing project. 12 | 13 | This code of conduct applies to all spaces managed by the Meteor Community project. This includes GitHub issues, and any other forums created by the team which the community uses for communication. We expect it to be honored by everyone who represents or participates in the project, whether officially or informally. 14 | 15 | #### When Something Happens 16 | 17 | If you see a Code of Conduct violation, follow these steps: 18 | 19 | * Let the person know that what they did is not appropriate and ask them to stop and/or edit their message(s). 20 | * That person should immediately stop the behavior and correct the issue. 21 | * If this doesn’t happen, or if you’re uncomfortable speaking up, contact admins. 22 | * As soon as available, an admin will join, identify themselves, and take further action (see below), starting with a warning, then temporary deactivation, then long-term deactivation. 23 | 24 | When reporting, please include any relevant details, links, screenshots, context, or other information that may be used to better understand and resolve the situation. 25 | 26 | The Admin team will prioritize the well-being and comfort of the recipients of the violation over the comfort of the violator. 27 | 28 | If you believe someone is violating the code of conduct, please report it to any member of the governing team. 29 | 30 | ### We Strive To: 31 | 32 | - **Be open, patient, and welcoming** 33 | 34 | Members of this community are open to collaboration, whether it's on PRs, issues, or problems. We're receptive to constructive comment and criticism, as we value what the experiences and skill sets of contributors bring to the project. We're accepting of all who wish to get involved, and find ways for anyone to participate in a way that best matches their strengths. 35 | 36 | - **Be considerate** 37 | 38 | We are considerate of our peers: other Meteor users and contributors. We’re thoughtful when addressing others’ efforts, keeping in mind that work is often undertaken for the benefit of the community. We also value others’ time and appreciate that not every issue or comment will be responded to immediately. We strive to be mindful in our communications, whether in person or online, and we're tactful when approaching views that are different from our own. 39 | 40 | - **Be respectful** 41 | 42 | As a community of professionals, we are professional in our handling of disagreements, and don’t allow frustration to turn into a personal attack. We work together to resolve conflict, assume good intentions and do our best to act in an empathic fashion. 43 | 44 | We do not tolerate harassment or exclusionary behavior. This includes, but is not limited to: 45 | - Violent threats or language directed against another person. 46 | - Discriminatory jokes and language. 47 | - Posting sexually explicit or sexualized content. 48 | - Posting content depicting or encouraging violence. 49 | - Posting (or threatening to post) other people's personally identifying information ("doxing"). 50 | - Personal insults, especially those using racist or sexist terms. 51 | - Unwelcome sexual attention. 52 | - Advocating for, or encouraging, any of the above behavior. 53 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 54 | 55 | - **Take responsibility for our words and our actions** 56 | 57 | We can all make mistakes; when we do, we take responsibility for them. If someone has been harmed or offended, we listen carefully and respectfully. We are also considerate of others’ attempts to amend their mistakes. 58 | 59 | - **Be collaborative** 60 | 61 | The work we produce is (and is part of) an ecosystem containing several parallel efforts working towards a similar goal. Collaboration between teams and individuals that each have their own goal and vision is essential to reduce redundancy and improve the quality of our work. 62 | 63 | Internally and externally, we celebrate good collaboration. Wherever possible, we work closely with upstream projects and others in the free software community to coordinate our efforts. We prefer to work transparently and involve interested parties as early as possible. 64 | 65 | - **Ask for help when in doubt** 66 | 67 | Nobody is expected to be perfect in this community. Asking questions early avoids many problems later, so questions are encouraged, though they may be directed to the appropriate forum. Those who are asked should be responsive and helpful. 68 | 69 | - **Take initiative** 70 | 71 | We encourage new participants to feel empowered to lead, to take action, and to experiment when they feel innovation could improve the project. If we have an idea for a new tool, or how an existing tool can be improved, we speak up and take ownership of that work when possible. 72 | 73 | ### Attribution 74 | 75 | This Code of Conduct was inspired by [Meteor CoC](https://github.com/meteor/meteor/blob/devel/CODE_OF_CONDUCT.md). 76 | 77 | Sections of this Code of Conduct were inspired in by the following Codes from other open source projects and resources we admire: 78 | 79 | - [The Contributor Covenant](http://contributor-covenant.org/version/1/4/) 80 | - [Python](https://www.python.org/psf/codeofconduct/) 81 | - [Ubuntu](http://www.ubuntu.com/about/about-ubuntu/conduct) 82 | - [Django](https://www.djangoproject.com/conduct/) 83 | 84 | *This Code of Conduct is licensed under the [Creative Commons Attribution-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/) license. This Code was last updated on March 12, 2019.* 85 | 86 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## vNEXT 2 | 3 | * Add proper package dependencies for newer versions of Meteor. 4 | * Fix an issue where an extraneous `collection` field was required for custom server-side subscriptions. (#40) 5 | * Make compatible with CoffeeScript 2 (#148) 6 | * `autocomplete` html attribute is now set to `off` in order to prevent browser displaying its own autocomplete dropdown (#99). 7 | * Updated example to the latest Meteor version 8 | * Support all utf-8 symbols in regex (#151) 9 | 10 | ## v0.5.1 11 | 12 | * Allow either top or bottom positioning in both normal and whole-field modes. (#75) 13 | 14 | ## v0.5.0 15 | 16 | * Switch to jQuery events instead of callbacks; you can now detect autocomplete selections using a template's event map. **Callbacks are no longer supported.** See the demo for a use example. (#48, #56) 17 | 18 | ## v0.4.10 19 | 20 | * Make the `Autocomplete.publishCursor(cursor, subscription)` function available on the server, which greatly simplifies the process of returning results for an autocomplete query over a publication. 21 | * Update the usage of the Mongo Collection API changed in Meteor 0.9.1 and later. 22 | 23 | ## v0.4.9 24 | 25 | * Update usage of template helpers for Meteor 0.9.4. (#66, #67) 26 | * Don't follow the cursor in whole-field autocompletion mode (#55, #63 -thanks @cretep). 27 | * Better compatibility of whole-field mode when using `TAB` and `Shift+TAB` after selections. (#64) 28 | 29 | ## v0.4.8 30 | 31 | * Updates for Meteor 0.9.1 APIs, since we use a lot of weird stuff. This is just to get things working; expect some general cleanup in the future as Meteor's API stabilizes for 1.0. 32 | 33 | ## v0.4.7 34 | 35 | * **Updated for Meteor 0.9.** 36 | * Made pre-sorting the autocomplete list an option that is off by default, for better performance on searches over large collections, especially on the client. 37 | * Fix errors resulting from trying to select nonexistent items. 38 | 39 | ## v0.4.6 40 | 41 | * Refactor UI components using the new Blaze API on Meteor 0.8.3, with Blaze Views. 42 | * Restore textarea block helper content. 43 | 44 | ## v0.4.5 45 | 46 | * Temporarily disable textarea block helper content until the Blaze API is updated. 47 | 48 | ## v0.4.4 49 | 50 | * Simulate pre-Blaze rendering behavior to properly deal with changing data contexts, until an updated Blaze Component API is released. 51 | * Support a custom specified template when no match is found. (#25) 52 | 53 | ## v0.4.3 54 | 55 | * Fix an issue where caret position was incorrect on a focus. 56 | 57 | ## v0.4.2 58 | 59 | * Use the Meteor caret-position package instead of the `jquery-caretposition` and `jquery-migrate` packages. 60 | * Added some validation for specifying rules, and tests for regular expressions. 61 | * Improve behavior of whole-field (tokenless) autocompletion. 62 | * Pressing the escape key while autocompleting now blurs the field. 63 | 64 | ## v0.4.1 65 | 66 | * Allow for creating any custom selector from an autocomplete match, in addition to the standard `$regex` behavior. 67 | 68 | ## v0.4.0 69 | 70 | * Revamped the behavior of token-less autocompletion. (See #4, #27, and #33) 71 | * The selection callback now passes the input element as the second argument. (#31) 72 | 73 | ## v0.3.0 74 | 75 | * Update for Meteor 0.8.0 (Blaze). **NOTE: You will need to update your app to use this version.** (#22) 76 | 77 | ## v0.2.4 78 | 79 | This is the last version of autocomplete that will support Meteor <0.8.0 (Blaze). 80 | 81 | * Add an optional `filter` field to allow additional static filters on a collection search. (#21) 82 | * Only insecure collections can be searched by default on the server side. **If you are using the default implementation, you will need to write your own publish function**. (#20) 83 | * Add automated testing infrastructure. 84 | 85 | ## v0.2.3 86 | 87 | * Support nested values for `field`, i.e. `'profile.foo'`. (#19) 88 | 89 | ## v0.2.2 90 | 91 | * Provided an option for callbacks when an item is selected and inserted (#18). 92 | 93 | ## v0.2.1 94 | 95 | * Fixed a bug with CSS positioning of the autocomplete popup. 96 | * Provided more control over regex options. Default option is case-insensitive `'i'`. 97 | 98 | ## v0.2.0 99 | 100 | * Added server-side (pub/sub) autocompletion (#6) - many thanks to @dandv; see #17 for implementation discussion. 101 | 102 | ## v0.1.1 103 | 104 | * Increased z-index on autocomplete container (#8). 105 | * Added jquery-migrate package to temporarily support caret position operations on Meteor 0.7.1.2 (this will be fixed in the future). 106 | 107 | ## v0.1.0 108 | 109 | * First release. 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Andrew Mao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | meteor-autocomplete [![Build Status](https://travis-ci.org/mizzao/meteor-autocomplete.svg)](https://travis-ci.org/mizzao/meteor-autocomplete) 2 | =================== 3 | 4 | Client/server autocompletion designed for Meteor's collections and reactivity. 5 | 6 | Check out a demo app at http://autocomplete.meteor.com or the [source](examples/pubsublocal). 7 | 8 | Help keep your favorite Meteor packages alive! If you depend on this package in your app and find it useful, consider a donation at [Gittip](https://www.gittip.com/mizzao/) for me (or other Meteor package maintainers). 9 | 10 | ## What's this do? 11 | 12 | Auto-completes typing in text `input`s or `textarea`s from different local or remote Meteor collections when triggered by certain symbols. You've probably seen this when referring to users or issues in a GitHub conversation. For example, you may want to ping a user: 13 | 14 | ![Autocompleting a user](docs/mention1.png) 15 | 16 | ...and ask them to look at a certain item: 17 | 18 | ![Autocompleting something else](docs/mention2.png) 19 | 20 | Features: 21 | - Multiple collection matching with different trigger tokens and fields 22 | - Fully live and reactive Meteor template rendering of drop-down list items 23 | - Drop-down can be positioned above or below the text 24 | - Mouse or keyboard interaction with autocomplete menu 25 | - Simple token-less autocompletion in an `` element, just like Bootstrap typeahead 26 | 27 | Meteor's client-side data availability makes this dynamic, full-fledged autocomplete widget possible. Use it in chat rooms, comments, other messaging systems, or wherever strikes your fancy. 28 | 29 | ## Usage 30 | 31 | Use Meteor to install the package: 32 | 33 | ``` 34 | meteor add mizzao:autocomplete 35 | ``` 36 | 37 | Add a text `input` or `textarea` to a template in one of the following ways, as a Spacebars template or block helper. Pass in any HTML parameters as other arguments to the template: 38 | 39 | ``` 40 | 45 | 46 | 53 | ``` 54 | 55 | Define a helper for the first argument, like the following example: 56 | 57 | ```javascript 58 | Template.foo.helpers({ 59 | settings: function() { 60 | return { 61 | position: "top", 62 | limit: 5, 63 | rules: [ 64 | { 65 | token: '@', 66 | collection: Meteor.users, 67 | field: "username", 68 | template: Template.userPill 69 | }, 70 | { 71 | token: '!', 72 | collection: Dataset, 73 | field: "_id", 74 | options: '', 75 | matchAll: true, 76 | filter: { type: "autocomplete" }, 77 | template: Template.dataPiece 78 | } 79 | ] 80 | }; 81 | } 82 | }); 83 | ``` 84 | 85 | ##### Top Level Options 86 | 87 | - `position` (= `top` or `bottom`) specifies if the autocomplete menu should render above or below the cursor. Select based on the placement of your `input`/`textarea` relative to other elements on the page. 88 | - `limit`: Controls how big the autocomplete menu should get. 89 | - `rules`: An array of matching rules for the autocomplete widget, which will be checked in order. 90 | 91 | ##### Rule Specific Options 92 | 93 | - `token`: (optional) What character should trigger this rule. Leave blank for whole-field behavior (see below). 94 | - `collection`: What collection should be used to match for this rule. Must be a `Mongo.Collection` for client-side collections, or a string for remote collections (available in `global` on the server.) 95 | - `subscription`: A custom subscription for server-side search; see below. 96 | - `template`: The template that should be used to render each list item. 97 | - `filter`: (optional) An object that will be merged with the autocomplete selector to limit the results to more specific documents in the collection. 98 | - `sort`: (default `false`) Whether to sort the results before applying the limit. For good performance on large collections, this should be turned on only for server-side searches where an index can be used. 99 | - `noMatchTemplate`: (optional) A template to display when nothing matches. This template can use the [reactive functions on the AutoComplete object](autocomplete-client.coffee) to display a specific message, or be [assigned mouse/keyboard events](http://docs.meteor.com/#eventmaps) for user interaction. 100 | 101 | Default matcher arguments: the default behavior is to create a regex against the field to be matched, which will be constructed using the arguments below. 102 | 103 | - `field`: The field of the collection that the rule will match against. Can be nested, i.e. `'profile.foo'`. 104 | - `options`: `'i'` (default) to specify the regex matching options. 105 | - `matchAll`: `false` (default) to match only fields starting with the matched string. (see below) 106 | 107 | Custom matcher: if this is specified, the *default* matcher arguments will be ignored. (Note that you should still specify `field`.) 108 | 109 | - `selector`: a one argument `function(match)` that takes the currently matched token suffix and returns the selector that should be added to the argument to `collection.find` to filter the autocomplete results. (**NOTE**: if you are using `$where`, the selector cannot be serialized to the server). 110 | 111 | ##### Detecting Selections 112 | 113 | Autocomplete triggers jQuery events that can be listened to using Meteor's event maps. The only currently supported event is `autocompleteselect`, which notifies of a selected element. For example: 114 | 115 | ``` 116 | Template.foo.events({ 117 | "autocompleteselect input": function(event, template, doc) { 118 | console.log("selected ", doc); 119 | } 120 | }); 121 | ``` 122 | 123 | See the example app for more details. 124 | 125 | ##### Regex Specification and Options 126 | 127 | Note that [regular expression searches](http://docs.mongodb.org/manual/reference/operator/query/regex/) can only use an index efficiently when the regular expression has an anchor for the beginning (i.e. `^`) of a string and is a case-sensitive match. Hence, when using case-sensitive matches and string start anchors (i.e. `matchAll: false`) searches can take advantage of server indices in Mongo. 128 | 129 | This behavior is demonstrated in the example app. 130 | 131 | ##### Whole-field (Tokenless) Autocompletion 132 | 133 | If you only need to autocomplete over a single collection and want to match the entire field, specify a `rules` array with a single object and omit the `token` argument. The behavior for this is a little different than with tokens; see the [demo](http://autocomplete.meteor.com). 134 | 135 | Mixing tokens with tokenless autocompletion is unsupported and will probably result in unexpected behavior. 136 | 137 | ##### Server-side Autocompletion and Text Search Engines 138 | 139 | For security purposes, a default implementation of server-side autocomplete is only provided for insecure collections, to be used while prototyping. In all other applications, write your own publish function with the same arguments as in the [autocomplete-recordset](autocomplete-server.coffee) publication and secure it properly, given that malicious clients can subscribe to this function in ways other than the autocomplete client code would. 140 | 141 | Make sure to push documents to the `autocompleteRecords` client-side collection. A convenience function, `Autocomplete.publishCursor`, is provided as an easy way to do this. See the default implementation for an example. 142 | 143 | Use of a custom publish function also allows you to: 144 | 145 | * use full-text search services outside of Meteor, such as [ElasticSearch](http://www.elasticsearch.org/) 146 | * use [preferential matching](https://github.com/mizzao/meteor-autocomplete/blob/a437c7b464ad9e779da2ca15566a5b91cf603902/autocomplete-server.coffee) for record fields that start with the autocomplete text, rather than contain it anywhere 147 | 148 | ##### Autocomplete Templates 149 | 150 | An autocomplete template is just a normal Meteor template that is passed in the matched document. The template will be passed the entire matched document as a data context, so render list items as fancily as you would like. For example, it's usually helpful to see metadata for matches as in the pictures above. 151 | 152 | Records that match the filter text typed after the token render a list of the `template` sorted in ascending order by `field`. For example, if you were matching on `Meteor.users` and you just wanted to display the username, you can do something very simple, and display the same field: 153 | 154 | ``` 155 | 158 | ``` 159 | 160 | However, you might want to do something a little more fancy and show not only the user, but whether they are online or not (with something like the [user-status](https://github.com/mizzao/meteor-user-status) package. In that case you could do something like the following: 161 | 162 | ``` 163 | 166 | ``` 167 | 168 | and accompanying code: 169 | 170 | ```javascript 171 | Template.userPill.labelClass = function() { 172 | if this._id === Meteor.userId() 173 | return "label-warning" 174 | else if this.profile.online === true 175 | return "label-success" 176 | else 177 | return "" 178 | } 179 | ``` 180 | 181 | This (using normal Bootstrap classes) will cause the user to show up in orange for him/herself, in green for other users that are online, and in grey otherwise. See [CrowdMapper's templates](https://github.com/mizzao/CrowdMapper/blob/master/client/views/common.html) for other interesting things you may want to do. 182 | 183 | ##### Examples 184 | 185 | For example settings see one of the following: 186 | 187 | - [Multi-field example](examples/pubsublocal/client/client.js) (from the app above) 188 | - [Single-field example](examples/pubsublocal/client/single.js) (also from the app above) 189 | - [Autocompleting chatroom example](https://github.com/mizzao/CrowdMapper/blob/master/client/views/chat.coffee) 190 | 191 | ### Future Work (a.k.a. send pull requests) 192 | 193 | - To reduce latency, we could additionally support using `Meteor.methods` to return an array of documents, instead of a subscription, if the client's cache of the collection is assumed to be read-only or if changes don't matter. 194 | - The widget can keep track of a list of ordered document ids for matched items instead of just spitting out the fields (which currently should be unique) 195 | - Could potentially support rendering DOM elements instead of just text. However, this can currently be managed in post-processing code for chat/post functions (like how GitHub does it). 196 | 197 | ### Credits/Notes 198 | 199 | - If you are not using Meteor, you may want to check out [jquery sew](https://github.com/tactivos/jquery-sew), from which this was heavily modified. 200 | - If you need tag autocompletion only (from one collection, and no text), try either the [x-editable smart package](https://github.com/nate-strauser/meteor-x-editable-bootstrap) with Select2 or [jquery-tokenInput](http://loopj.com/jquery-tokeninput/). Those support rendering DOM elements in the input field. 201 | 202 | ### Main Contributors 203 | 204 | - Andrew Mao ([mizzao](https://github.com/mizzao)) 205 | - Dan Dascalescu ([dandv](https://github.com/dandv)) 206 | - Adam Love ([Neobii](https://github.com/Neobii)) 207 | 208 | -------------------------------------------------------------------------------- /autocomplete-client.coffee: -------------------------------------------------------------------------------- 1 | AutoCompleteRecords = new Mongo.Collection("autocompleteRecords") 2 | 3 | isServerSearch = (rule) -> rule.subscription? || _.isString(rule.collection) 4 | 5 | validateRule = (rule) -> 6 | if rule.subscription? and rule.collection? 7 | throw new Error("Rule cannot specify both a server-side subscription and a client/server collection to search simultaneously") 8 | 9 | unless rule.subscription? or Match.test(rule.collection, Match.OneOf(String, Mongo.Collection)) 10 | throw new Error("Collection to search must be either a Mongo collection or server-side name") 11 | 12 | # XXX back-compat message, to be removed 13 | if rule.callback? 14 | console.warn("autocomplete no longer supports callbacks; use event listeners instead.") 15 | 16 | isWholeField = (rule) -> 17 | # either '' or null both count as whole field. 18 | return !rule.token 19 | 20 | getRegExp = (rule) -> 21 | unless isWholeField(rule) 22 | # Expressions for the range from the last word break to the current cursor position 23 | new RegExp('(^|\\b|\\s)' + rule.token + '([\\\\w.a-zA-Z0-9\u0080-\u9fff]*)$') 24 | else 25 | # Whole-field behavior - word characters or spaces 26 | new RegExp('(^)(.*)$') 27 | 28 | getFindParams = (rule, filter, limit) -> 29 | # This is a different 'filter' - the selector from the settings 30 | # We need to extend so that we don't copy over rule.filter 31 | selector = _.extend({}, rule.filter || {}) 32 | options = { limit: limit } 33 | 34 | # Match anything, no sort, limit X 35 | return [ selector, options ] unless filter 36 | 37 | if rule.sort and rule.field 38 | sortspec = {} 39 | # Only sort if there is a filter, for faster performance on a match of anything 40 | sortspec[rule.field] = 1 41 | options.sort = sortspec 42 | 43 | if _.isFunction(rule.selector) 44 | # Custom selector 45 | _.extend(selector, rule.selector(filter)) 46 | else 47 | selector[rule.field] = { 48 | $regex: if rule.matchAll then filter else "^" + filter 49 | # default is case insensitive search - empty string is not the same as undefined! 50 | $options: if (typeof rule.options is 'undefined') then 'i' else rule.options 51 | } 52 | 53 | return [ selector, options ] 54 | 55 | getField = (obj, str) -> 56 | obj = obj[key] for key in str.split(".") 57 | return obj 58 | 59 | class @AutoComplete 60 | 61 | @KEYS: [ 62 | 40, # DOWN 63 | 38, # UP 64 | 13, # ENTER 65 | 27, # ESCAPE 66 | 9 # TAB 67 | ] 68 | 69 | constructor: (settings) -> 70 | @limit = settings.limit || 5 71 | @position = settings.position || "bottom" 72 | 73 | @rules = settings.rules 74 | validateRule(rule) for rule in @rules 75 | 76 | @expressions = (getRegExp(rule) for rule in @rules) 77 | 78 | @matched = -1 79 | @loaded = true 80 | 81 | # Reactive dependencies for current matching rule and filter 82 | @ruleDep = new Deps.Dependency 83 | @filterDep = new Deps.Dependency 84 | @loadingDep = new Deps.Dependency 85 | 86 | # autosubscribe to the record set published by the server based on the filter 87 | # This will tear down server subscriptions when they are no longer being used. 88 | @sub = null 89 | @comp = Deps.autorun => 90 | # Stop any existing sub immediately, don't wait 91 | @sub?.stop() 92 | 93 | return unless (rule = @matchedRule()) and (filter = @getFilter()) isnt null 94 | 95 | # subscribe only for server-side collections 96 | unless isServerSearch(rule) 97 | @setLoaded(true) # Immediately loaded 98 | return 99 | 100 | [ selector, options ] = getFindParams(rule, filter, @limit) 101 | 102 | # console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field 103 | @setLoaded(false) 104 | subName = rule.subscription || "autocomplete-recordset" 105 | @sub = Meteor.subscribe(subName, 106 | selector, options, rule.collection, => @setLoaded(true)) 107 | 108 | teardown: -> 109 | # Stop the reactive computation we started for this autocomplete instance 110 | @comp.stop() 111 | 112 | # reactive getters and setters for @filter and the currently matched rule 113 | matchedRule: -> 114 | @ruleDep.depend() 115 | if @matched >= 0 then @rules[@matched] else null 116 | 117 | setMatchedRule: (i) -> 118 | @matched = i 119 | @ruleDep.changed() 120 | 121 | getFilter: -> 122 | @filterDep.depend() 123 | return @filter 124 | 125 | setFilter: (x) -> 126 | @filter = x 127 | @filterDep.changed() 128 | return @filter 129 | 130 | isLoaded: -> 131 | @loadingDep.depend() 132 | return @loaded 133 | 134 | setLoaded: (val) -> 135 | return if val is @loaded # Don't cause redraws unnecessarily 136 | @loaded = val 137 | @loadingDep.changed() 138 | 139 | onKeyUp: -> 140 | return unless @$element # Don't try to do this while loading 141 | startpos = @element.selectionStart 142 | val = @getText().substring(0, startpos) 143 | 144 | ### 145 | Matching on multiple expressions. 146 | We always go from a matched state to an unmatched one 147 | before going to a different matched one. 148 | ### 149 | i = 0 150 | breakLoop = false 151 | while i < @expressions.length 152 | matches = val.match(@expressions[i]) 153 | 154 | # matching -> not matching 155 | if not matches and @matched is i 156 | @setMatchedRule(-1) 157 | breakLoop = true 158 | 159 | # not matching -> matching 160 | if matches and @matched is -1 161 | @setMatchedRule(i) 162 | breakLoop = true 163 | 164 | # Did filter change? 165 | if matches and @filter isnt matches[2] 166 | @setFilter(matches[2]) 167 | breakLoop = true 168 | 169 | break if breakLoop 170 | i++ 171 | 172 | onKeyDown: (e) -> 173 | return if @matched is -1 or (@constructor.KEYS.indexOf(e.keyCode) < 0) 174 | 175 | switch e.keyCode 176 | when 9, 13 # TAB, ENTER 177 | if @select() # Don't jump fields or submit if select successful 178 | e.preventDefault() 179 | e.stopPropagation() 180 | # preventDefault needed below to avoid moving cursor when selecting 181 | when 40 # DOWN 182 | e.preventDefault() 183 | @next() 184 | when 38 # UP 185 | e.preventDefault() 186 | @prev() 187 | when 27 # ESCAPE 188 | @$element.blur() 189 | @hideList() 190 | 191 | return 192 | 193 | onFocus: -> 194 | # We need to run onKeyUp after the focus resolves, 195 | # or the caret position (selectionStart) will not be correct 196 | Meteor.defer => @onKeyUp() 197 | 198 | onBlur: -> 199 | # We need to delay this so click events work 200 | # TODO this is a bit of a hack; see if we can't be smarter 201 | Meteor.setTimeout => 202 | @hideList() 203 | , 500 204 | 205 | onItemClick: (doc, e) => @processSelection(doc, @rules[@matched]) 206 | 207 | onItemHover: (doc, e) -> 208 | @tmplInst.$(".-autocomplete-item").removeClass("selected") 209 | $(e.target).closest(".-autocomplete-item").addClass("selected") 210 | 211 | filteredList: -> 212 | # @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered 213 | filter = @getFilter() # Reactively depend on the filter 214 | return null if @matched is -1 215 | 216 | rule = @rules[@matched] 217 | # Don't display list unless we have a token or a filter (or both) 218 | # Single field: nothing displayed until something is typed 219 | return null unless rule.token or filter 220 | 221 | [ selector, options ] = getFindParams(rule, filter, @limit) 222 | 223 | Meteor.defer => @ensureSelection() 224 | 225 | # if server collection, the server has already done the filtering work 226 | return AutoCompleteRecords.find({}, options) if isServerSearch(rule) 227 | 228 | # Otherwise, search on client 229 | return rule.collection.find(selector, options) 230 | 231 | isShowing: -> 232 | rule = @matchedRule() 233 | # Same rules as above 234 | showing = rule? and (rule.token or @getFilter()) 235 | 236 | # Do this after the render 237 | if showing 238 | Meteor.defer => 239 | @positionContainer() 240 | @ensureSelection() 241 | 242 | return showing 243 | 244 | # Replace text with currently selected item 245 | select: -> 246 | node = @tmplInst.find(".-autocomplete-item.selected") 247 | return false unless node? 248 | doc = Blaze.getData(node) 249 | return false unless doc # Don't select if nothing matched 250 | 251 | @processSelection(doc, @rules[@matched]) 252 | return true 253 | 254 | processSelection: (doc, rule) -> 255 | replacement = getField(doc, rule.field) 256 | 257 | unless isWholeField(rule) 258 | @replace(replacement, rule) 259 | @hideList() 260 | 261 | else 262 | # Empty string or doesn't exist? 263 | # Single-field replacement: replace whole field 264 | @setText(replacement) 265 | 266 | # Field retains focus, but list is hidden unless another key is pressed 267 | # Must be deferred or onKeyUp will trigger and match again 268 | # TODO this is a hack; see above 269 | @onBlur() 270 | 271 | @$element.trigger("autocompleteselect", doc) 272 | return 273 | 274 | # Replace the appropriate region 275 | replace: (replacement) -> 276 | startpos = @element.selectionStart 277 | fullStuff = @getText() 278 | val = fullStuff.substring(0, startpos) 279 | val = val.replace(@expressions[@matched], "$1" + @rules[@matched].token + replacement) 280 | posfix = fullStuff.substring(startpos, fullStuff.length) 281 | separator = (if posfix.match(/^\s/) then "" else " ") 282 | finalFight = val + separator + posfix 283 | @setText finalFight 284 | 285 | newPosition = val.length + 1 286 | @element.setSelectionRange(newPosition, newPosition) 287 | return 288 | 289 | hideList: -> 290 | @setMatchedRule(-1) 291 | @setFilter(null) 292 | 293 | getText: -> 294 | return @$element.val() || @$element.text() 295 | 296 | setText: (text) -> 297 | if @$element.is("input,textarea") 298 | @$element.val(text).change() 299 | else 300 | @$element.html(text) 301 | 302 | ### 303 | Rendering functions 304 | ### 305 | positionContainer: -> 306 | # First render; Pick the first item and set css whenever list gets shown 307 | position = @$element.position() 308 | 309 | rule = @matchedRule() 310 | 311 | offset = getCaretCoordinates(@element, @element.selectionStart) 312 | 313 | # In whole-field positioning, we don't move the container and make it the 314 | # full width of the field. 315 | if rule? and isWholeField(rule) 316 | pos = 317 | left: position.left 318 | width: @$element.outerWidth() # position.offsetWidth 319 | else # Normal positioning, at token word 320 | pos = 321 | left: position.left + offset.left 322 | 323 | # Position menu from top (above) or from bottom of caret (below, default) 324 | if @position is "top" 325 | pos.bottom = @$element.offsetParent().height() - position.top - offset.top 326 | else 327 | pos.top = position.top + offset.top + parseInt(@$element.css('font-size')) 328 | 329 | @tmplInst.$(".-autocomplete-container").css(pos) 330 | 331 | ensureSelection : -> 332 | # Re-render; make sure selected item is something in the list or none if list empty 333 | selectedItem = @tmplInst.$(".-autocomplete-item.selected") 334 | 335 | unless selectedItem.length 336 | # Select anything 337 | @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") 338 | 339 | # Select next item in list 340 | next: -> 341 | currentItem = @tmplInst.$(".-autocomplete-item.selected") 342 | return unless currentItem.length # Don't try to iterate an empty list 343 | currentItem.removeClass("selected") 344 | 345 | next = currentItem.next() 346 | if next.length 347 | next.addClass("selected") 348 | else # End of list or lost selection; Go back to first item 349 | @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") 350 | 351 | # Select previous item in list 352 | prev: -> 353 | currentItem = @tmplInst.$(".-autocomplete-item.selected") 354 | return unless currentItem.length # Don't try to iterate an empty list 355 | currentItem.removeClass("selected") 356 | 357 | prev = currentItem.prev() 358 | if prev.length 359 | prev.addClass("selected") 360 | else # Beginning of list or lost selection; Go to end of list 361 | @tmplInst.$(".-autocomplete-item:last-child").addClass("selected") 362 | 363 | # This doesn't need to be reactive because list already changes reactively 364 | # and will cause all of the items to re-render anyway 365 | currentTemplate: -> @rules[@matched].template 366 | 367 | AutocompleteTest = 368 | records: AutoCompleteRecords 369 | isServerSearch: isServerSearch 370 | getRegExp: getRegExp 371 | getFindParams: getFindParams 372 | -------------------------------------------------------------------------------- /autocomplete-server.coffee: -------------------------------------------------------------------------------- 1 | class Autocomplete 2 | @publishCursor: (cursor, sub) -> 3 | # This also attaches an onStop callback to sub, so we don't need to worry about that. 4 | # https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js 5 | Mongo.Collection._publishCursor(cursor, sub, "autocompleteRecords") 6 | 7 | Meteor.publish 'autocomplete-recordset', (selector, options, collName) -> 8 | collection = global[collName] 9 | unless collection 10 | throw new Error(collName + ' is not defined on the global namespace of the server.') 11 | 12 | # This is a semi-documented Meteor feature: 13 | # https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js 14 | unless collection._isInsecure() 15 | Meteor._debug(collName + ' is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.') 16 | return [] # We need this for the subscription to be marked ready 17 | 18 | # guard against client-side DOS: hard limit to 50 19 | options.limit = Math.min(50, Math.abs(options.limit)) if options.limit 20 | 21 | # Push this into our own collection on the client so they don't interfere with other publications of the named collection. 22 | # This also stops the observer automatically when the subscription is stopped. 23 | Autocomplete.publishCursor( collection.find(selector, options), this) 24 | 25 | # Mark the subscription ready after the initial addition of documents. 26 | this.ready() 27 | 28 | -------------------------------------------------------------------------------- /autocomplete.css: -------------------------------------------------------------------------------- 1 | .-autocomplete-container { 2 | position: absolute; 3 | background: white; 4 | border: 1px solid #DDD; 5 | border-radius: 3px; 6 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 7 | min-width: 180px; 8 | z-index: 1000; 9 | } 10 | 11 | .-autocomplete-list { 12 | list-style: none; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | .-autocomplete-item { 18 | display: block; 19 | padding: 5px 10px; 20 | border-bottom: 1px solid #DDD; 21 | } 22 | 23 | .-autocomplete-item.selected { 24 | color: white; 25 | background: #4183C4; 26 | text-decoration: none; 27 | } 28 | -------------------------------------------------------------------------------- /docs/mention1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meteor-Community-Packages/meteor-autocomplete/a77c3cf3200ea31ab0c06eca5a193d33be08e4ed/docs/mention1.png -------------------------------------------------------------------------------- /docs/mention2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meteor-Community-Packages/meteor-autocomplete/a77c3cf3200ea31ab0c06eca5a193d33be08e4ed/docs/mention2.png -------------------------------------------------------------------------------- /examples/pubsublocal/.gitignore: -------------------------------------------------------------------------------- 1 | .meteor/local 2 | .meteor/meteorite 3 | settings.json 4 | node_modules/ 5 | 6 | # the mongodb dump 7 | dump/* 8 | -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.3.5-remove-old-dev-bundle-link 15 | 1.4.0-remove-old-dev-bundle-link 16 | 1.4.1-add-shell-server-package 17 | 1.4.3-split-account-service-packages 18 | 1.5-add-dynamic-import-package 19 | 1.7-split-underscore-from-meteor-base 20 | 1.8.3-split-jquery-from-blaze 21 | -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | dev_bundle 2 | local 3 | -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 136t8nc1inuy9x1nm4ry3 8 | -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/identifier: -------------------------------------------------------------------------------- 1 | 1uiyyydkjmd1653fcdp -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | standard-app-packages 7 | coffeescript 8 | insecure@1.0.7 9 | mizzao:autocomplete 10 | twbs:bootstrap 11 | standard-minifier-css@1.6.1 12 | standard-minifier-js@2.6.0 13 | shell-server 14 | dynamic-import 15 | ecmascript 16 | -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.11.1 2 | -------------------------------------------------------------------------------- /examples/pubsublocal/.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | autoupdate@1.6.0 3 | babel-compiler@7.5.3 4 | babel-runtime@1.5.0 5 | base64@1.0.12 6 | binary-heap@1.0.11 7 | blaze@2.3.4 8 | blaze-tools@1.0.10 9 | boilerplate-generator@1.7.1 10 | caching-compiler@1.2.2 11 | caching-html-compiler@1.1.3 12 | callback-hook@1.3.0 13 | check@1.3.1 14 | coffeescript@1.0.17 15 | dandv:caret-position@2.1.1 16 | ddp@1.4.0 17 | ddp-client@2.3.3 18 | ddp-common@1.4.0 19 | ddp-server@2.3.2 20 | deps@1.0.12 21 | diff-sequence@1.1.1 22 | dynamic-import@0.5.3 23 | ecmascript@0.14.3 24 | ecmascript-runtime@0.7.0 25 | ecmascript-runtime-client@0.11.0 26 | ecmascript-runtime-server@0.10.0 27 | ejson@1.1.1 28 | fastclick@1.0.13 29 | fetch@0.1.1 30 | geojson-utils@1.0.10 31 | html-tools@1.0.11 32 | htmljs@1.0.11 33 | id-map@1.1.0 34 | insecure@1.0.7 35 | inter-process-messaging@0.1.1 36 | jquery@1.11.11 37 | launch-screen@1.2.0 38 | livedata@1.0.18 39 | logging@1.1.20 40 | meteor@1.9.3 41 | meteor-platform@1.2.6 42 | minifier-css@1.5.3 43 | minifier-js@2.6.0 44 | minimongo@1.6.0 45 | mizzao:autocomplete@0.5.1 46 | mobile-status-bar@1.1.0 47 | modern-browsers@0.1.5 48 | modules@0.15.0 49 | modules-runtime@0.12.0 50 | mongo@1.10.0 51 | mongo-decimal@0.1.1 52 | mongo-dev-server@1.1.0 53 | mongo-id@1.0.7 54 | npm-mongo@3.8.1 55 | observe-sequence@1.0.16 56 | ordered-dict@1.1.0 57 | promise@0.11.2 58 | random@1.2.0 59 | reactive-dict@1.3.0 60 | reactive-var@1.0.11 61 | reload@1.3.0 62 | retry@1.1.0 63 | routepolicy@1.1.0 64 | session@1.2.0 65 | shell-server@0.5.0 66 | socket-stream-client@0.3.1 67 | spacebars@1.0.15 68 | spacebars-compiler@1.1.3 69 | standard-app-packages@1.0.9 70 | standard-minifier-css@1.6.1 71 | standard-minifier-js@2.6.0 72 | templating@1.3.2 73 | templating-compiler@1.3.3 74 | templating-runtime@1.3.2 75 | templating-tools@1.1.2 76 | tracker@1.2.0 77 | twbs:bootstrap@3.3.6 78 | ui@1.0.13 79 | underscore@1.0.10 80 | webapp@1.9.1 81 | webapp-hashing@1.0.9 82 | -------------------------------------------------------------------------------- /examples/pubsublocal/README.md: -------------------------------------------------------------------------------- 1 | # Demo/testing app for autocomplete 2 | 3 | To run: 4 | 5 | ```shell script 6 | npm run start 7 | ``` 8 | or 9 | ```shell script 10 | meteor 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/pubsublocal/client/client.js: -------------------------------------------------------------------------------- 1 | // client-only collection to demo interoperability with server-side one 2 | Fruits = new Mongo.Collection(null); 3 | 4 | ['Apple', 'Banana', 'Cherry', 'Date', 'Fig', 'Lemon', 'Melon', 'Prune', 'Raspberry', 'Strawberry', 'Blueberry', 'Blackberry', 'Boysenberry', 'Licorice', 'Watermelon', 'Tomato', 'Jackfruit', 'Kiwi', 'Lime', 'Clementine', 'Tangerine', 'Orange', 'Grape'].forEach(function (fruit) { 5 | Fruits.insert({type: fruit}) 6 | }); 7 | 8 | Template.pubsub.helpers({ 9 | settings: function() { 10 | return { 11 | position: Session.get("position"), 12 | limit: 30, // more than 20, to emphasize matches outside strings *starting* with the filter 13 | rules: [ 14 | { 15 | token: '@', 16 | // string means a server-side collection; otherwise, assume a client-side collection 17 | collection: 'BigCollection', 18 | field: 'name', 19 | options: '', // Use case-sensitive match to take advantage of server index. 20 | template: Template.serverCollectionPill, 21 | noMatchTemplate: Template.serverNoMatch 22 | }, 23 | { 24 | token: '!', 25 | collection: Fruits, // Mongo.Collection object means client-side collection 26 | field: 'type', 27 | // set to true to search anywhere in the field, which cannot use an index. 28 | matchAll: true, // 'ba' will match 'bar' and 'baz' first, then 'abacus' 29 | template: Template.clientCollectionPill 30 | } 31 | ] 32 | } 33 | } 34 | }); 35 | 36 | Template.pubsub.events({ 37 | "autocompleteselect textarea": function(e, t, doc) { 38 | console.log("selected ", doc); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /examples/pubsublocal/client/options.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /examples/pubsublocal/client/options.js: -------------------------------------------------------------------------------- 1 | Session.setDefault("position", "top"); 2 | 3 | Template.options.helpers({ 4 | position: function(arg) { 5 | return Session.equals("position", arg); 6 | } 7 | }); 8 | 9 | Template.options.events({ 10 | "change input": function(e, t) { 11 | Session.set(e.target.name, e.target.value); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /examples/pubsublocal/client/pubsublocal.html: -------------------------------------------------------------------------------- 1 | 2 | Server-side autocomplete pub/sub example 3 | 4 | 5 | 6 | 7 | Fork me on GitHub 8 | 9 | 10 |
11 |
12 |
13 | {{> pubsub}} 14 | {{> options}} 15 |
16 |
17 | {{> single}} 18 |
19 |
20 |
21 | 22 | 23 | 49 | 50 | 53 | 54 | 57 | 58 | 61 | 62 | -------------------------------------------------------------------------------- /examples/pubsublocal/client/single.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /examples/pubsublocal/client/single.js: -------------------------------------------------------------------------------- 1 | StandardLegends = new Mongo.Collection(null); 2 | 3 | Template.single.helpers({ 4 | settings: function() { 5 | return { 6 | position: Session.get("position"), 7 | limit: 10, 8 | rules: [ 9 | { 10 | // token: '', 11 | collection: StandardLegends, 12 | field: 'legend', 13 | matchAll: true, 14 | template: Template.standardLegends 15 | } 16 | ] 17 | }; 18 | }, 19 | legends: function() { 20 | return StandardLegends.find(); 21 | } 22 | }); 23 | 24 | [ 25 | { 26 | legend: '110° HOT WATER RETURN', 27 | code: '355', 28 | year: '2007', 29 | color: 'White', 30 | bg: 'Green' 31 | }, 32 | { 33 | legend: '110° HOT WATER RETURN', 34 | code: '360', 35 | year: '1996', 36 | color: 'Black', 37 | bg: 'Yellow' 38 | }, 39 | { 40 | legend: '110° HOT WATER SUPPLY', 41 | code: '361', 42 | year: '2007', 43 | color: 'White', 44 | bg: 'Green' 45 | }, 46 | { 47 | legend: '110° HOT WATER SUPPLY', 48 | code: '356', 49 | year: '1996', 50 | color: 'Black', 51 | bg: 'Yellow' 52 | }, 53 | { 54 | legend: '140° HOT WATER RETURN', 55 | code: '357', 56 | year: '2007', 57 | color: 'White', 58 | bg: 'Green' 59 | }, 60 | { 61 | legend: '140° HOT WATER RETURN', 62 | code: '362', 63 | year: '1996', 64 | color: 'Black', 65 | bg: 'Yellow' 66 | }, 67 | { 68 | legend: '140° HOT WATER SUPPLY', 69 | code: '364', 70 | year: '2007', 71 | color: 'White', 72 | bg: 'Green' 73 | }, 74 | { 75 | legend: '140° HOT WATER SUPPLY', 76 | code: '358', 77 | year: '1996', 78 | color: 'Black', 79 | bg: 'Yellow' 80 | }, 81 | { 82 | legend: 'ACID', 83 | code: '100', 84 | year: '2007', 85 | color: 'Black', 86 | bg: 'Orange' 87 | }, 88 | { 89 | legend: 'ACID', 90 | code: '108', 91 | year: '1996', 92 | color: 'Black', 93 | bg: 'Yellow' 94 | }, 95 | { 96 | legend: 'ACID VENT', 97 | code: '102', 98 | year: '2007', 99 | color: 'Black', 100 | bg: 'Orange' 101 | }, 102 | { 103 | legend: 'ACID VENT', 104 | code: '106', 105 | year: '1996', 106 | color: 'Black', 107 | bg: 'Yellow' 108 | }, 109 | { 110 | legend: 'ACID WASTE', 111 | code: '105', 112 | year: '2007', 113 | color: 'Black', 114 | bg: 'Orange' 115 | }, 116 | { 117 | legend: 'ACID WASTE', 118 | code: '107', 119 | year: '1996', 120 | color: 'Black', 121 | bg: 'Yellow' 122 | }, 123 | { 124 | legend: 'AIR', 125 | code: '111', 126 | year: '2007', 127 | color: 'White', 128 | bg: 'Blue' 129 | }, 130 | { 131 | legend: 'AMMONIA', 132 | code: '115', 133 | year: '2007', 134 | color: 'Black', 135 | bg: 'Orange' 136 | }, 137 | { 138 | legend: 'AMMONIA', 139 | code: '117', 140 | year: '1996', 141 | color: 'Black', 142 | bg: 'Yellow' 143 | }, 144 | { 145 | legend: 'ARGON', 146 | code: '118', 147 | year: '2007', 148 | color: 'White', 149 | bg: 'Green' 150 | }, 151 | { 152 | legend: 'ASBESTOS FREE', 153 | code: '119', 154 | year: '2007', 155 | color: 'White', 156 | bg: 'Blue' 157 | }, 158 | { 159 | legend: 'BOILER BLOW DOWN', 160 | code: '120', 161 | year: '2007', 162 | color: 'White', 163 | bg: 'Green' 164 | }, 165 | { 166 | legend: 'BOILER FEED WATER', 167 | code: '121', 168 | year: '2007', 169 | color: 'White', 170 | bg: 'Green' 171 | }, 172 | { 173 | legend: 'CARBON DIOXIDE', 174 | code: '122', 175 | year: '2007', 176 | color: 'Black', 177 | bg: 'Yellow' 178 | }, 179 | { 180 | legend: 'CARBON DIOXIDE', 181 | code: '124', 182 | year: '2007', 183 | color: 'White', 184 | bg: 'Silver' 185 | }, 186 | { 187 | legend: 'FREE FOOD', 188 | code: '42', 189 | year: '2014', 190 | color: 'Red', 191 | bg: 'White' 192 | }, 193 | { 194 | legend: '', 195 | code: '', 196 | year: '', 197 | color: '', 198 | bg: '' 199 | } 200 | ].forEach(function (obj) { 201 | StandardLegends.insert(obj); 202 | }); 203 | -------------------------------------------------------------------------------- /examples/pubsublocal/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEPLOY_HOSTNAME=galaxy.meteor.com meteor deploy autocomplete.meteorapp.com --settings settings.json 3 | -------------------------------------------------------------------------------- /examples/pubsublocal/lib/collections.js: -------------------------------------------------------------------------------- 1 | BigCollection = new Mongo.Collection('bigcollection'); 2 | -------------------------------------------------------------------------------- /examples/pubsublocal/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-autocomplete-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/runtime": { 8 | "version": "7.11.2", 9 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", 10 | "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", 11 | "requires": { 12 | "regenerator-runtime": "^0.13.4" 13 | } 14 | }, 15 | "jquery": { 16 | "version": "3.5.1", 17 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", 18 | "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" 19 | }, 20 | "meteor-node-stubs": { 21 | "version": "1.0.1", 22 | "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.0.1.tgz", 23 | "integrity": "sha512-I4PE/z7eAl45XEsebHA4pcQbgjqEdK3EBGgiUoIZBi3bMQcMq6blLWZo+WdtK4Or9X4NJOiYWw4GmHiubr3egA==", 24 | "requires": { 25 | "assert": "^1.4.1", 26 | "browserify-zlib": "^0.2.0", 27 | "buffer": "^5.2.1", 28 | "console-browserify": "^1.1.0", 29 | "constants-browserify": "^1.0.0", 30 | "crypto-browserify": "^3.12.0", 31 | "domain-browser": "^1.2.0", 32 | "events": "^3.0.0", 33 | "https-browserify": "^1.0.0", 34 | "os-browserify": "^0.3.0", 35 | "path-browserify": "^1.0.0", 36 | "process": "^0.11.10", 37 | "punycode": "^2.1.1", 38 | "querystring-es3": "^0.2.1", 39 | "readable-stream": "^3.3.0", 40 | "stream-browserify": "^2.0.2", 41 | "stream-http": "^3.0.0", 42 | "string_decoder": "^1.2.0", 43 | "timers-browserify": "^2.0.10", 44 | "tty-browserify": "0.0.1", 45 | "url": "^0.11.0", 46 | "util": "^0.11.1", 47 | "vm-browserify": "^1.1.0" 48 | }, 49 | "dependencies": { 50 | "asn1.js": { 51 | "version": "4.10.1", 52 | "bundled": true, 53 | "requires": { 54 | "bn.js": "^4.0.0", 55 | "inherits": "^2.0.1", 56 | "minimalistic-assert": "^1.0.0" 57 | } 58 | }, 59 | "assert": { 60 | "version": "1.4.1", 61 | "bundled": true, 62 | "requires": { 63 | "util": "0.10.3" 64 | }, 65 | "dependencies": { 66 | "util": { 67 | "version": "0.10.3", 68 | "bundled": true, 69 | "requires": { 70 | "inherits": "2.0.1" 71 | } 72 | } 73 | } 74 | }, 75 | "base64-js": { 76 | "version": "1.3.0", 77 | "bundled": true 78 | }, 79 | "bn.js": { 80 | "version": "4.11.8", 81 | "bundled": true 82 | }, 83 | "brorand": { 84 | "version": "1.1.0", 85 | "bundled": true 86 | }, 87 | "browserify-aes": { 88 | "version": "1.2.0", 89 | "bundled": true, 90 | "requires": { 91 | "buffer-xor": "^1.0.3", 92 | "cipher-base": "^1.0.0", 93 | "create-hash": "^1.1.0", 94 | "evp_bytestokey": "^1.0.3", 95 | "inherits": "^2.0.1", 96 | "safe-buffer": "^5.0.1" 97 | } 98 | }, 99 | "browserify-cipher": { 100 | "version": "1.0.1", 101 | "bundled": true, 102 | "requires": { 103 | "browserify-aes": "^1.0.4", 104 | "browserify-des": "^1.0.0", 105 | "evp_bytestokey": "^1.0.0" 106 | } 107 | }, 108 | "browserify-des": { 109 | "version": "1.0.2", 110 | "bundled": true, 111 | "requires": { 112 | "cipher-base": "^1.0.1", 113 | "des.js": "^1.0.0", 114 | "inherits": "^2.0.1", 115 | "safe-buffer": "^5.1.2" 116 | } 117 | }, 118 | "browserify-rsa": { 119 | "version": "4.0.1", 120 | "bundled": true, 121 | "requires": { 122 | "bn.js": "^4.1.0", 123 | "randombytes": "^2.0.1" 124 | } 125 | }, 126 | "browserify-sign": { 127 | "version": "4.0.4", 128 | "bundled": true, 129 | "requires": { 130 | "bn.js": "^4.1.1", 131 | "browserify-rsa": "^4.0.0", 132 | "create-hash": "^1.1.0", 133 | "create-hmac": "^1.1.2", 134 | "elliptic": "^6.0.0", 135 | "inherits": "^2.0.1", 136 | "parse-asn1": "^5.0.0" 137 | } 138 | }, 139 | "browserify-zlib": { 140 | "version": "0.2.0", 141 | "bundled": true, 142 | "requires": { 143 | "pako": "~1.0.5" 144 | } 145 | }, 146 | "buffer": { 147 | "version": "5.2.1", 148 | "bundled": true, 149 | "requires": { 150 | "base64-js": "^1.0.2", 151 | "ieee754": "^1.1.4" 152 | } 153 | }, 154 | "buffer-xor": { 155 | "version": "1.0.3", 156 | "bundled": true 157 | }, 158 | "builtin-status-codes": { 159 | "version": "3.0.0", 160 | "bundled": true 161 | }, 162 | "cipher-base": { 163 | "version": "1.0.4", 164 | "bundled": true, 165 | "requires": { 166 | "inherits": "^2.0.1", 167 | "safe-buffer": "^5.0.1" 168 | } 169 | }, 170 | "console-browserify": { 171 | "version": "1.1.0", 172 | "bundled": true, 173 | "requires": { 174 | "date-now": "^0.1.4" 175 | } 176 | }, 177 | "constants-browserify": { 178 | "version": "1.0.0", 179 | "bundled": true 180 | }, 181 | "core-util-is": { 182 | "version": "1.0.2", 183 | "bundled": true 184 | }, 185 | "create-ecdh": { 186 | "version": "4.0.3", 187 | "bundled": true, 188 | "requires": { 189 | "bn.js": "^4.1.0", 190 | "elliptic": "^6.0.0" 191 | } 192 | }, 193 | "create-hash": { 194 | "version": "1.2.0", 195 | "bundled": true, 196 | "requires": { 197 | "cipher-base": "^1.0.1", 198 | "inherits": "^2.0.1", 199 | "md5.js": "^1.3.4", 200 | "ripemd160": "^2.0.1", 201 | "sha.js": "^2.4.0" 202 | } 203 | }, 204 | "create-hmac": { 205 | "version": "1.1.7", 206 | "bundled": true, 207 | "requires": { 208 | "cipher-base": "^1.0.3", 209 | "create-hash": "^1.1.0", 210 | "inherits": "^2.0.1", 211 | "ripemd160": "^2.0.0", 212 | "safe-buffer": "^5.0.1", 213 | "sha.js": "^2.4.8" 214 | } 215 | }, 216 | "crypto-browserify": { 217 | "version": "3.12.0", 218 | "bundled": true, 219 | "requires": { 220 | "browserify-cipher": "^1.0.0", 221 | "browserify-sign": "^4.0.0", 222 | "create-ecdh": "^4.0.0", 223 | "create-hash": "^1.1.0", 224 | "create-hmac": "^1.1.0", 225 | "diffie-hellman": "^5.0.0", 226 | "inherits": "^2.0.1", 227 | "pbkdf2": "^3.0.3", 228 | "public-encrypt": "^4.0.0", 229 | "randombytes": "^2.0.0", 230 | "randomfill": "^1.0.3" 231 | } 232 | }, 233 | "date-now": { 234 | "version": "0.1.4", 235 | "bundled": true 236 | }, 237 | "des.js": { 238 | "version": "1.0.0", 239 | "bundled": true, 240 | "requires": { 241 | "inherits": "^2.0.1", 242 | "minimalistic-assert": "^1.0.0" 243 | } 244 | }, 245 | "diffie-hellman": { 246 | "version": "5.0.3", 247 | "bundled": true, 248 | "requires": { 249 | "bn.js": "^4.1.0", 250 | "miller-rabin": "^4.0.0", 251 | "randombytes": "^2.0.0" 252 | } 253 | }, 254 | "domain-browser": { 255 | "version": "1.2.0", 256 | "bundled": true 257 | }, 258 | "elliptic": { 259 | "version": "6.5.3", 260 | "bundled": true, 261 | "requires": { 262 | "bn.js": "^4.4.0", 263 | "brorand": "^1.0.1", 264 | "hash.js": "^1.0.0", 265 | "hmac-drbg": "^1.0.0", 266 | "inherits": "^2.0.1", 267 | "minimalistic-assert": "^1.0.0", 268 | "minimalistic-crypto-utils": "^1.0.0" 269 | } 270 | }, 271 | "events": { 272 | "version": "3.0.0", 273 | "bundled": true 274 | }, 275 | "evp_bytestokey": { 276 | "version": "1.0.3", 277 | "bundled": true, 278 | "requires": { 279 | "md5.js": "^1.3.4", 280 | "safe-buffer": "^5.1.1" 281 | } 282 | }, 283 | "hash-base": { 284 | "version": "3.0.4", 285 | "bundled": true, 286 | "requires": { 287 | "inherits": "^2.0.1", 288 | "safe-buffer": "^5.0.1" 289 | } 290 | }, 291 | "hash.js": { 292 | "version": "1.1.7", 293 | "bundled": true, 294 | "requires": { 295 | "inherits": "^2.0.3", 296 | "minimalistic-assert": "^1.0.1" 297 | }, 298 | "dependencies": { 299 | "inherits": { 300 | "version": "2.0.3", 301 | "bundled": true 302 | } 303 | } 304 | }, 305 | "hmac-drbg": { 306 | "version": "1.0.1", 307 | "bundled": true, 308 | "requires": { 309 | "hash.js": "^1.0.3", 310 | "minimalistic-assert": "^1.0.0", 311 | "minimalistic-crypto-utils": "^1.0.1" 312 | } 313 | }, 314 | "https-browserify": { 315 | "version": "1.0.0", 316 | "bundled": true 317 | }, 318 | "ieee754": { 319 | "version": "1.1.13", 320 | "bundled": true 321 | }, 322 | "inherits": { 323 | "version": "2.0.1", 324 | "bundled": true 325 | }, 326 | "isarray": { 327 | "version": "1.0.0", 328 | "bundled": true 329 | }, 330 | "md5.js": { 331 | "version": "1.3.5", 332 | "bundled": true, 333 | "requires": { 334 | "hash-base": "^3.0.0", 335 | "inherits": "^2.0.1", 336 | "safe-buffer": "^5.1.2" 337 | } 338 | }, 339 | "miller-rabin": { 340 | "version": "4.0.1", 341 | "bundled": true, 342 | "requires": { 343 | "bn.js": "^4.0.0", 344 | "brorand": "^1.0.1" 345 | } 346 | }, 347 | "minimalistic-assert": { 348 | "version": "1.0.1", 349 | "bundled": true 350 | }, 351 | "minimalistic-crypto-utils": { 352 | "version": "1.0.1", 353 | "bundled": true 354 | }, 355 | "os-browserify": { 356 | "version": "0.3.0", 357 | "bundled": true 358 | }, 359 | "pako": { 360 | "version": "1.0.10", 361 | "bundled": true 362 | }, 363 | "parse-asn1": { 364 | "version": "5.1.4", 365 | "bundled": true, 366 | "requires": { 367 | "asn1.js": "^4.0.0", 368 | "browserify-aes": "^1.0.0", 369 | "create-hash": "^1.1.0", 370 | "evp_bytestokey": "^1.0.0", 371 | "pbkdf2": "^3.0.3", 372 | "safe-buffer": "^5.1.1" 373 | } 374 | }, 375 | "path-browserify": { 376 | "version": "1.0.0", 377 | "bundled": true 378 | }, 379 | "pbkdf2": { 380 | "version": "3.0.17", 381 | "bundled": true, 382 | "requires": { 383 | "create-hash": "^1.1.2", 384 | "create-hmac": "^1.1.4", 385 | "ripemd160": "^2.0.1", 386 | "safe-buffer": "^5.0.1", 387 | "sha.js": "^2.4.8" 388 | } 389 | }, 390 | "process": { 391 | "version": "0.11.10", 392 | "bundled": true 393 | }, 394 | "process-nextick-args": { 395 | "version": "2.0.0", 396 | "bundled": true 397 | }, 398 | "public-encrypt": { 399 | "version": "4.0.3", 400 | "bundled": true, 401 | "requires": { 402 | "bn.js": "^4.1.0", 403 | "browserify-rsa": "^4.0.0", 404 | "create-hash": "^1.1.0", 405 | "parse-asn1": "^5.0.0", 406 | "randombytes": "^2.0.1", 407 | "safe-buffer": "^5.1.2" 408 | } 409 | }, 410 | "punycode": { 411 | "version": "2.1.1", 412 | "bundled": true 413 | }, 414 | "querystring": { 415 | "version": "0.2.0", 416 | "bundled": true 417 | }, 418 | "querystring-es3": { 419 | "version": "0.2.1", 420 | "bundled": true 421 | }, 422 | "randombytes": { 423 | "version": "2.1.0", 424 | "bundled": true, 425 | "requires": { 426 | "safe-buffer": "^5.1.0" 427 | } 428 | }, 429 | "randomfill": { 430 | "version": "1.0.4", 431 | "bundled": true, 432 | "requires": { 433 | "randombytes": "^2.0.5", 434 | "safe-buffer": "^5.1.0" 435 | } 436 | }, 437 | "readable-stream": { 438 | "version": "3.3.0", 439 | "bundled": true, 440 | "requires": { 441 | "inherits": "^2.0.3", 442 | "string_decoder": "^1.1.1", 443 | "util-deprecate": "^1.0.1" 444 | }, 445 | "dependencies": { 446 | "inherits": { 447 | "version": "2.0.3", 448 | "bundled": true 449 | } 450 | } 451 | }, 452 | "ripemd160": { 453 | "version": "2.0.2", 454 | "bundled": true, 455 | "requires": { 456 | "hash-base": "^3.0.0", 457 | "inherits": "^2.0.1" 458 | } 459 | }, 460 | "safe-buffer": { 461 | "version": "5.1.2", 462 | "bundled": true 463 | }, 464 | "setimmediate": { 465 | "version": "1.0.5", 466 | "bundled": true 467 | }, 468 | "sha.js": { 469 | "version": "2.4.11", 470 | "bundled": true, 471 | "requires": { 472 | "inherits": "^2.0.1", 473 | "safe-buffer": "^5.0.1" 474 | } 475 | }, 476 | "stream-browserify": { 477 | "version": "2.0.2", 478 | "bundled": true, 479 | "requires": { 480 | "inherits": "~2.0.1", 481 | "readable-stream": "^2.0.2" 482 | }, 483 | "dependencies": { 484 | "readable-stream": { 485 | "version": "2.3.6", 486 | "bundled": true, 487 | "requires": { 488 | "core-util-is": "~1.0.0", 489 | "inherits": "~2.0.3", 490 | "isarray": "~1.0.0", 491 | "process-nextick-args": "~2.0.0", 492 | "safe-buffer": "~5.1.1", 493 | "string_decoder": "~1.1.1", 494 | "util-deprecate": "~1.0.1" 495 | }, 496 | "dependencies": { 497 | "inherits": { 498 | "version": "2.0.3", 499 | "bundled": true 500 | } 501 | } 502 | }, 503 | "string_decoder": { 504 | "version": "1.1.1", 505 | "bundled": true, 506 | "requires": { 507 | "safe-buffer": "~5.1.0" 508 | } 509 | } 510 | } 511 | }, 512 | "stream-http": { 513 | "version": "3.0.0", 514 | "bundled": true, 515 | "requires": { 516 | "builtin-status-codes": "^3.0.0", 517 | "inherits": "^2.0.1", 518 | "readable-stream": "^3.0.6", 519 | "xtend": "^4.0.0" 520 | } 521 | }, 522 | "string_decoder": { 523 | "version": "1.2.0", 524 | "bundled": true, 525 | "requires": { 526 | "safe-buffer": "~5.1.0" 527 | } 528 | }, 529 | "timers-browserify": { 530 | "version": "2.0.10", 531 | "bundled": true, 532 | "requires": { 533 | "setimmediate": "^1.0.4" 534 | } 535 | }, 536 | "tty-browserify": { 537 | "version": "0.0.1", 538 | "bundled": true 539 | }, 540 | "url": { 541 | "version": "0.11.0", 542 | "bundled": true, 543 | "requires": { 544 | "punycode": "1.3.2", 545 | "querystring": "0.2.0" 546 | }, 547 | "dependencies": { 548 | "punycode": { 549 | "version": "1.3.2", 550 | "bundled": true 551 | } 552 | } 553 | }, 554 | "util": { 555 | "version": "0.11.1", 556 | "bundled": true, 557 | "requires": { 558 | "inherits": "2.0.3" 559 | }, 560 | "dependencies": { 561 | "inherits": { 562 | "version": "2.0.3", 563 | "bundled": true 564 | } 565 | } 566 | }, 567 | "util-deprecate": { 568 | "version": "1.0.2", 569 | "bundled": true 570 | }, 571 | "vm-browserify": { 572 | "version": "1.1.0", 573 | "bundled": true 574 | }, 575 | "xtend": { 576 | "version": "4.0.1", 577 | "bundled": true 578 | } 579 | } 580 | }, 581 | "regenerator-runtime": { 582 | "version": "0.13.7", 583 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", 584 | "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" 585 | } 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /examples/pubsublocal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-autocomplete-example", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@babel/runtime": "^7.12.1", 6 | "jquery": "^3.5.1", 7 | "meteor-node-stubs": "^1.0.1" 8 | }, 9 | "scripts": { 10 | "start": "meteor run" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/pubsublocal/packages/autocomplete: -------------------------------------------------------------------------------- 1 | ../../.. -------------------------------------------------------------------------------- /examples/pubsublocal/server/server.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | if (!BigCollection.find().count()) { 3 | // Create a "large" collection with a series of records that area easy to 4 | // predict by a human, but not continuous, so that only some searches will 5 | // match. For example, all 4-letter words that can be typed with the 20 6 | // letters from 'a' to 't'. Furthermore, stuff them in the database in a 7 | // non-alphabetical order, to test how sorting works. 8 | var someLetters = 'tsrqponmlkjihgfedcba'.split(''); 9 | for (var i1 = 0; i1 < someLetters.length; i1++) { 10 | for (var i2 = 0; i2 < someLetters.length; i2++) { 11 | for (var i3 = 0; i3 < someLetters.length; i3++) { 12 | for (var i4 = 0; i4 < someLetters.length; i4++) { 13 | BigCollection.insert({ 14 | _id: i1.toString() + '-' + i2.toString() + '-' + i3.toString() + '-' + i4.toString(), 15 | name: someLetters[i1]+someLetters[i2]+someLetters[i3]+someLetters[i4] 16 | }) 17 | } 18 | } 19 | } 20 | } 21 | } 22 | 23 | // Create an index on the name field of BigCollection 24 | BigCollection._ensureIndex({name: 1}); 25 | }); 26 | 27 | // don't publish anything - the out-of-the-box server code will take care of that 28 | -------------------------------------------------------------------------------- /examples/pubsublocal/upload-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # from http://stackoverflow.com/q/18883103/586086 3 | CMD=`meteor mongo -U autocomplete.meteor.com | tail -1 | sed 's_mongodb://\([a-z0-9\-]*\):\([a-f0-9\-]*\)@\(.*\)/\(.*\)_mongorestore -u \1 -p \2 -h \3 -d \4_'` 4 | echo $CMD 5 | -------------------------------------------------------------------------------- /inputs.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 36 | 37 | 40 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "mizzao:autocomplete", 3 | summary: "Client/server autocompletion designed for Meteor's collections and reactivity", 4 | version: "0.5.1", 5 | git: "https://github.com/mizzao/meteor-autocomplete.git" 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.versionsFrom("1.0"); 10 | 11 | api.use(['blaze', 'templating', 'jquery', 'check', 'tracker'], 'client'); 12 | api.use(['coffeescript@1.0.0 || 2.0.0', 'underscore']); // both 13 | api.use(['mongo', 'ddp']); 14 | 15 | api.use("dandv:caret-position@2.1.1", 'client'); 16 | 17 | // Our files 18 | api.addFiles([ 19 | 'autocomplete.css', 20 | 'inputs.html', 21 | 'autocomplete-client.coffee', 22 | 'templates.coffee' 23 | ], 'client'); 24 | 25 | api.addFiles([ 26 | 'autocomplete-server.coffee' 27 | ], 'server'); 28 | 29 | api.export('Autocomplete', 'server'); 30 | api.export('AutocompleteTest', {testOnly: true}); 31 | }); 32 | 33 | Package.onTest(function(api) { 34 | api.use("mizzao:autocomplete"); 35 | 36 | api.use('coffeescript@2.4.1'); 37 | api.use('mongo'); 38 | api.use('tinytest'); 39 | 40 | api.addFiles('tests/rule_tests.coffee', 'client'); 41 | api.addFiles('tests/regex_tests.coffee', 'client'); 42 | api.addFiles('tests/param_tests.coffee', 'client'); 43 | api.addFiles('tests/security_tests.coffee'); 44 | }); 45 | -------------------------------------------------------------------------------- /templates.coffee: -------------------------------------------------------------------------------- 1 | # Events on template instances, sent to the autocomplete class 2 | acEvents = 3 | "keydown": (e, t) -> t.ac.onKeyDown(e) 4 | "keyup": (e, t) -> t.ac.onKeyUp(e) 5 | "focus": (e, t) -> t.ac.onFocus(e) 6 | "blur": (e, t) -> t.ac.onBlur(e) 7 | 8 | Template.inputAutocomplete.events(acEvents) 9 | Template.textareaAutocomplete.events(acEvents) 10 | 11 | attributes = -> _.omit(@, 'settings') # Render all but the settings parameter 12 | 13 | autocompleteHelpers = { 14 | attributes, 15 | autocompleteContainer: new Template('AutocompleteContainer', -> 16 | ac = new AutoComplete( Blaze.getData().settings ) 17 | # Set the autocomplete object on the parent template instance 18 | this.parentView.templateInstance().ac = ac 19 | 20 | # Set nodes on render in the autocomplete class 21 | this.onViewReady -> 22 | ac.element = this.parentView.firstNode() 23 | ac.$element = $(ac.element) 24 | 25 | return Blaze.With(ac, -> Template._autocompleteContainer) 26 | ) 27 | } 28 | 29 | Template.inputAutocomplete.helpers(autocompleteHelpers) 30 | Template.textareaAutocomplete.helpers(autocompleteHelpers) 31 | 32 | Template._autocompleteContainer.rendered = -> 33 | @data.tmplInst = this 34 | 35 | Template._autocompleteContainer.destroyed = -> 36 | # Meteor._debug "autocomplete destroyed" 37 | @data.teardown() 38 | 39 | ### 40 | List rendering helpers 41 | ### 42 | 43 | Template._autocompleteContainer.events 44 | # t.data is the AutoComplete instance; `this` is the data item 45 | "click .-autocomplete-item": (e, t) -> t.data.onItemClick(this, e) 46 | "mouseenter .-autocomplete-item": (e, t) -> t.data.onItemHover(this, e) 47 | 48 | Template._autocompleteContainer.helpers 49 | empty: -> @filteredList().count() is 0 50 | noMatchTemplate: -> @matchedRule().noMatchTemplate || Template._noMatch 51 | -------------------------------------------------------------------------------- /tests/param_tests.coffee: -------------------------------------------------------------------------------- 1 | Tinytest.add "autocomplete - params - default case insensitive", (test) -> 2 | rule = 3 | field: "foo" 4 | filter = "blah" 5 | limit = 5 6 | 7 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 8 | 9 | test.equal sel.foo.$regex, "^blah" 10 | test.equal sel.foo.$options, "i" 11 | 12 | Tinytest.add "autocomplete - params - limit", (test) -> 13 | rule = 14 | field: "foo" 15 | filter = "blah" 16 | limit = 5 17 | 18 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 19 | 20 | test.equal opts.limit, 5 21 | 22 | Tinytest.add "autocomplete - params - match all", (test) -> 23 | rule = 24 | field: "foo" 25 | matchAll: true 26 | filter = "blah" 27 | limit = 5 28 | 29 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 30 | 31 | test.equal sel.foo.$regex, "blah" 32 | 33 | Tinytest.add "autocomplete - params - replace options", (test) -> 34 | rule = 35 | field: "foo" 36 | options: "" 37 | filter = "blah" 38 | limit = 5 39 | 40 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 41 | 42 | test.equal sel.foo.$regex, "^blah" 43 | test.equal sel.foo.$options, "" 44 | 45 | Tinytest.add "autocomplete - params - no sort if filter empty", (test) -> 46 | rule = 47 | field: "foo" 48 | filter = "" 49 | limit = 5 50 | 51 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 52 | 53 | test.isFalse opts.sort 54 | 55 | Tinytest.add "autocomplete - params - no sort by default", (test) -> 56 | rule = 57 | field: "foo" 58 | filter = "blah" 59 | limit = 5 60 | 61 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 62 | 63 | test.isFalse opts.sort 64 | 65 | Tinytest.add "autocomplete - params - sort if enabled and filter exists", (test) -> 66 | rule = 67 | field: "foo" 68 | sort: true 69 | filter = "blah" 70 | limit = 5 71 | 72 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 73 | 74 | test.equal opts.sort.foo, 1 75 | 76 | Tinytest.add "autocomplete - params - incorporate filter", (test) -> 77 | rule = 78 | field: "foo" 79 | filter: {type: "autocomplete"} 80 | filter = "blah" 81 | limit = 5 82 | 83 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 84 | 85 | test.equal sel.type, "autocomplete" 86 | test.isFalse rule.filter.blah # should not be modified 87 | 88 | Tinytest.add "autocomplete - params - custom selector", (test) -> 89 | rule = 90 | selector: (filter) -> { foo: filter } 91 | filter = "blah" 92 | limit = 5 93 | 94 | [sel, opts] = AutocompleteTest.getFindParams(rule, filter, limit) 95 | 96 | test.equal sel.foo, "blah" 97 | 98 | 99 | -------------------------------------------------------------------------------- /tests/regex_tests.coffee: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | Test that regular expressions match what we think they match. 4 | ### 5 | Tinytest.add "autocomplete - regexp - whole field behavior", (test) -> 6 | rule = {} 7 | 8 | regex = AutocompleteTest.getRegExp(rule) 9 | matches = "hello there".match(regex) 10 | 11 | test.equal matches[2], "hello there" 12 | 13 | Tinytest.add "autocomplete - regexp - whole field behavior - non-English characters", (test) -> 14 | rule = {} 15 | 16 | regex AutocompleteTest.getRegExp(rule) 17 | matches = "上野 上田".match(regex) 18 | 19 | test.equal matches[2], "上野 上田" 20 | 21 | Tinytest.add "autocomplete - regexp - token behavior", (test) -> 22 | rule = { 23 | token: "!" 24 | } 25 | 26 | regex = AutocompleteTest.getRegExp(rule) 27 | matches = "hello !there".match(regex) 28 | 29 | test.equal matches[2], "there" 30 | -------------------------------------------------------------------------------- /tests/rule_tests.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Test that rule validations work properly. 3 | ### 4 | Cause = new Mongo.Collection(null) 5 | 6 | Tinytest.add "autocomplete - rules - vanilla client side collection search", (test) -> 7 | settings = 8 | position: 'bottom' 9 | limit: 10 10 | rules: [ 11 | { 12 | collection: Cause, 13 | field: "name", 14 | matchAll: true, 15 | # template: Template.cause 16 | } 17 | ] 18 | 19 | test.isFalse(AutocompleteTest.isServerSearch(settings.rules[0])) 20 | 21 | new AutoComplete(settings) 22 | test.ok() 23 | 24 | # From https://github.com/mizzao/meteor-autocomplete/issues/36 25 | Tinytest.add "autocomplete - rules - check for collection string with subscription", (test) -> 26 | settings = 27 | position: 'bottom' 28 | limit: 10 29 | rules: [ 30 | { 31 | collection: Cause, 32 | field: "name", 33 | matchAll: true, 34 | subscription: 'causes', 35 | # template: Template.cause 36 | } 37 | ] 38 | 39 | test.throws -> new AutoComplete(settings) 40 | 41 | Tinytest.add "autocomplete - rules - server side collection with default sub", (test) -> 42 | settings = 43 | position: 'bottom' 44 | limit: 10 45 | rules: [ 46 | { 47 | collection: "Cause", 48 | field: "name", 49 | matchAll: true, 50 | # template: Template.cause 51 | } 52 | ] 53 | 54 | test.isTrue(AutocompleteTest.isServerSearch(settings.rules[0])) 55 | 56 | new AutoComplete(settings) 57 | test.ok() 58 | 59 | Tinytest.add "autocomplete - rules - server side collection with custom sub", (test) -> 60 | settings = 61 | position: 'bottom' 62 | limit: 10 63 | rules: [ 64 | { 65 | field: "name", 66 | matchAll: true, 67 | subscription: 'causes', 68 | # template: Template.cause 69 | } 70 | ] 71 | 72 | test.isTrue(AutocompleteTest.isServerSearch(settings.rules[0])) 73 | 74 | new AutoComplete(settings) 75 | test.ok() 76 | -------------------------------------------------------------------------------- /tests/security_tests.coffee: -------------------------------------------------------------------------------- 1 | if Meteor.isServer 2 | @SecureCollection = new Mongo.Collection("secure") 3 | @InsecureCollection = new Mongo.Collection("notsecure") 4 | 5 | if SecureCollection.find().count() is 0 6 | SecureCollection.insert 7 | foo: "bar" 8 | 9 | if InsecureCollection.find().count() is 0 10 | InsecureCollection.insert 11 | foo: "baz" 12 | 13 | InsecureCollection._insecure = true 14 | 15 | Tinytest.add "autocomplete - server - helper functions exported", (test) -> 16 | test.isTrue(Autocomplete) 17 | test.isTrue(Autocomplete.publishCursor) 18 | 19 | if Meteor.isClient 20 | AutoCompleteRecords = AutocompleteTest.records 21 | 22 | Tinytest.addAsync "autocomplete - security - sub insecure collection", (test, next) -> 23 | sub = Meteor.subscribe "autocomplete-recordset", {}, {}, 'InsecureCollection', -> 24 | test.equal AutoCompleteRecords.find().count(), 1 25 | test.equal AutoCompleteRecords.findOne()?.foo, "baz" 26 | sub.stop() 27 | next() 28 | 29 | Tinytest.addAsync "autocomplete - security - sub secure collection", (test, next) -> 30 | sub = Meteor.subscribe "autocomplete-recordset", {}, {}, 'SecureCollection', -> 31 | test.equal AutoCompleteRecords.find().count(), 0 32 | test.isFalse AutoCompleteRecords.findOne() 33 | sub.stop() 34 | next() 35 | 36 | --------------------------------------------------------------------------------