Fix blur and sort behavior for autocomplete
Summary: Ref T10163. - If you click a result, we get a blur before your click hits, and deactivate before the click can work. Instead, wait before responding to blur. - Use the standard sort handler which puts unixnames over human names. Also use the standard filter which deals with disabled users not matching unless they're the only match. Test Plan: - Clicked a result, got a replacement. - Named myself "dog dog", typed "@dog", user "@dog" was now first match despite me being "@admin". - Used normal typeaheads to make sure I didn't break sort handler. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10163 Differential Revision: https://secure.phabricator.com/D15031
This commit is contained in:
parent
77447fc945
commit
3c19004f9f
|
@ -8,7 +8,7 @@
|
||||||
return array(
|
return array(
|
||||||
'names' => array(
|
'names' => array(
|
||||||
'core.pkg.css' => '3a97c8b9',
|
'core.pkg.css' => '3a97c8b9',
|
||||||
'core.pkg.js' => '1f5f365a',
|
'core.pkg.js' => '5813273d',
|
||||||
'darkconsole.pkg.js' => 'e7393ebb',
|
'darkconsole.pkg.js' => 'e7393ebb',
|
||||||
'differential.pkg.css' => '2de124c9',
|
'differential.pkg.css' => '2de124c9',
|
||||||
'differential.pkg.js' => 'f83532f8',
|
'differential.pkg.js' => 'f83532f8',
|
||||||
|
@ -458,7 +458,7 @@ return array(
|
||||||
'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
|
'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
|
||||||
'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
|
'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
|
||||||
'rsrc/js/core/Notification.js' => 'ccf1cbf8',
|
'rsrc/js/core/Notification.js' => 'ccf1cbf8',
|
||||||
'rsrc/js/core/Prefab.js' => 'a15cbd65',
|
'rsrc/js/core/Prefab.js' => 'e67df814',
|
||||||
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
|
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
|
||||||
'rsrc/js/core/TextAreaUtils.js' => '9e54692d',
|
'rsrc/js/core/TextAreaUtils.js' => '9e54692d',
|
||||||
'rsrc/js/core/Title.js' => 'df5e11d2',
|
'rsrc/js/core/Title.js' => 'df5e11d2',
|
||||||
|
@ -507,7 +507,7 @@ return array(
|
||||||
'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836',
|
'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836',
|
||||||
'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
|
'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
|
||||||
'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262',
|
'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262',
|
||||||
'rsrc/js/phuix/PHUIXAutocomplete.js' => 'c5f5e42f',
|
'rsrc/js/phuix/PHUIXAutocomplete.js' => '2b735afc',
|
||||||
'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
|
'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
|
||||||
'rsrc/js/phuix/PHUIXFormControl.js' => '8fba1997',
|
'rsrc/js/phuix/PHUIXFormControl.js' => '8fba1997',
|
||||||
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
|
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
|
||||||
|
@ -760,7 +760,7 @@ return array(
|
||||||
'phabricator-notification-menu-css' => 'f31c0bde',
|
'phabricator-notification-menu-css' => 'f31c0bde',
|
||||||
'phabricator-object-selector-css' => '85ee8ce6',
|
'phabricator-object-selector-css' => '85ee8ce6',
|
||||||
'phabricator-phtize' => 'd254d646',
|
'phabricator-phtize' => 'd254d646',
|
||||||
'phabricator-prefab' => 'a15cbd65',
|
'phabricator-prefab' => 'e67df814',
|
||||||
'phabricator-remarkup-css' => 'b748dc17',
|
'phabricator-remarkup-css' => 'b748dc17',
|
||||||
'phabricator-search-results-css' => '7dea472c',
|
'phabricator-search-results-css' => '7dea472c',
|
||||||
'phabricator-shaped-request' => '7cbe244b',
|
'phabricator-shaped-request' => '7cbe244b',
|
||||||
|
@ -836,7 +836,7 @@ return array(
|
||||||
'phui-workpanel-view-css' => 'adec7699',
|
'phui-workpanel-view-css' => 'adec7699',
|
||||||
'phuix-action-list-view' => 'b5c256b8',
|
'phuix-action-list-view' => 'b5c256b8',
|
||||||
'phuix-action-view' => '8cf6d262',
|
'phuix-action-view' => '8cf6d262',
|
||||||
'phuix-autocomplete' => 'c5f5e42f',
|
'phuix-autocomplete' => '2b735afc',
|
||||||
'phuix-dropdown-menu' => 'bd4c8dca',
|
'phuix-dropdown-menu' => 'bd4c8dca',
|
||||||
'phuix-form-control-view' => '8fba1997',
|
'phuix-form-control-view' => '8fba1997',
|
||||||
'phuix-icon-view' => 'bff6884b',
|
'phuix-icon-view' => 'bff6884b',
|
||||||
|
@ -1023,6 +1023,12 @@ return array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-util',
|
'javelin-util',
|
||||||
),
|
),
|
||||||
|
'2b735afc' => array(
|
||||||
|
'javelin-install',
|
||||||
|
'javelin-dom',
|
||||||
|
'phuix-icon-view',
|
||||||
|
'phabricator-prefab',
|
||||||
|
),
|
||||||
'2b8de964' => array(
|
'2b8de964' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-util',
|
'javelin-util',
|
||||||
|
@ -1589,18 +1595,6 @@ return array(
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
'javelin-reactor-dom',
|
'javelin-reactor-dom',
|
||||||
),
|
),
|
||||||
'a15cbd65' => array(
|
|
||||||
'javelin-install',
|
|
||||||
'javelin-util',
|
|
||||||
'javelin-dom',
|
|
||||||
'javelin-typeahead',
|
|
||||||
'javelin-tokenizer',
|
|
||||||
'javelin-typeahead-preloaded-source',
|
|
||||||
'javelin-typeahead-ondemand-source',
|
|
||||||
'javelin-dom',
|
|
||||||
'javelin-stratcom',
|
|
||||||
'javelin-util',
|
|
||||||
),
|
|
||||||
'a16ec1c6' => array(
|
'a16ec1c6' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
@ -1799,12 +1793,6 @@ return array(
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
'javelin-vector',
|
'javelin-vector',
|
||||||
),
|
),
|
||||||
'c5f5e42f' => array(
|
|
||||||
'javelin-install',
|
|
||||||
'javelin-dom',
|
|
||||||
'phuix-icon-view',
|
|
||||||
'phabricator-prefab',
|
|
||||||
),
|
|
||||||
'c6f720ff' => array(
|
'c6f720ff' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
@ -1977,6 +1965,18 @@ return array(
|
||||||
'javelin-workflow',
|
'javelin-workflow',
|
||||||
'javelin-magical-init',
|
'javelin-magical-init',
|
||||||
),
|
),
|
||||||
|
'e67df814' => array(
|
||||||
|
'javelin-install',
|
||||||
|
'javelin-util',
|
||||||
|
'javelin-dom',
|
||||||
|
'javelin-typeahead',
|
||||||
|
'javelin-tokenizer',
|
||||||
|
'javelin-typeahead-preloaded-source',
|
||||||
|
'javelin-typeahead-ondemand-source',
|
||||||
|
'javelin-dom',
|
||||||
|
'javelin-stratcom',
|
||||||
|
'javelin-util',
|
||||||
|
),
|
||||||
'e6e25838' => array(
|
'e6e25838' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
),
|
),
|
||||||
|
|
|
@ -99,81 +99,8 @@ JX.install('Prefab', {
|
||||||
datasource = new JX.TypeaheadPreloadedSource(config.src);
|
datasource = new JX.TypeaheadPreloadedSource(config.src);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort results so that the viewing user always comes up first; after
|
datasource.setSortHandler(
|
||||||
// that, prefer unixname matches to realname matches.
|
JX.bind(datasource, JX.Prefab.sortHandler, config));
|
||||||
|
|
||||||
var sort_handler = function(value, list, cmp) {
|
|
||||||
var priority_hits = {};
|
|
||||||
var self_hits = {};
|
|
||||||
|
|
||||||
var tokens = this.tokenize(value);
|
|
||||||
|
|
||||||
for (var ii = 0; ii < list.length; ii++) {
|
|
||||||
var item = list[ii];
|
|
||||||
|
|
||||||
for (var jj = 0; jj < tokens.length; jj++) {
|
|
||||||
if (item.name.indexOf(tokens[jj]) === 0) {
|
|
||||||
priority_hits[item.id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.priority) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.username && item.priority == config.username) {
|
|
||||||
self_hits[item.id] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var hh = 0; hh < tokens.length; hh++) {
|
|
||||||
if (item.priority.substr(0, tokens[hh].length) == tokens[hh]) {
|
|
||||||
priority_hits[item.id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.sort(function(u, v) {
|
|
||||||
if (self_hits[u.id] != self_hits[v.id]) {
|
|
||||||
return self_hits[v.id] ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If one result is open and one is closed, show the open result
|
|
||||||
// first. The "!" tricks here are becaused closed values are display
|
|
||||||
// strings, so the value is either `null` or some truthy string. If
|
|
||||||
// we compare the values directly, we'll apply this rule to two
|
|
||||||
// objects which are both closed but for different reasons, like
|
|
||||||
// "Archived" and "Disabled".
|
|
||||||
|
|
||||||
var u_open = !u.closed;
|
|
||||||
var v_open = !v.closed;
|
|
||||||
|
|
||||||
if (u_open != v_open) {
|
|
||||||
if (u_open) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (priority_hits[u.id] != priority_hits[v.id]) {
|
|
||||||
return priority_hits[v.id] ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort users ahead of other result types.
|
|
||||||
if (u.priorityType != v.priorityType) {
|
|
||||||
if (u.priorityType == 'user') {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (v.priorityType == 'user') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmp(u, v);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
datasource.setSortHandler(JX.bind(datasource, sort_handler));
|
|
||||||
datasource.setFilterHandler(JX.Prefab.filterClosedResults);
|
datasource.setFilterHandler(JX.Prefab.filterClosedResults);
|
||||||
datasource.setTransformer(JX.Prefab.transformDatasourceResults);
|
datasource.setTransformer(JX.Prefab.transformDatasourceResults);
|
||||||
|
|
||||||
|
@ -251,6 +178,80 @@ JX.install('Prefab', {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sortHandler: function(config, value, list, cmp) {
|
||||||
|
// Sort results so that the viewing user always comes up first; after
|
||||||
|
// that, prefer unixname matches to realname matches.
|
||||||
|
var priority_hits = {};
|
||||||
|
var self_hits = {};
|
||||||
|
|
||||||
|
var tokens = this.tokenize(value);
|
||||||
|
|
||||||
|
for (var ii = 0; ii < list.length; ii++) {
|
||||||
|
var item = list[ii];
|
||||||
|
|
||||||
|
for (var jj = 0; jj < tokens.length; jj++) {
|
||||||
|
if (item.name.indexOf(tokens[jj]) === 0) {
|
||||||
|
priority_hits[item.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.priority) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.username && item.priority == config.username) {
|
||||||
|
self_hits[item.id] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var hh = 0; hh < tokens.length; hh++) {
|
||||||
|
if (item.priority.substr(0, tokens[hh].length) == tokens[hh]) {
|
||||||
|
priority_hits[item.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.sort(function(u, v) {
|
||||||
|
if (self_hits[u.id] != self_hits[v.id]) {
|
||||||
|
return self_hits[v.id] ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If one result is open and one is closed, show the open result
|
||||||
|
// first. The "!" tricks here are becaused closed values are display
|
||||||
|
// strings, so the value is either `null` or some truthy string. If
|
||||||
|
// we compare the values directly, we'll apply this rule to two
|
||||||
|
// objects which are both closed but for different reasons, like
|
||||||
|
// "Archived" and "Disabled".
|
||||||
|
|
||||||
|
var u_open = !u.closed;
|
||||||
|
var v_open = !v.closed;
|
||||||
|
|
||||||
|
if (u_open != v_open) {
|
||||||
|
if (u_open) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority_hits[u.id] != priority_hits[v.id]) {
|
||||||
|
return priority_hits[v.id] ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort users ahead of other result types.
|
||||||
|
if (u.priorityType != v.priorityType) {
|
||||||
|
if (u.priorityType == 'user') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (v.priorityType == 'user') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmp(u, v);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter callback for tokenizers and typeaheads which filters out closed
|
* Filter callback for tokenizers and typeaheads which filters out closed
|
||||||
* or disabled objects unless they are the only options.
|
* or disabled objects unless they are the only options.
|
||||||
|
|
|
@ -60,8 +60,14 @@ JX.install('PHUIXAutocomplete', {
|
||||||
var device = JX.bind(this, this._ondevice);
|
var device = JX.bind(this, this._ondevice);
|
||||||
JX.Stratcom.listen('phabricator-device-change', null, device);
|
JX.Stratcom.listen('phabricator-device-change', null, device);
|
||||||
|
|
||||||
// When the user clicks away from the textarea, deactivate.
|
// When the user clicks away from the textarea, deactivate. However, we
|
||||||
var deactivate = JX.bind(this, this._deactivate);
|
// don't want to deactivate if we're blurring because they clicked an
|
||||||
|
// option in the dropdown, so put a timeout on the deactivation. This
|
||||||
|
// will let the click run first if they did actually click a result.
|
||||||
|
var deactivate = JX.bind(this, function() {
|
||||||
|
setTimeout(JX.bind(this, this._deactivate), 10);
|
||||||
|
});
|
||||||
|
|
||||||
JX.DOM.listen(area, 'blur', null, deactivate);
|
JX.DOM.listen(area, 'blur', null, deactivate);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -134,6 +140,9 @@ JX.install('PHUIXAutocomplete', {
|
||||||
JX.bind(this, this._onresults, code));
|
JX.bind(this, this._onresults, code));
|
||||||
|
|
||||||
datasource.setTransformer(JX.bind(this, this._transformresult));
|
datasource.setTransformer(JX.bind(this, this._transformresult));
|
||||||
|
datasource.setSortHandler(
|
||||||
|
JX.bind(datasource, JX.Prefab.sortHandler, {}));
|
||||||
|
datasource.setFilterHandler(JX.Prefab.filterClosedResults);
|
||||||
|
|
||||||
this._datasources[code] = datasource;
|
this._datasources[code] = datasource;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue