mirror of
https://github.com/discourse/discourse.git
synced 2025-05-23 10:01:11 +08:00
ES6: user-search lib and autocomplete. Cancels many promises rather than
leaving them as pending forever.
This commit is contained in:
420
app/assets/javascripts/discourse/lib/autocomplete.js.es6
Normal file
420
app/assets/javascripts/discourse/lib/autocomplete.js.es6
Normal file
@ -0,0 +1,420 @@
|
||||
/**
|
||||
This is a jQuery plugin to support autocompleting values in our text fields.
|
||||
|
||||
@module $.fn.autocomplete
|
||||
**/
|
||||
|
||||
export var CANCELLED_STATUS = "__CANCELLED";
|
||||
|
||||
var shiftMap = [];
|
||||
shiftMap[192] = "~";
|
||||
shiftMap[49] = "!";
|
||||
shiftMap[50] = "@";
|
||||
shiftMap[51] = "#";
|
||||
shiftMap[52] = "$";
|
||||
shiftMap[53] = "%";
|
||||
shiftMap[54] = "^";
|
||||
shiftMap[55] = "&";
|
||||
shiftMap[56] = "*";
|
||||
shiftMap[57] = "(";
|
||||
shiftMap[48] = ")";
|
||||
shiftMap[109] = "_";
|
||||
shiftMap[107] = "+";
|
||||
shiftMap[219] = "{";
|
||||
shiftMap[221] = "}";
|
||||
shiftMap[220] = "|";
|
||||
shiftMap[59] = ":";
|
||||
shiftMap[222] = "\"";
|
||||
shiftMap[188] = "<";
|
||||
shiftMap[190] = ">";
|
||||
shiftMap[191] = "?";
|
||||
shiftMap[32] = " ";
|
||||
|
||||
function mapKeyPressToActualCharacter(isShiftKey, characterCode) {
|
||||
if ( characterCode === 27 || characterCode === 8 || characterCode === 9 || characterCode === 20 || characterCode === 16 || characterCode === 17 || characterCode === 91 || characterCode === 13 || characterCode === 92 || characterCode === 18 ) { return false; }
|
||||
|
||||
// Lookup non-letter keypress while holding shift
|
||||
if (isShiftKey && ( characterCode < 65 || characterCode > 90 )) {
|
||||
return shiftMap[characterCode];
|
||||
}
|
||||
|
||||
var stringValue = String.fromCharCode(characterCode);
|
||||
if ( !isShiftKey ) {
|
||||
stringValue = stringValue.toLowerCase();
|
||||
}
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
export default function(options) {
|
||||
var autocompletePlugin = this;
|
||||
|
||||
if (this.length === 0) return;
|
||||
|
||||
if (options && options.cancel && this.data("closeAutocomplete")) {
|
||||
this.data("closeAutocomplete")();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (this.length !== 1) {
|
||||
alert("only supporting one matcher at the moment");
|
||||
}
|
||||
|
||||
var disabled = options && options.disabled;
|
||||
var wrap = null;
|
||||
var autocompleteOptions = null;
|
||||
var selectedOption = null;
|
||||
var completeStart = null;
|
||||
var completeEnd = null;
|
||||
var me = this;
|
||||
var div = null;
|
||||
|
||||
// input is handled differently
|
||||
var isInput = this[0].tagName === "INPUT";
|
||||
var inputSelectedItems = [];
|
||||
|
||||
var closeAutocomplete = function() {
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
}
|
||||
div = null;
|
||||
completeStart = null;
|
||||
autocompleteOptions = null;
|
||||
};
|
||||
|
||||
var addInputSelectedItem = function(item) {
|
||||
var transformed,
|
||||
transformedItem = item;
|
||||
|
||||
if (options.transformComplete) { transformedItem = options.transformComplete(transformedItem); }
|
||||
// dump what we have in single mode, just in case
|
||||
if (options.single) { inputSelectedItems = []; }
|
||||
transformed = _.isArray(transformedItem) ? transformedItem : [transformedItem || item];
|
||||
|
||||
var divs = transformed.map(function(itm) {
|
||||
var d = $("<div class='item'><span>" + itm + "<a class='remove' href='#'><i class='fa fa-times'></i></a></span></div>");
|
||||
var prev = me.parent().find('.item:last');
|
||||
if (prev.length === 0) {
|
||||
me.parent().prepend(d);
|
||||
} else {
|
||||
prev.after(d);
|
||||
}
|
||||
inputSelectedItems.push(itm);
|
||||
return d[0];
|
||||
});
|
||||
|
||||
if (options.onChangeItems) { options.onChangeItems(inputSelectedItems); }
|
||||
|
||||
$(divs).find('a').click(function() {
|
||||
closeAutocomplete();
|
||||
inputSelectedItems.splice($.inArray(transformedItem, inputSelectedItems), 1);
|
||||
$(this).parent().parent().remove();
|
||||
if (options.single) {
|
||||
me.show();
|
||||
}
|
||||
if (options.onChangeItems) {
|
||||
options.onChangeItems(inputSelectedItems);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
var completeTerm = function(term) {
|
||||
if (term) {
|
||||
if (isInput) {
|
||||
me.val("");
|
||||
if(options.single){
|
||||
me.hide();
|
||||
}
|
||||
addInputSelectedItem(term);
|
||||
} else {
|
||||
if (options.transformComplete) {
|
||||
term = options.transformComplete(term);
|
||||
}
|
||||
var text = me.val();
|
||||
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
|
||||
me.val(text);
|
||||
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
|
||||
}
|
||||
}
|
||||
closeAutocomplete();
|
||||
};
|
||||
|
||||
if (isInput) {
|
||||
var width = this.width();
|
||||
wrap = this.wrap("<div class='ac-wrap clearfix" + (disabled ? " disabled": "") + "'/>").parent();
|
||||
wrap.width(width);
|
||||
if(options.single) {
|
||||
this.css("width","100%");
|
||||
} else {
|
||||
this.width(150);
|
||||
}
|
||||
this.attr('name', this.attr('name') + "-renamed");
|
||||
var vals = this.val().split(",");
|
||||
_.each(vals,function(x) {
|
||||
if (x !== "") {
|
||||
if (options.reverseTransform) {
|
||||
x = options.reverseTransform(x);
|
||||
}
|
||||
addInputSelectedItem(x);
|
||||
}
|
||||
});
|
||||
if(options.items) {
|
||||
_.each(options.items, function(item){
|
||||
addInputSelectedItem(item);
|
||||
});
|
||||
}
|
||||
this.val("");
|
||||
completeStart = 0;
|
||||
wrap.click(function() {
|
||||
autocompletePlugin.focus();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
var markSelected = function() {
|
||||
var links = div.find('li a');
|
||||
links.removeClass('selected');
|
||||
return $(links[selectedOption]).addClass('selected');
|
||||
};
|
||||
|
||||
var renderAutocomplete = function() {
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
}
|
||||
if (autocompleteOptions.length === 0) return;
|
||||
|
||||
div = $(options.template({ options: autocompleteOptions }));
|
||||
|
||||
var ul = div.find('ul');
|
||||
selectedOption = 0;
|
||||
markSelected();
|
||||
ul.find('li').click(function() {
|
||||
selectedOption = ul.find('li').index(this);
|
||||
completeTerm(autocompleteOptions[selectedOption]);
|
||||
return false;
|
||||
});
|
||||
var pos = null;
|
||||
var vOffset = 0;
|
||||
var hOffset = 0;
|
||||
if (isInput) {
|
||||
pos = {
|
||||
left: 0,
|
||||
top: 0
|
||||
};
|
||||
vOffset = -32;
|
||||
hOffset = 0;
|
||||
} else {
|
||||
pos = me.caretPosition({
|
||||
pos: completeStart,
|
||||
key: options.key
|
||||
});
|
||||
hOffset = 27;
|
||||
}
|
||||
div.css({
|
||||
left: "-1000px"
|
||||
});
|
||||
|
||||
me.parent().append(div);
|
||||
|
||||
if(!isInput){
|
||||
vOffset = div.height();
|
||||
}
|
||||
|
||||
var mePos = me.position();
|
||||
var borderTop = parseInt(me.css('border-top-width'), 10) || 0;
|
||||
div.css({
|
||||
position: 'absolute',
|
||||
top: (mePos.top + pos.top - vOffset + borderTop) + 'px',
|
||||
left: (mePos.left + pos.left + hOffset) + 'px'
|
||||
});
|
||||
};
|
||||
|
||||
var updateAutoComplete = function(r) {
|
||||
|
||||
if (completeStart === null) return;
|
||||
|
||||
if (r && r.then && typeof(r.then) === "function") {
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
}
|
||||
r.then(updateAutoComplete);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow an update method to cancel. This allows us to debounce
|
||||
// promises without leaking
|
||||
if (r === CANCELLED_STATUS) {
|
||||
return;
|
||||
}
|
||||
|
||||
autocompleteOptions = r;
|
||||
if (!r || r.length === 0) {
|
||||
closeAutocomplete();
|
||||
} else {
|
||||
renderAutocomplete();
|
||||
}
|
||||
};
|
||||
|
||||
// chain to allow multiples
|
||||
var oldClose = me.data("closeAutocomplete");
|
||||
me.data("closeAutocomplete", function() {
|
||||
if (oldClose) {
|
||||
oldClose();
|
||||
}
|
||||
closeAutocomplete();
|
||||
});
|
||||
|
||||
|
||||
$(this).keypress(function(e) {
|
||||
|
||||
if (!options.key) return;
|
||||
|
||||
// keep hunting backwards till you hit a
|
||||
if (e.which === options.key.charCodeAt(0)) {
|
||||
var caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
var prevChar = me.val().charAt(caretPosition - 1);
|
||||
if (!prevChar || /\s/.test(prevChar)) {
|
||||
completeStart = completeEnd = caretPosition;
|
||||
updateAutoComplete(options.dataSource(""));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(this).keydown(function(e) {
|
||||
var c, caretPosition, i, initial, next, prev, prevIsGood, stopFound, term, total, userToComplete;
|
||||
|
||||
if(options.allowAny){
|
||||
// saves us wiring up a change event as well, keypress is while its pressed
|
||||
_.delay(function(){
|
||||
if(inputSelectedItems.length === 0) {
|
||||
inputSelectedItems.push("");
|
||||
}
|
||||
|
||||
if(_.isString(inputSelectedItems[0]) && me.val().length > 0) {
|
||||
inputSelectedItems.pop();
|
||||
inputSelectedItems.push(me.val());
|
||||
if (options.onChangeItems) {
|
||||
options.onChangeItems(inputSelectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
},50);
|
||||
}
|
||||
|
||||
if (!options.key) {
|
||||
completeStart = 0;
|
||||
}
|
||||
if (e.which === 16) return;
|
||||
if ((completeStart === null) && e.which === 8 && options.key) {
|
||||
c = Discourse.Utilities.caretPosition(me[0]);
|
||||
next = me[0].value[c];
|
||||
c -= 1;
|
||||
initial = c;
|
||||
prevIsGood = true;
|
||||
while (prevIsGood && c >= 0) {
|
||||
c -= 1;
|
||||
prev = me[0].value[c];
|
||||
stopFound = prev === options.key;
|
||||
if (stopFound) {
|
||||
prev = me[0].value[c - 1];
|
||||
if (!prev || /\s/.test(prev)) {
|
||||
completeStart = c;
|
||||
caretPosition = completeEnd = initial;
|
||||
term = me[0].value.substring(c + 1, initial);
|
||||
updateAutoComplete(options.dataSource(term));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
prevIsGood = /[a-zA-Z\.]/.test(prev);
|
||||
}
|
||||
}
|
||||
|
||||
// ESC
|
||||
if (e.which === 27) {
|
||||
if (completeStart !== null) {
|
||||
closeAutocomplete();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (completeStart !== null) {
|
||||
caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
|
||||
// If we've backspaced past the beginning, cancel unless no key
|
||||
if (caretPosition <= completeStart && options.key) {
|
||||
closeAutocomplete();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keyboard codes! So 80's.
|
||||
switch (e.which) {
|
||||
case 13:
|
||||
case 39:
|
||||
case 9:
|
||||
if (!autocompleteOptions) return true;
|
||||
if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) {
|
||||
completeTerm(userToComplete);
|
||||
} else {
|
||||
// We're cancelling it, really.
|
||||
return true;
|
||||
}
|
||||
e.stopImmediatePropagation();
|
||||
return false;
|
||||
case 38:
|
||||
selectedOption = selectedOption - 1;
|
||||
if (selectedOption < 0) {
|
||||
selectedOption = 0;
|
||||
}
|
||||
markSelected();
|
||||
return false;
|
||||
case 40:
|
||||
total = autocompleteOptions.length;
|
||||
selectedOption = selectedOption + 1;
|
||||
if (selectedOption >= total) {
|
||||
selectedOption = total - 1;
|
||||
}
|
||||
if (selectedOption < 0) {
|
||||
selectedOption = 0;
|
||||
}
|
||||
markSelected();
|
||||
return false;
|
||||
default:
|
||||
// otherwise they're typing - let's search for it!
|
||||
completeEnd = caretPosition;
|
||||
if (e.which === 8) {
|
||||
caretPosition--;
|
||||
}
|
||||
if (caretPosition < 0) {
|
||||
closeAutocomplete();
|
||||
if (isInput) {
|
||||
i = wrap.find('a:last');
|
||||
if (i) {
|
||||
i.click();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
|
||||
if (e.which >= 48 && e.which <= 90) {
|
||||
term += mapKeyPressToActualCharacter(e.shiftKey, e.which);
|
||||
} else if (e.which === 187) {
|
||||
term += "+";
|
||||
} else if (e.which === 189) {
|
||||
term += (e.shiftKey) ? "_" : "-";
|
||||
} else if (e.which === 220) {
|
||||
term += (e.shiftKey) ? "|" : "]";
|
||||
} else if (e.which === 222) {
|
||||
term += (e.shiftKey) ? "\"" : "'";
|
||||
} else if (e.which !== 8) {
|
||||
term += ",";
|
||||
}
|
||||
|
||||
updateAutoComplete(options.dataSource(term));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
Reference in New Issue
Block a user