The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Rocket.Chat/packages/meteor-autocomplete/client/autocomplete-client.js

417 lines
9.3 KiB

/* globals Deps */
const AutoCompleteRecords = new Mongo.Collection('autocompleteRecords');
const isServerSearch = function(rule) {
return _.isString(rule.collection);
};
const validateRule = function(rule) {
if ((rule.subscription != null) && !Match.test(rule.collection, String)) {
throw new Error('Collection name must be specified as string for server-side search');
}
if (rule.callback != null) {
return console.warn('autocomplete no longer supports callbacks; use event listeners instead.');
}
};
const isWholeField = function(rule) {
return !rule.token;
};
const getRegExp = function(rule) {
if (!isWholeField(rule)) {
return new RegExp(`(^|\\b|\\s)${ rule.token }([\\w.]*)$`);
} else {
return new RegExp('(^)(.*)$');
}
};
const getFindParams = function(rule, filter, limit) {
const selector = _.extend({}, rule.filter || {});
const options = {
limit
};
if (!filter) {
return [selector, options];
}
if (rule.sort && rule.field) {
const sortspec = {};
sortspec[rule.field] = 1;
options.sort = sortspec;
}
if (_.isFunction(rule.selector)) {
_.extend(selector, rule.selector(filter));
} else {
selector[rule.field] = {
$regex: rule.matchAll ? filter : `^${ filter }`,
$options: typeof rule.options === 'undefined' ? 'i' : rule.options
};
}
return [selector, options];
};
const getField = function(obj, str) {
const string = str.split('.');
string.forEach((key) => {
obj = obj[key];
});
return obj;
};
this.AutoComplete = class {
constructor(settings) {
this.KEYS = [40, 38, 13, 27, 9];
this.limit = settings.limit || 5;
this.position = settings.position || 'bottom';
this.rules = settings.rules;
const rules = this.rules;
Object.keys(rules).forEach((key) => {
const rule = rules[key];
validateRule(rule);
});
this.expressions = (function() {
const results = [];
Object.keys(rules).forEach((key) => {
const rule = rules[key];
results.push(getRegExp(rule));
});
return results;
});
this.matched = -1;
this.loaded = true;
this.ruleDep = new Deps.Dependency;
this.filterDep = new Deps.Dependency;
this.loadingDep = new Deps.Dependency;
this.sub = null;
this.comp = Deps.autorun((function(_this) {
return function() {
let filter, options, ref1, ref2, selector, subName;
if ((ref1 = _this.sub) != null) {
ref1.stop();
}
if (!((rule = _this.matchedRule()) && (filter = _this.getFilter()) !== null)) {
return;
}
if (!isServerSearch(rule)) {
_this.setLoaded(true);
return;
}
ref2 = getFindParams(rule, filter, _this.limit), selector = ref2[0], options = ref2[1];
_this.setLoaded(false);
subName = rule.subscription || 'autocomplete-recordset';
return _this.sub = Meteor.subscribe(subName, selector, options, rule.collection, function() {
return _this.setLoaded(true);
});
};
})(this));
}
teardown() {
return this.comp.stop();
}
matchedRule() {
this.ruleDep.depend();
if (this.matched >= 0) {
return this.rules[this.matched];
} else {
return null;
}
}
setMatchedRule(i) {
this.matched = i;
return this.ruleDep.changed();
}
getFilter() {
this.filterDep.depend();
return this.filter;
}
setFilter(x) {
this.filter = x;
this.filterDep.changed();
return this.filter;
}
isLoaded() {
this.loadingDep.depend();
return this.loaded;
}
setLoaded(val) {
if (val === this.loaded) {
return;
}
this.loaded = val;
return this.loadingDep.changed();
}
onKeyUp() {
let breakLoop, i, matches, results, startpos, val;
if (!this.$element) {
return;
}
startpos = this.element.selectionStart;
val = this.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;
results = [];
while (i < this.expressions.length) {
matches = val.match(this.expressions[i]);
if (!matches && this.matched === i) {
this.setMatchedRule(-1);
breakLoop = true;
}
if (matches && this.matched === -1) {
this.setMatchedRule(i);
breakLoop = true;
}
if (matches && this.filter !== matches[2]) {
this.setFilter(matches[2]);
breakLoop = true;
}
if (breakLoop) {
break;
}
results.push(i++);
}
return results;
}
onKeyDown(e) {
if (this.matched === -1 || (this.constructor.KEYS.indexOf(e.keyCode) < 0)) {
return;
}
switch (e.keyCode) {
case 9:
case 13:
if (this.select()) {
e.preventDefault();
e.stopPropagation();
}
break;
case 40:
e.preventDefault();
this.next();
break;
case 38:
e.preventDefault();
this.prev();
break;
case 27:
this.$element.blur();
this.hideList();
}
}
onFocus() {
return Meteor.defer((function(_this) {
return function() {
return _this.onKeyUp();
};
})(this));
}
onBlur() {
return Meteor.setTimeout((function(_this) {
return function() {
return _this.hideList();
};
}(this)), 500);
}
onItemClick(doc, e) {
return this.processSelection(doc, this.rules[this.matched]);
}
onItemHover(doc, e) {
this.tmplInst.$('.-autocomplete-item').removeClass('selected');
return $(e.target).closest('.-autocomplete-item').addClass('selected');
}
filteredList() {
let filter, options, ref, rule, selector;
filter = this.getFilter();
if (this.matched === -1) {
return null;
}
rule = this.rules[this.matched];
if (!(rule.token || filter)) {
return null;
}
ref = getFindParams(rule, filter, this.limit), selector = ref[0], options = ref[1];
Meteor.defer((function(_this) {
return function() {
return _this.ensureSelection();
};
})(this));
if (isServerSearch(rule)) {
return AutoCompleteRecords.find({}, options);
}
return rule.collection.find(selector, options);
}
isShowing() {
let rule, showing;
rule = this.matchedRule();
showing = (rule != null) && (rule.token || this.getFilter());
if (showing) {
Meteor.defer((function(_this) {
return function() {
_this.positionContainer();
return _this.ensureSelection();
};
})(this));
}
return showing;
}
select() {
let doc, node;
node = this.tmplInst.find('.-autocomplete-item.selected');
if (node == null) {
return false;
}
doc = Blaze.getData(node);
if (!doc) {
return false;
}
this.processSelection(doc, this.rules[this.matched]);
return true;
}
processSelection(doc, rule) {
let replacement;
replacement = getField(doc, rule.field);
if (!isWholeField(rule)) {
this.replace(replacement, rule);
this.hideList();
} else {
this.setText(replacement);
this.onBlur();
}
this.$element.trigger('autocompleteselect', doc);
}
replace(replacement) {
let finalFight, fullStuff, newPosition, posfix, separator, startpos, val;
startpos = this.element.selectionStart;
fullStuff = this.getText();
val = fullStuff.substring(0, startpos);
val = val.replace(this.expressions[this.matched], `$1${ this.rules[this.matched].token }${ replacement }`);
posfix = fullStuff.substring(startpos, fullStuff.length);
separator = (posfix.match(/^\s/) ? '' : ' ');
finalFight = val + separator + posfix;
this.setText(finalFight);
newPosition = val.length + 1;
this.element.setSelectionRange(newPosition, newPosition);
}
hideList() {
this.setMatchedRule(-1);
return this.setFilter(null);
}
getText() {
return this.$element.val() || this.$element.text();
}
setText(text) {
if (this.$element.is('input,textarea')) {
return this.$element.val(text);
} else {
return this.$element.html(text);
}
}
/*
Rendering functions
*/
positionContainer() {
let offset, pos, position, rule;
position = this.$element.position();
rule = this.matchedRule();
offset = getCaretCoordinates(this.element, this.element.selectionStart);
if ((rule != null) && isWholeField(rule)) {
pos = {
left: position.left,
width: this.$element.outerWidth()
};
} else {
pos = {
left: position.left + offset.left
};
}
if (this.position === 'top') {
pos.bottom = this.$element.offsetParent().height() - position.top - offset.top;
} else {
pos.top = position.top + offset.top + parseInt(this.$element.css('font-size'));
}
return this.tmplInst.$('.-autocomplete-container').css(pos);
}
ensureSelection() {
let selectedItem;
selectedItem = this.tmplInst.$('.-autocomplete-item.selected');
if (!selectedItem.length) {
return this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected');
}
}
next() {
let currentItem, next;
currentItem = this.tmplInst.$('.-autocomplete-item.selected');
if (!currentItem.length) {
return;
}
currentItem.removeClass('selected');
next = currentItem.next();
if (next.length) {
return next.addClass('selected');
} else {
return this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected');
}
}
prev() {
let currentItem, prev;
currentItem = this.tmplInst.$('.-autocomplete-item.selected');
if (!currentItem.length) {
return;
}
currentItem.removeClass('selected');
prev = currentItem.prev();
if (prev.length) {
return prev.addClass('selected');
} else {
return this.tmplInst.$('.-autocomplete-item:last-child').addClass('selected');
}
}
currentTemplate() {
return this.rules[this.matched].template;
}
};
const AutocompleteTest = {
records: AutoCompleteRecords,
getRegExp,
getFindParams
};