Fix loading of mizzao’s package

pull/4129/head
Rodrigo Nascimento 9 years ago
parent 7e42466d53
commit 8bb0eab169
No known key found for this signature in database
GPG Key ID: 2C85B3AFE75D23F9
  1. 367
      packages/meteor-autocomplete/autocomplete-client.coffee
  2. 27
      packages/meteor-autocomplete/autocomplete-server.coffee
  3. 27
      packages/meteor-autocomplete/autocomplete.css
  4. 39
      packages/meteor-autocomplete/inputs.html
  5. 44
      packages/meteor-autocomplete/package.js
  6. 50
      packages/meteor-autocomplete/templates.coffee
  7. 15
      packages/rocketchat-theme/assets/stylesheets/base.less

@ -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

@ -577,18 +577,9 @@ textarea[disabled] {
font-size: 14px;
padding: 8px 8px;
}
> i {
visibility: hidden;
&:after {
content: " ";
visibility: visible;
background-image: url('images/logo/loading.gif');
background-position: center;
background-repeat: no-repeat;
display: block;
height: 40px;
margin-bottom: 12px;
}
.loading {
position: relative;
min-height: 60px;
}
}

Loading…
Cancel
Save