@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Hide the autocompleter intelligently when you ignore it and keep typing

Summary:
Ref T10163. When we think the user has finished typing a word (because they typed a space, period, or other similar characters) and nothing else they might type could possibly change the outcome (usually because the words they have typed already match nothing), just deactivate the autocomplete.

As a special case, if the word they have typed already select exactly one result, //and// they have already typed exactly that result, assume they just typed it from memory and deactivate.

Test Plan:
- Typed `@dog qwer zxcv` and saw autocomplete deactivate on the space before `z` (on my local install, `@dog` is ambiguous but `@dog qwer` matches nothing).
- Typed `@epriestley ` and saw autocomplete deactivate on space.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10163

Differential Revision: https://secure.phabricator.com/D15039

+152 -33
+25 -25
resources/celerity/map.php
··· 8 8 return array( 9 9 'names' => array( 10 10 'core.pkg.css' => '3a97c8b9', 11 - 'core.pkg.js' => '5813273d', 11 + 'core.pkg.js' => '573e6664', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => '2de124c9', 14 14 'differential.pkg.js' => 'f83532f8', ··· 249 249 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f', 250 250 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838', 251 251 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd', 252 - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187', 252 + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '013ffff9', 253 253 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', 254 - 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '2818f5ce', 254 + 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '1bc11c4a', 255 255 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', 256 256 'rsrc/externals/raphael/g.raphael.js' => '40dde778', 257 257 'rsrc/externals/raphael/g.raphael.line.js' => '40da039e', ··· 507 507 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', 508 508 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 509 509 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 510 - 'rsrc/js/phuix/PHUIXAutocomplete.js' => '5582787f', 510 + 'rsrc/js/phuix/PHUIXAutocomplete.js' => '0abdd4a8', 511 511 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', 512 512 'rsrc/js/phuix/PHUIXFormControl.js' => '8fba1997', 513 513 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ··· 709 709 'javelin-typeahead' => '70baed2f', 710 710 'javelin-typeahead-composite-source' => '503e17fd', 711 711 'javelin-typeahead-normalizer' => 'e6e25838', 712 - 'javelin-typeahead-ondemand-source' => '8b3fd187', 712 + 'javelin-typeahead-ondemand-source' => '013ffff9', 713 713 'javelin-typeahead-preloaded-source' => '54f314a0', 714 - 'javelin-typeahead-source' => '2818f5ce', 714 + 'javelin-typeahead-source' => '1bc11c4a', 715 715 'javelin-typeahead-static-source' => '6c0e62fa', 716 716 'javelin-uri' => 'c989ade3', 717 717 'javelin-util' => '93cc50d6', ··· 836 836 'phui-workpanel-view-css' => 'adec7699', 837 837 'phuix-action-list-view' => 'b5c256b8', 838 838 'phuix-action-view' => '8cf6d262', 839 - 'phuix-autocomplete' => '5582787f', 839 + 'phuix-autocomplete' => '0abdd4a8', 840 840 'phuix-dropdown-menu' => 'bd4c8dca', 841 841 'phuix-form-control-view' => '8fba1997', 842 842 'phuix-icon-view' => 'bff6884b', ··· 863 863 'unhandled-exception-css' => '4c96257a', 864 864 ), 865 865 'requires' => array( 866 + '013ffff9' => array( 867 + 'javelin-install', 868 + 'javelin-util', 869 + 'javelin-request', 870 + 'javelin-typeahead-source', 871 + ), 866 872 '01774ab2' => array( 867 873 'javelin-dom', 868 874 'javelin-util', ··· 911 917 'javelin-dom', 912 918 'javelin-router', 913 919 ), 920 + '0abdd4a8' => array( 921 + 'javelin-install', 922 + 'javelin-dom', 923 + 'phuix-icon-view', 924 + 'phabricator-prefab', 925 + ), 914 926 '0b7a4f6e' => array( 915 927 'javelin-behavior', 916 928 'javelin-typeahead-ondemand-source', ··· 949 961 'javelin-install', 950 962 'javelin-util', 951 963 'phabricator-keyboard-shortcut-manager', 964 + ), 965 + '1bc11c4a' => array( 966 + 'javelin-install', 967 + 'javelin-util', 968 + 'javelin-dom', 969 + 'javelin-typeahead-normalizer', 952 970 ), 953 971 '1d298e3a' => array( 954 972 'javelin-install', ··· 1008 1026 'phabricator-phtize', 1009 1027 'phabricator-drag-and-drop-file-upload', 1010 1028 'phabricator-draggable-list', 1011 - ), 1012 - '2818f5ce' => array( 1013 - 'javelin-install', 1014 - 'javelin-util', 1015 - 'javelin-dom', 1016 - 'javelin-typeahead-normalizer', 1017 1029 ), 1018 1030 '2926fff2' => array( 1019 1031 'javelin-behavior', ··· 1198 1210 'javelin-request', 1199 1211 'javelin-typeahead-source', 1200 1212 ), 1201 - '5582787f' => array( 1202 - 'javelin-install', 1203 - 'javelin-dom', 1204 - 'phuix-icon-view', 1205 - 'phabricator-prefab', 1206 - ), 1207 1213 '558829c2' => array( 1208 1214 'javelin-stratcom', 1209 1215 'javelin-behavior', ··· 1488 1494 'javelin-dom', 1489 1495 'javelin-stratcom', 1490 1496 'javelin-vector', 1491 - ), 1492 - '8b3fd187' => array( 1493 - 'javelin-install', 1494 - 'javelin-util', 1495 - 'javelin-request', 1496 - 'javelin-typeahead-source', 1497 1497 ), 1498 1498 '8bdb2835' => array( 1499 1499 'phui-fontkit-css',
+14 -5
webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js
··· 38 38 lastChange : null, 39 39 haveData : null, 40 40 41 - didChange : function(raw_value) { 41 + didChange : function(raw_value, force) { 42 42 this.lastChange = JX.now(); 43 43 var value = this.normalize(raw_value); 44 44 ··· 59 59 } 60 60 61 61 this.waitForResults(); 62 - setTimeout( 63 - JX.bind(this, this.sendRequest, this.lastChange, value, raw_value), 64 - this.getQueryDelay() 65 - ); 62 + 63 + var send_request = JX.bind( 64 + this, 65 + this.sendRequest, 66 + this.lastChange, 67 + value, 68 + raw_value); 69 + 70 + if (force) { 71 + send_request(); 72 + } else { 73 + setTimeout(send_request, this.getQueryDelay()); 74 + } 66 75 } 67 76 }, 68 77
+1 -1
webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js
··· 289 289 this.filterAndSortHits(value, hits); 290 290 291 291 var nodes = this.renderNodes(value, hits); 292 - this.invoke('resultsready', nodes, value); 292 + this.invoke('resultsready', nodes, value, partial); 293 293 if (!partial) { 294 294 this.invoke('complete'); 295 295 }
+112 -2
webroot/rsrc/js/phuix/PHUIXAutocomplete.js
··· 12 12 this._map = {}; 13 13 this._datasources = {}; 14 14 this._listNodes = []; 15 + this._resultMap = {}; 15 16 }, 16 17 17 18 members: { ··· 35 36 _x: null, 36 37 _y: null, 37 38 _visible: false, 39 + _resultMap: null, 38 40 39 41 setArea: function(area) { 40 42 this._area = area; ··· 205 207 } 206 208 }, 207 209 208 - _onresults: function(code, nodes, value) { 210 + _onresults: function(code, nodes, value, partial) { 211 + // Even if these results are out of date, we still want to fill in the 212 + // result map so we can terminate things later. 213 + if (!partial) { 214 + if (!this._resultMap[code]) { 215 + this._resultMap[code] = {}; 216 + } 217 + 218 + var hits = []; 219 + for (var ii = 0; ii < nodes.length; ii++) { 220 + var result = this._datasources[code].getResult(nodes[ii].rel); 221 + if (!result) { 222 + hits = null; 223 + break; 224 + } 225 + 226 + hits.push(result.autocomplete); 227 + } 228 + 229 + if (hits !== null) { 230 + this._resultMap[code][value] = hits; 231 + } 232 + } 233 + 209 234 if (code !== this._active) { 210 235 return; 211 236 } 212 237 213 238 if (value !== this._value) { 214 239 return; 240 + } 241 + 242 + if (this._isTerminatedString(value)) { 243 + if (this._hasUnrefinableResults(value)) { 244 + this._deactivate(); 245 + return; 246 + } 215 247 } 216 248 217 249 var list = this._getListNode(); ··· 291 323 return ['#', '@', ',', '!', '?']; 292 324 }, 293 325 326 + _getTerminators: function() { 327 + return [' ', ':', ',', '.', '!', '?']; 328 + }, 329 + 330 + _isTerminatedString: function(string) { 331 + var terminators = this._getTerminators(); 332 + for (var ii = 0; ii < terminators.length; ii++) { 333 + var term = terminators[ii]; 334 + if (string.substring(string.length - term.length) == term) { 335 + return true; 336 + } 337 + } 338 + 339 + return false; 340 + }, 341 + 342 + _hasUnrefinableResults: function(query) { 343 + if (!this._resultMap[this._active]) { 344 + return false; 345 + } 346 + 347 + var map = this._resultMap[this._active]; 348 + 349 + for (var ii = 1; ii < query.length; ii++) { 350 + var prefix = query.substring(0, ii); 351 + if (map.hasOwnProperty(prefix)) { 352 + var results = map[prefix]; 353 + 354 + // If any prefix of the query has no results, the full query also 355 + // has no results so we can not refine them. 356 + if (!results.length) { 357 + return true; 358 + } 359 + 360 + // If there is exactly one match and the it is a prefix of the query, 361 + // we can safely assume the user just typed out the right result 362 + // from memory and doesn't need to refine it. 363 + if (results.length == 1) { 364 + // Strip the first character off, like a "#" or "@". 365 + var result = results[0].substring(1); 366 + 367 + if (query.length >= result.length) { 368 + if (query.substring(0, result.length) === result) { 369 + return true; 370 + } 371 + } 372 + } 373 + } 374 + } 375 + 376 + return false; 377 + }, 378 + 294 379 _trim: function(str) { 295 380 var suffixes = this._getSuffixes(); 296 381 for (var ii = 0; ii < suffixes.length; ii++) { ··· 416 501 } 417 502 } 418 503 419 - this._datasource.didChange(trim); 504 + // If the input is terminated by a space or another word-terminating 505 + // punctuation mark, we're going to deactivate if the results can not 506 + // be refined by addding more words. 507 + 508 + // The idea is that if you type "@alan ab", you're allowed to keep 509 + // editing "ab" until you type a space, period, or other terminator, 510 + // since you might not be sure how to spell someone's last name or the 511 + // second word of a project. 512 + 513 + // Once you do terminate a word, if the words you have have entered match 514 + // nothing or match only one exact match, we can safely deactivate and 515 + // assume you're just typing text because further words could never 516 + // refine the result set. 517 + 518 + var force; 519 + if (this._isTerminatedString(text)) { 520 + if (this._hasUnrefinableResults(text)) { 521 + this._deactivate(); 522 + return; 523 + } 524 + force = true; 525 + } else { 526 + force = false; 527 + } 528 + 529 + this._datasource.didChange(trim, force); 420 530 421 531 this._x = x; 422 532 this._y = y;