diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => '3a97c8b9', - 'core.pkg.js' => '5813273d', + 'core.pkg.js' => '573e6664', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', 'differential.pkg.js' => 'f83532f8', @@ -249,9 +249,9 @@ 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f', 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '013ffff9', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce', + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '1bc11c4a', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', 'rsrc/externals/raphael/g.raphael.js' => '40dde778', 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', @@ -507,7 +507,7 @@ 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', - 'rsrc/js/phuix/PHUIXAutocomplete.js' => '5582787f', + 'rsrc/js/phuix/PHUIXAutocomplete.js' => '0abdd4a8', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', 'rsrc/js/phuix/PHUIXFormControl.js' => '8fba1997', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', @@ -709,9 +709,9 @@ 'javelin-typeahead' => '70baed2f', 'javelin-typeahead-composite-source' => '503e17fd', 'javelin-typeahead-normalizer' => 'e6e25838', - 'javelin-typeahead-ondemand-source' => '8b3fd187', + 'javelin-typeahead-ondemand-source' => '013ffff9', 'javelin-typeahead-preloaded-source' => '54f314a0', - 'javelin-typeahead-source' => '2818f5ce', + 'javelin-typeahead-source' => '1bc11c4a', 'javelin-typeahead-static-source' => '6c0e62fa', 'javelin-uri' => 'c989ade3', 'javelin-util' => '93cc50d6', @@ -836,7 +836,7 @@ 'phui-workpanel-view-css' => 'adec7699', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', - 'phuix-autocomplete' => '5582787f', + 'phuix-autocomplete' => '0abdd4a8', 'phuix-dropdown-menu' => 'bd4c8dca', 'phuix-form-control-view' => '8fba1997', 'phuix-icon-view' => 'bff6884b', @@ -863,6 +863,12 @@ 'unhandled-exception-css' => '4c96257a', ), 'requires' => array( + '013ffff9' => array( + 'javelin-install', + 'javelin-util', + 'javelin-request', + 'javelin-typeahead-source', + ), '01774ab2' => array( 'javelin-dom', 'javelin-util', @@ -911,6 +917,12 @@ 'javelin-dom', 'javelin-router', ), + '0abdd4a8' => array( + 'javelin-install', + 'javelin-dom', + 'phuix-icon-view', + 'phabricator-prefab', + ), '0b7a4f6e' => array( 'javelin-behavior', 'javelin-typeahead-ondemand-source', @@ -950,6 +962,12 @@ 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), + '1bc11c4a' => array( + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-typeahead-normalizer', + ), '1d298e3a' => array( 'javelin-install', 'javelin-util', @@ -1009,12 +1027,6 @@ 'phabricator-drag-and-drop-file-upload', 'phabricator-draggable-list', ), - '2818f5ce' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-typeahead-normalizer', - ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', @@ -1198,12 +1210,6 @@ 'javelin-request', 'javelin-typeahead-source', ), - '5582787f' => array( - 'javelin-install', - 'javelin-dom', - 'phuix-icon-view', - 'phabricator-prefab', - ), '558829c2' => array( 'javelin-stratcom', 'javelin-behavior', @@ -1489,12 +1495,6 @@ 'javelin-stratcom', 'javelin-vector', ), - '8b3fd187' => array( - 'javelin-install', - 'javelin-util', - 'javelin-request', - 'javelin-typeahead-source', - ), '8bdb2835' => array( 'phui-fontkit-css', ), diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js --- a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js +++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js @@ -38,7 +38,7 @@ lastChange : null, haveData : null, - didChange : function(raw_value) { + didChange : function(raw_value, force) { this.lastChange = JX.now(); var value = this.normalize(raw_value); @@ -59,10 +59,19 @@ } this.waitForResults(); - setTimeout( - JX.bind(this, this.sendRequest, this.lastChange, value, raw_value), - this.getQueryDelay() - ); + + var send_request = JX.bind( + this, + this.sendRequest, + this.lastChange, + value, + raw_value); + + if (force) { + send_request(); + } else { + setTimeout(send_request, this.getQueryDelay()); + } } }, diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js --- a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js +++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js @@ -289,7 +289,7 @@ this.filterAndSortHits(value, hits); var nodes = this.renderNodes(value, hits); - this.invoke('resultsready', nodes, value); + this.invoke('resultsready', nodes, value, partial); if (!partial) { this.invoke('complete'); } diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js --- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js +++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js @@ -12,6 +12,7 @@ this._map = {}; this._datasources = {}; this._listNodes = []; + this._resultMap = {}; }, members: { @@ -35,6 +36,7 @@ _x: null, _y: null, _visible: false, + _resultMap: null, setArea: function(area) { this._area = area; @@ -205,7 +207,30 @@ } }, - _onresults: function(code, nodes, value) { + _onresults: function(code, nodes, value, partial) { + // Even if these results are out of date, we still want to fill in the + // result map so we can terminate things later. + if (!partial) { + if (!this._resultMap[code]) { + this._resultMap[code] = {}; + } + + var hits = []; + for (var ii = 0; ii < nodes.length; ii++) { + var result = this._datasources[code].getResult(nodes[ii].rel); + if (!result) { + hits = null; + break; + } + + hits.push(result.autocomplete); + } + + if (hits !== null) { + this._resultMap[code][value] = hits; + } + } + if (code !== this._active) { return; } @@ -214,6 +239,13 @@ return; } + if (this._isTerminatedString(value)) { + if (this._hasUnrefinableResults(value)) { + this._deactivate(); + return; + } + } + var list = this._getListNode(); JX.DOM.setContent(list, nodes); @@ -291,6 +323,59 @@ return ['#', '@', ',', '!', '?']; }, + _getTerminators: function() { + return [' ', ':', ',', '.', '!', '?']; + }, + + _isTerminatedString: function(string) { + var terminators = this._getTerminators(); + for (var ii = 0; ii < terminators.length; ii++) { + var term = terminators[ii]; + if (string.substring(string.length - term.length) == term) { + return true; + } + } + + return false; + }, + + _hasUnrefinableResults: function(query) { + if (!this._resultMap[this._active]) { + return false; + } + + var map = this._resultMap[this._active]; + + for (var ii = 1; ii < query.length; ii++) { + var prefix = query.substring(0, ii); + if (map.hasOwnProperty(prefix)) { + var results = map[prefix]; + + // If any prefix of the query has no results, the full query also + // has no results so we can not refine them. + if (!results.length) { + return true; + } + + // If there is exactly one match and the it is a prefix of the query, + // we can safely assume the user just typed out the right result + // from memory and doesn't need to refine it. + if (results.length == 1) { + // Strip the first character off, like a "#" or "@". + var result = results[0].substring(1); + + if (query.length >= result.length) { + if (query.substring(0, result.length) === result) { + return true; + } + } + } + } + } + + return false; + }, + _trim: function(str) { var suffixes = this._getSuffixes(); for (var ii = 0; ii < suffixes.length; ii++) { @@ -416,7 +501,32 @@ } } - this._datasource.didChange(trim); + // If the input is terminated by a space or another word-terminating + // punctuation mark, we're going to deactivate if the results can not + // be refined by addding more words. + + // The idea is that if you type "@alan ab", you're allowed to keep + // editing "ab" until you type a space, period, or other terminator, + // since you might not be sure how to spell someone's last name or the + // second word of a project. + + // Once you do terminate a word, if the words you have have entered match + // nothing or match only one exact match, we can safely deactivate and + // assume you're just typing text because further words could never + // refine the result set. + + var force; + if (this._isTerminatedString(text)) { + if (this._hasUnrefinableResults(text)) { + this._deactivate(); + return; + } + force = true; + } else { + force = false; + } + + this._datasource.didChange(trim, force); this._x = x; this._y = y;