parent
7e42466d53
commit
8bb0eab169
@ -0,0 +1,367 @@ |
|||||||
|
AutoCompleteRecords = new Mongo.Collection("autocompleteRecords") |
||||||
|
|
||||||
|
isServerSearch = (rule) -> _.isString(rule.collection) |
||||||
|
|
||||||
|
validateRule = (rule) -> |
||||||
|
if rule.subscription? and not Match.test(rule.collection, String) |
||||||
|
throw new Error("Collection name must be specified as string for server-side search") |
||||||
|
|
||||||
|
# XXX back-compat message, to be removed |
||||||
|
if rule.callback? |
||||||
|
console.warn("autocomplete no longer supports callbacks; use event listeners instead.") |
||||||
|
|
||||||
|
isWholeField = (rule) -> |
||||||
|
# either '' or null both count as whole field. |
||||||
|
return !rule.token |
||||||
|
|
||||||
|
getRegExp = (rule) -> |
||||||
|
unless isWholeField(rule) |
||||||
|
# Expressions for the range from the last word break to the current cursor position |
||||||
|
new RegExp('(^|\\b|\\s)' + rule.token + '([\\w.]*)$') |
||||||
|
else |
||||||
|
# Whole-field behavior - word characters or spaces |
||||||
|
new RegExp('(^)(.*)$') |
||||||
|
|
||||||
|
getFindParams = (rule, filter, limit) -> |
||||||
|
# This is a different 'filter' - the selector from the settings |
||||||
|
# We need to extend so that we don't copy over rule.filter |
||||||
|
selector = _.extend({}, rule.filter || {}) |
||||||
|
options = { limit: limit } |
||||||
|
|
||||||
|
# Match anything, no sort, limit X |
||||||
|
return [ selector, options ] unless filter |
||||||
|
|
||||||
|
if rule.sort and rule.field |
||||||
|
sortspec = {} |
||||||
|
# Only sort if there is a filter, for faster performance on a match of anything |
||||||
|
sortspec[rule.field] = 1 |
||||||
|
options.sort = sortspec |
||||||
|
|
||||||
|
if _.isFunction(rule.selector) |
||||||
|
# Custom selector |
||||||
|
_.extend(selector, rule.selector(filter)) |
||||||
|
else |
||||||
|
selector[rule.field] = { |
||||||
|
$regex: if rule.matchAll then filter else "^" + filter |
||||||
|
# default is case insensitive search - empty string is not the same as undefined! |
||||||
|
$options: if (typeof rule.options is 'undefined') then 'i' else rule.options |
||||||
|
} |
||||||
|
|
||||||
|
return [ selector, options ] |
||||||
|
|
||||||
|
getField = (obj, str) -> |
||||||
|
obj = obj[key] for key in str.split(".") |
||||||
|
return obj |
||||||
|
|
||||||
|
class @AutoComplete |
||||||
|
|
||||||
|
@KEYS: [ |
||||||
|
40, # DOWN |
||||||
|
38, # UP |
||||||
|
13, # ENTER |
||||||
|
27, # ESCAPE |
||||||
|
9 # TAB |
||||||
|
] |
||||||
|
|
||||||
|
constructor: (settings) -> |
||||||
|
@limit = settings.limit || 5 |
||||||
|
@position = settings.position || "bottom" |
||||||
|
|
||||||
|
@rules = settings.rules |
||||||
|
validateRule(rule) for rule in @rules |
||||||
|
|
||||||
|
@expressions = (getRegExp(rule) for rule in @rules) |
||||||
|
|
||||||
|
@matched = -1 |
||||||
|
@loaded = true |
||||||
|
|
||||||
|
# Reactive dependencies for current matching rule and filter |
||||||
|
@ruleDep = new Deps.Dependency |
||||||
|
@filterDep = new Deps.Dependency |
||||||
|
@loadingDep = new Deps.Dependency |
||||||
|
|
||||||
|
# autosubscribe to the record set published by the server based on the filter |
||||||
|
# This will tear down server subscriptions when they are no longer being used. |
||||||
|
@sub = null |
||||||
|
@comp = Deps.autorun => |
||||||
|
# Stop any existing sub immediately, don't wait |
||||||
|
@sub?.stop() |
||||||
|
|
||||||
|
return unless (rule = @matchedRule()) and (filter = @getFilter()) isnt null |
||||||
|
|
||||||
|
# subscribe only for server-side collections |
||||||
|
unless isServerSearch(rule) |
||||||
|
@setLoaded(true) # Immediately loaded |
||||||
|
return |
||||||
|
|
||||||
|
[ selector, options ] = getFindParams(rule, filter, @limit) |
||||||
|
|
||||||
|
# console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field |
||||||
|
@setLoaded(false) |
||||||
|
subName = rule.subscription || "autocomplete-recordset" |
||||||
|
@sub = Meteor.subscribe(subName, |
||||||
|
selector, options, rule.collection, => @setLoaded(true)) |
||||||
|
|
||||||
|
teardown: -> |
||||||
|
# Stop the reactive computation we started for this autocomplete instance |
||||||
|
@comp.stop() |
||||||
|
|
||||||
|
# reactive getters and setters for @filter and the currently matched rule |
||||||
|
matchedRule: -> |
||||||
|
@ruleDep.depend() |
||||||
|
if @matched >= 0 then @rules[@matched] else null |
||||||
|
|
||||||
|
setMatchedRule: (i) -> |
||||||
|
@matched = i |
||||||
|
@ruleDep.changed() |
||||||
|
|
||||||
|
getFilter: -> |
||||||
|
@filterDep.depend() |
||||||
|
return @filter |
||||||
|
|
||||||
|
setFilter: (x) -> |
||||||
|
@filter = x |
||||||
|
@filterDep.changed() |
||||||
|
return @filter |
||||||
|
|
||||||
|
isLoaded: -> |
||||||
|
@loadingDep.depend() |
||||||
|
return @loaded |
||||||
|
|
||||||
|
setLoaded: (val) -> |
||||||
|
return if val is @loaded # Don't cause redraws unnecessarily |
||||||
|
@loaded = val |
||||||
|
@loadingDep.changed() |
||||||
|
|
||||||
|
onKeyUp: -> |
||||||
|
return unless @$element # Don't try to do this while loading |
||||||
|
startpos = @element.selectionStart |
||||||
|
val = @getText().substring(0, startpos) |
||||||
|
|
||||||
|
### |
||||||
|
Matching on multiple expressions. |
||||||
|
We always go from a matched state to an unmatched one |
||||||
|
before going to a different matched one. |
||||||
|
### |
||||||
|
i = 0 |
||||||
|
breakLoop = false |
||||||
|
while i < @expressions.length |
||||||
|
matches = val.match(@expressions[i]) |
||||||
|
|
||||||
|
# matching -> not matching |
||||||
|
if not matches and @matched is i |
||||||
|
@setMatchedRule(-1) |
||||||
|
breakLoop = true |
||||||
|
|
||||||
|
# not matching -> matching |
||||||
|
if matches and @matched is -1 |
||||||
|
@setMatchedRule(i) |
||||||
|
breakLoop = true |
||||||
|
|
||||||
|
# Did filter change? |
||||||
|
if matches and @filter isnt matches[2] |
||||||
|
@setFilter(matches[2]) |
||||||
|
breakLoop = true |
||||||
|
|
||||||
|
break if breakLoop |
||||||
|
i++ |
||||||
|
|
||||||
|
onKeyDown: (e) -> |
||||||
|
return if @matched is -1 or (@constructor.KEYS.indexOf(e.keyCode) < 0) |
||||||
|
|
||||||
|
switch e.keyCode |
||||||
|
when 9, 13 # TAB, ENTER |
||||||
|
if @select() # Don't jump fields or submit if select successful |
||||||
|
e.preventDefault() |
||||||
|
e.stopPropagation() |
||||||
|
# preventDefault needed below to avoid moving cursor when selecting |
||||||
|
when 40 # DOWN |
||||||
|
e.preventDefault() |
||||||
|
@next() |
||||||
|
when 38 # UP |
||||||
|
e.preventDefault() |
||||||
|
@prev() |
||||||
|
when 27 # ESCAPE |
||||||
|
@$element.blur() |
||||||
|
@hideList() |
||||||
|
|
||||||
|
return |
||||||
|
|
||||||
|
onFocus: -> |
||||||
|
# We need to run onKeyUp after the focus resolves, |
||||||
|
# or the caret position (selectionStart) will not be correct |
||||||
|
Meteor.defer => @onKeyUp() |
||||||
|
|
||||||
|
onBlur: -> |
||||||
|
# We need to delay this so click events work |
||||||
|
# TODO this is a bit of a hack; see if we can't be smarter |
||||||
|
Meteor.setTimeout => |
||||||
|
@hideList() |
||||||
|
, 500 |
||||||
|
|
||||||
|
onItemClick: (doc, e) => @processSelection(doc, @rules[@matched]) |
||||||
|
|
||||||
|
onItemHover: (doc, e) -> |
||||||
|
@tmplInst.$(".-autocomplete-item").removeClass("selected") |
||||||
|
$(e.target).closest(".-autocomplete-item").addClass("selected") |
||||||
|
|
||||||
|
filteredList: -> |
||||||
|
# @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered |
||||||
|
filter = @getFilter() # Reactively depend on the filter |
||||||
|
return null if @matched is -1 |
||||||
|
|
||||||
|
rule = @rules[@matched] |
||||||
|
# Don't display list unless we have a token or a filter (or both) |
||||||
|
# Single field: nothing displayed until something is typed |
||||||
|
return null unless rule.token or filter |
||||||
|
|
||||||
|
[ selector, options ] = getFindParams(rule, filter, @limit) |
||||||
|
|
||||||
|
Meteor.defer => @ensureSelection() |
||||||
|
|
||||||
|
# if server collection, the server has already done the filtering work |
||||||
|
return AutoCompleteRecords.find({}, options) if isServerSearch(rule) |
||||||
|
|
||||||
|
# Otherwise, search on client |
||||||
|
return rule.collection.find(selector, options) |
||||||
|
|
||||||
|
isShowing: -> |
||||||
|
rule = @matchedRule() |
||||||
|
# Same rules as above |
||||||
|
showing = rule? and (rule.token or @getFilter()) |
||||||
|
|
||||||
|
# Do this after the render |
||||||
|
if showing |
||||||
|
Meteor.defer => |
||||||
|
@positionContainer() |
||||||
|
@ensureSelection() |
||||||
|
|
||||||
|
return showing |
||||||
|
|
||||||
|
# Replace text with currently selected item |
||||||
|
select: -> |
||||||
|
node = @tmplInst.find(".-autocomplete-item.selected") |
||||||
|
return false unless node? |
||||||
|
doc = Blaze.getData(node) |
||||||
|
return false unless doc # Don't select if nothing matched |
||||||
|
|
||||||
|
@processSelection(doc, @rules[@matched]) |
||||||
|
return true |
||||||
|
|
||||||
|
processSelection: (doc, rule) -> |
||||||
|
replacement = getField(doc, rule.field) |
||||||
|
|
||||||
|
unless isWholeField(rule) |
||||||
|
@replace(replacement, rule) |
||||||
|
@hideList() |
||||||
|
|
||||||
|
else |
||||||
|
# Empty string or doesn't exist? |
||||||
|
# Single-field replacement: replace whole field |
||||||
|
@setText(replacement) |
||||||
|
|
||||||
|
# Field retains focus, but list is hidden unless another key is pressed |
||||||
|
# Must be deferred or onKeyUp will trigger and match again |
||||||
|
# TODO this is a hack; see above |
||||||
|
@onBlur() |
||||||
|
|
||||||
|
@$element.trigger("autocompleteselect", doc) |
||||||
|
return |
||||||
|
|
||||||
|
# Replace the appropriate region |
||||||
|
replace: (replacement) -> |
||||||
|
startpos = @element.selectionStart |
||||||
|
fullStuff = @getText() |
||||||
|
val = fullStuff.substring(0, startpos) |
||||||
|
val = val.replace(@expressions[@matched], "$1" + @rules[@matched].token + replacement) |
||||||
|
posfix = fullStuff.substring(startpos, fullStuff.length) |
||||||
|
separator = (if posfix.match(/^\s/) then "" else " ") |
||||||
|
finalFight = val + separator + posfix |
||||||
|
@setText finalFight |
||||||
|
|
||||||
|
newPosition = val.length + 1 |
||||||
|
@element.setSelectionRange(newPosition, newPosition) |
||||||
|
return |
||||||
|
|
||||||
|
hideList: -> |
||||||
|
@setMatchedRule(-1) |
||||||
|
@setFilter(null) |
||||||
|
|
||||||
|
getText: -> |
||||||
|
return @$element.val() || @$element.text() |
||||||
|
|
||||||
|
setText: (text) -> |
||||||
|
if @$element.is("input,textarea") |
||||||
|
@$element.val(text) |
||||||
|
else |
||||||
|
@$element.html(text) |
||||||
|
|
||||||
|
### |
||||||
|
Rendering functions |
||||||
|
### |
||||||
|
positionContainer: -> |
||||||
|
# First render; Pick the first item and set css whenever list gets shown |
||||||
|
position = @$element.position() |
||||||
|
|
||||||
|
rule = @matchedRule() |
||||||
|
|
||||||
|
offset = getCaretCoordinates(@element, @element.selectionStart) |
||||||
|
|
||||||
|
# In whole-field positioning, we don't move the container and make it the |
||||||
|
# full width of the field. |
||||||
|
if rule? and isWholeField(rule) |
||||||
|
pos = |
||||||
|
left: position.left |
||||||
|
width: @$element.outerWidth() # position.offsetWidth |
||||||
|
else # Normal positioning, at token word |
||||||
|
pos = |
||||||
|
left: position.left + offset.left |
||||||
|
|
||||||
|
# Position menu from top (above) or from bottom of caret (below, default) |
||||||
|
if @position is "top" |
||||||
|
pos.bottom = @$element.offsetParent().height() - position.top - offset.top |
||||||
|
else |
||||||
|
pos.top = position.top + offset.top + parseInt(@$element.css('font-size')) |
||||||
|
|
||||||
|
@tmplInst.$(".-autocomplete-container").css(pos) |
||||||
|
|
||||||
|
ensureSelection : -> |
||||||
|
# Re-render; make sure selected item is something in the list or none if list empty |
||||||
|
selectedItem = @tmplInst.$(".-autocomplete-item.selected") |
||||||
|
|
||||||
|
unless selectedItem.length |
||||||
|
# Select anything |
||||||
|
@tmplInst.$(".-autocomplete-item:first-child").addClass("selected") |
||||||
|
|
||||||
|
# Select next item in list |
||||||
|
next: -> |
||||||
|
currentItem = @tmplInst.$(".-autocomplete-item.selected") |
||||||
|
return unless currentItem.length # Don't try to iterate an empty list |
||||||
|
currentItem.removeClass("selected") |
||||||
|
|
||||||
|
next = currentItem.next() |
||||||
|
if next.length |
||||||
|
next.addClass("selected") |
||||||
|
else # End of list or lost selection; Go back to first item |
||||||
|
@tmplInst.$(".-autocomplete-item:first-child").addClass("selected") |
||||||
|
|
||||||
|
# Select previous item in list |
||||||
|
prev: -> |
||||||
|
currentItem = @tmplInst.$(".-autocomplete-item.selected") |
||||||
|
return unless currentItem.length # Don't try to iterate an empty list |
||||||
|
currentItem.removeClass("selected") |
||||||
|
|
||||||
|
prev = currentItem.prev() |
||||||
|
if prev.length |
||||||
|
prev.addClass("selected") |
||||||
|
else # Beginning of list or lost selection; Go to end of list |
||||||
|
@tmplInst.$(".-autocomplete-item:last-child").addClass("selected") |
||||||
|
|
||||||
|
# This doesn't need to be reactive because list already changes reactively |
||||||
|
# and will cause all of the items to re-render anyway |
||||||
|
currentTemplate: -> @rules[@matched].template |
||||||
|
|
||||||
|
AutocompleteTest = |
||||||
|
records: AutoCompleteRecords |
||||||
|
getRegExp: getRegExp |
||||||
|
getFindParams: getFindParams |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
class Autocomplete |
||||||
|
@publishCursor: (cursor, sub) -> |
||||||
|
# This also attaches an onStop callback to sub, so we don't need to worry about that. |
||||||
|
# https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js |
||||||
|
Mongo.Collection._publishCursor(cursor, sub, "autocompleteRecords") |
||||||
|
|
||||||
|
Meteor.publish 'autocomplete-recordset', (selector, options, collName) -> |
||||||
|
collection = global[collName] |
||||||
|
unless collection |
||||||
|
throw new Error(collName + ' is not defined on the global namespace of the server.') |
||||||
|
|
||||||
|
# This is a semi-documented Meteor feature: |
||||||
|
# https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js |
||||||
|
unless collection._isInsecure() |
||||||
|
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.') |
||||||
|
return [] # We need this for the subscription to be marked ready |
||||||
|
|
||||||
|
# guard against client-side DOS: hard limit to 50 |
||||||
|
options.limit = Math.min(50, Math.abs(options.limit)) if options.limit |
||||||
|
|
||||||
|
# Push this into our own collection on the client so they don't interfere with other publications of the named collection. |
||||||
|
# This also stops the observer automatically when the subscription is stopped. |
||||||
|
Autocomplete.publishCursor( collection.find(selector, options), this) |
||||||
|
|
||||||
|
# Mark the subscription ready after the initial addition of documents. |
||||||
|
this.ready() |
||||||
|
|
||||||
@ -0,0 +1,27 @@ |
|||||||
|
.-autocomplete-container { |
||||||
|
position: absolute; |
||||||
|
background: white; |
||||||
|
border: 1px solid #DDD; |
||||||
|
border-radius: 3px; |
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); |
||||||
|
min-width: 180px; |
||||||
|
z-index: 1000; |
||||||
|
} |
||||||
|
|
||||||
|
.-autocomplete-list { |
||||||
|
list-style: none; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.-autocomplete-item { |
||||||
|
display: block; |
||||||
|
padding: 5px 10px; |
||||||
|
border-bottom: 1px solid #DDD; |
||||||
|
} |
||||||
|
|
||||||
|
.-autocomplete-item.selected { |
||||||
|
color: white; |
||||||
|
background: #4183C4; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
<template name="inputAutocomplete"> |
||||||
|
<input type="text" {{attributes}}> |
||||||
|
{{> autocompleteContainer}} |
||||||
|
</template> |
||||||
|
|
||||||
|
<template name="textareaAutocomplete"> |
||||||
|
<textarea {{attributes}}>{{> UI.contentBlock}}</textarea> |
||||||
|
{{> autocompleteContainer}} |
||||||
|
</template> |
||||||
|
|
||||||
|
<template name="_autocompleteContainer"> |
||||||
|
{{#if isShowing}} |
||||||
|
<div class='-autocomplete-container'> |
||||||
|
{{#if isLoaded}} |
||||||
|
{{#unless empty}} |
||||||
|
<ul class='-autocomplete-list'> |
||||||
|
{{#each filteredList}} |
||||||
|
<li class="-autocomplete-item"> |
||||||
|
{{#with ../currentTemplate }} |
||||||
|
{{#with ..}} {{! original 'data' context to itemTemplate}} |
||||||
|
{{> ..}} {{! return value from itemTemplate }} |
||||||
|
{{/with}} |
||||||
|
{{/with}} |
||||||
|
</li> |
||||||
|
{{/each}} |
||||||
|
</ul> |
||||||
|
{{else}} |
||||||
|
{{> noMatchTemplate }} |
||||||
|
{{/unless}} |
||||||
|
{{else}} |
||||||
|
{{> loading}} |
||||||
|
{{/if}} |
||||||
|
</div> |
||||||
|
{{/if}} |
||||||
|
</template> |
||||||
|
|
||||||
|
<template name="_noMatch"> |
||||||
|
(<i>no matches</i>) |
||||||
|
</template> |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
Package.describe({ |
||||||
|
name: "mizzao:autocomplete", |
||||||
|
summary: "Client/server autocompletion designed for Meteor's collections and reactivity", |
||||||
|
version: "0.5.1", |
||||||
|
git: "https://github.com/mizzao/meteor-autocomplete.git" |
||||||
|
}); |
||||||
|
|
||||||
|
Package.onUse(function (api) { |
||||||
|
api.versionsFrom("1.0"); |
||||||
|
|
||||||
|
api.use(['blaze', 'templating', 'jquery'], 'client'); |
||||||
|
api.use(['coffeescript', 'underscore']); // both
|
||||||
|
api.use(['mongo', 'ddp']); |
||||||
|
|
||||||
|
api.use("dandv:caret-position@2.1.0-3", 'client'); |
||||||
|
|
||||||
|
// Our files
|
||||||
|
api.addFiles([ |
||||||
|
'autocomplete.css', |
||||||
|
'inputs.html', |
||||||
|
'autocomplete-client.coffee', |
||||||
|
'templates.coffee' |
||||||
|
], 'client'); |
||||||
|
|
||||||
|
api.addFiles([ |
||||||
|
'autocomplete-server.coffee' |
||||||
|
], 'server'); |
||||||
|
|
||||||
|
api.export('Autocomplete', 'server'); |
||||||
|
api.export('AutocompleteTest', {testOnly: true}); |
||||||
|
}); |
||||||
|
|
||||||
|
Package.onTest(function(api) { |
||||||
|
api.use("mizzao:autocomplete"); |
||||||
|
|
||||||
|
api.use('coffeescript'); |
||||||
|
api.use('mongo'); |
||||||
|
api.use('tinytest'); |
||||||
|
|
||||||
|
api.addFiles('tests/rule_tests.coffee', 'client'); |
||||||
|
api.addFiles('tests/regex_tests.coffee', 'client'); |
||||||
|
api.addFiles('tests/param_tests.coffee', 'client'); |
||||||
|
api.addFiles('tests/security_tests.coffee'); |
||||||
|
}); |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
# Events on template instances, sent to the autocomplete class |
||||||
|
acEvents = |
||||||
|
"keydown": (e, t) -> t.ac.onKeyDown(e) |
||||||
|
"keyup": (e, t) -> t.ac.onKeyUp(e) |
||||||
|
"focus": (e, t) -> t.ac.onFocus(e) |
||||||
|
"blur": (e, t) -> t.ac.onBlur(e) |
||||||
|
|
||||||
|
Template.inputAutocomplete.events(acEvents) |
||||||
|
Template.textareaAutocomplete.events(acEvents) |
||||||
|
|
||||||
|
attributes = -> _.omit(@, 'settings') # Render all but the settings parameter |
||||||
|
|
||||||
|
autocompleteHelpers = { |
||||||
|
attributes, |
||||||
|
autocompleteContainer: new Template('AutocompleteContainer', -> |
||||||
|
ac = new AutoComplete( Blaze.getData().settings ) |
||||||
|
# Set the autocomplete object on the parent template instance |
||||||
|
this.parentView.templateInstance().ac = ac |
||||||
|
|
||||||
|
# Set nodes on render in the autocomplete class |
||||||
|
this.onViewReady -> |
||||||
|
ac.element = this.parentView.firstNode() |
||||||
|
ac.$element = $(ac.element) |
||||||
|
|
||||||
|
return Blaze.With(ac, -> Template._autocompleteContainer) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
Template.inputAutocomplete.helpers(autocompleteHelpers) |
||||||
|
Template.textareaAutocomplete.helpers(autocompleteHelpers) |
||||||
|
|
||||||
|
Template._autocompleteContainer.rendered = -> |
||||||
|
@data.tmplInst = this |
||||||
|
|
||||||
|
Template._autocompleteContainer.destroyed = -> |
||||||
|
# Meteor._debug "autocomplete destroyed" |
||||||
|
@data.teardown() |
||||||
|
|
||||||
|
### |
||||||
|
List rendering helpers |
||||||
|
### |
||||||
|
|
||||||
|
Template._autocompleteContainer.events |
||||||
|
# t.data is the AutoComplete instance; `this` is the data item |
||||||
|
"click .-autocomplete-item": (e, t) -> t.data.onItemClick(this, e) |
||||||
|
"mouseenter .-autocomplete-item": (e, t) -> t.data.onItemHover(this, e) |
||||||
|
|
||||||
|
Template._autocompleteContainer.helpers |
||||||
|
empty: -> @filteredList().count() is 0 |
||||||
|
noMatchTemplate: -> @matchedRule().noMatchTemplate || Template._noMatch |
||||||
Loading…
Reference in new issue