Page MenuHomePhabricator

D15029.diff
No OneTemporary

D15029.diff

diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -7,8 +7,8 @@
*/
return array(
'names' => array(
- 'core.pkg.css' => '1eed0b4f',
- 'core.pkg.js' => '6ae03393',
+ 'core.pkg.css' => 'f30d5cbd',
+ 'core.pkg.js' => '1f5f365a',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '2de124c9',
'differential.pkg.js' => 'f83532f8',
@@ -102,9 +102,9 @@
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => 'a76cefc9',
- 'rsrc/css/core/remarkup.css' => 'b6ad82e4',
+ 'rsrc/css/core/remarkup.css' => 'b748dc17',
'rsrc/css/core/syntax.css' => '9fd11da8',
- 'rsrc/css/core/z-index.css' => '57ddcaa2',
+ 'rsrc/css/core/z-index.css' => 'a36a45da',
'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa',
'rsrc/css/font/font-aleo.css' => '8bdb2835',
'rsrc/css/font/font-awesome.css' => 'c43323c5',
@@ -457,7 +457,7 @@
'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
'rsrc/js/core/Notification.js' => 'ccf1cbf8',
- 'rsrc/js/core/Prefab.js' => '666c80c5',
+ 'rsrc/js/core/Prefab.js' => 'a15cbd65',
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
'rsrc/js/core/TextAreaUtils.js' => '9e54692d',
'rsrc/js/core/Title.js' => 'df5e11d2',
@@ -487,7 +487,7 @@
'rsrc/js/core/behavior-object-selector.js' => '49b73b36',
'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
'rsrc/js/core/behavior-phabricator-nav.js' => '56a1ca03',
- 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'b60b6d9b',
+ 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '340c8eff',
'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b',
'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e',
'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e',
@@ -506,6 +506,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' => 'c5f5e42f',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
'rsrc/js/phuix/PHUIXFormControl.js' => '8fba1997',
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
@@ -639,7 +640,7 @@
'javelin-behavior-phabricator-notification-example' => '8ce821c5',
'javelin-behavior-phabricator-object-selector' => '49b73b36',
'javelin-behavior-phabricator-oncopy' => '2926fff2',
- 'javelin-behavior-phabricator-remarkup-assist' => 'b60b6d9b',
+ 'javelin-behavior-phabricator-remarkup-assist' => '340c8eff',
'javelin-behavior-phabricator-reveal-content' => '60821bc7',
'javelin-behavior-phabricator-search-typeahead' => '0b7a4f6e',
'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6',
@@ -758,8 +759,8 @@
'phabricator-notification-menu-css' => 'f31c0bde',
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
- 'phabricator-prefab' => '666c80c5',
- 'phabricator-remarkup-css' => 'b6ad82e4',
+ 'phabricator-prefab' => 'a15cbd65',
+ 'phabricator-remarkup-css' => 'b748dc17',
'phabricator-search-results-css' => '7dea472c',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-side-menu-view-css' => '91b7a42c',
@@ -780,7 +781,7 @@
'phabricator-uiexample-reactor-select' => 'a155550f',
'phabricator-uiexample-reactor-sendclass' => '1def2711',
'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
- 'phabricator-zindex-css' => '57ddcaa2',
+ 'phabricator-zindex-css' => 'a36a45da',
'phame-css' => 'dac8fdf2',
'pholio-css' => '95174bdd',
'pholio-edit-css' => '3ad9d1ee',
@@ -833,6 +834,7 @@
'phui-workpanel-view-css' => 'adec7699',
'phuix-action-list-view' => 'b5c256b8',
'phuix-action-view' => '8cf6d262',
+ 'phuix-autocomplete' => 'c5f5e42f',
'phuix-dropdown-menu' => 'bd4c8dca',
'phuix-form-control-view' => '8fba1997',
'phuix-icon-view' => 'bff6884b',
@@ -1050,6 +1052,16 @@
'331b1611' => array(
'javelin-install',
),
+ '340c8eff' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ 'phabricator-phtize',
+ 'phabricator-textareautils',
+ 'javelin-workflow',
+ 'javelin-vector',
+ 'phuix-autocomplete',
+ ),
'3ab51e2c' => array(
'javelin-behavior',
'javelin-behavior-device',
@@ -1293,18 +1305,6 @@
'javelin-vector',
'differential-inline-comment-editor',
),
- '666c80c5' => 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',
- ),
'66dd6e9e' => array(
'javelin-behavior',
'javelin-behavior-device',
@@ -1587,6 +1587,18 @@
'javelin-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(
'javelin-install',
'javelin-dom',
@@ -1742,15 +1754,6 @@
'javelin-dom',
'javelin-util',
),
- 'b60b6d9b' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-dom',
- 'phabricator-phtize',
- 'phabricator-textareautils',
- 'javelin-workflow',
- 'javelin-vector',
- ),
'b6993408' => array(
'javelin-behavior',
'javelin-stratcom',
@@ -1794,6 +1797,12 @@
'javelin-dom',
'javelin-vector',
),
+ 'c5f5e42f' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'phuix-icon-view',
+ 'phabricator-prefab',
+ ),
'c6f720ff' => array(
'javelin-install',
'javelin-dom',
diff --git a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php
--- a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php
+++ b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php
@@ -59,12 +59,15 @@
$closed = pht('Mailing List');
}
+ $username = $user->getUsername();
+
$result = id(new PhabricatorTypeaheadResult())
->setName($user->getFullName())
- ->setURI('/p/'.$user->getUsername())
+ ->setURI('/p/'.$username.'/')
->setPHID($user->getPHID())
- ->setPriorityString($user->getUsername())
+ ->setPriorityString($username)
->setPriorityType('user')
+ ->setAutocomplete('@'.$username)
->setClosed($closed);
if ($user->getIsMailingList()) {
diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php
--- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php
+++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php
@@ -65,13 +65,18 @@
->setName($all_strings)
->setDisplayName($proj->getName())
->setDisplayType(pht('Project'))
- ->setURI('/tag/'.$proj->getPrimarySlug().'/')
+ ->setURI($proj->getURI())
->setPHID($proj->getPHID())
->setIcon($proj->getDisplayIconIcon())
->setColor($proj->getColor())
->setPriorityType('proj')
->setClosed($closed);
+ $slug = $proj->getPrimarySlug();
+ if (strlen($slug)) {
+ $proj_result->setAutocomplete('#'.$slug);
+ }
+
$proj_result->setImageURI($proj->getProfileImageURI());
$results[] = $proj_result;
diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
--- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
+++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
@@ -16,6 +16,7 @@
private $closed;
private $tokenType;
private $unique;
+ private $autocomplete;
public function setIcon($icon) {
$this->icon = $icon;
@@ -114,6 +115,15 @@
return $this->color;
}
+ public function setAutocomplete($autocomplete) {
+ $this->autocomplete = $autocomplete;
+ return $this;
+ }
+
+ public function getAutocomplete() {
+ return $this->autocomplete;
+ }
+
public function getSortKey() {
// Put unique results (special parameter functions) ahead of other
// results.
@@ -142,6 +152,7 @@
$this->color,
$this->tokenType,
$this->unique ? 1 : null,
+ $this->autocomplete,
);
while (end($data) === null) {
array_pop($data);
diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php
--- a/src/view/form/control/PhabricatorRemarkupControl.php
+++ b/src/view/form/control/PhabricatorRemarkupControl.php
@@ -44,6 +44,9 @@
$root_id = celerity_generate_unique_node_id();
+ $user_datasource = new PhabricatorPeopleDatasource();
+ $proj_datasource = new PhabricatorProjectDatasource();
+
Javelin::initBehavior(
'phabricator-remarkup-assist',
array(
@@ -59,6 +62,20 @@
),
'disabled' => $this->getDisabled(),
'rootID' => $root_id,
+ 'autocompleteMap' => (object)array(
+ 64 => array( // "@"
+ 'datasourceURI' => $user_datasource->getDatasourceURI(),
+ 'headerIcon' => 'fa-user',
+ 'headerText' => pht('Find User:'),
+ 'hintText' => $user_datasource->getPlaceholderText(),
+ ),
+ 35 => array( // "#"
+ 'datasourceURI' => $proj_datasource->getDatasourceURI(),
+ 'headerIcon' => 'fa-briefcase',
+ 'headerText' => pht('Find Project:'),
+ 'hintText' => $proj_datasource->getPlaceholderText(),
+ ),
+ ),
));
Javelin::initBehavior('phabricator-tooltips', array());
diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css
--- a/webroot/rsrc/css/core/remarkup.css
+++ b/webroot/rsrc/css/core/remarkup.css
@@ -561,3 +561,52 @@
.device .remarkup-assist-nodevice {
display: none;
}
+
+.phuix-autocomplete {
+ position: absolute;
+ width: 300px;
+ box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.300);
+ background: #ffffff;
+ border: 1px solid {$blueborder};
+}
+
+.phuix-autocomplete-head {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 4px 8px;
+ background: {$lightgreybackground};
+ color: {$greytext};
+}
+
+.phuix-autocomplete-head .phui-icon-view {
+ margin-right: 4px;
+ color: {$greytext};
+}
+
+.phuix-autocomplete-echo {
+ margin-left: 4px;
+ color: {$lightgreytext};
+}
+
+.phuix-autocomplete-list a.jx-result {
+ display: block;
+ padding: 4px 8px;
+ font-size: {$normalfontsize};
+ border-top: 1px solid {$hoverborder};
+ color: {$darkgreytext};
+}
+
+.phuix-autocomplete-list a.jx-result .phui-icon-view {
+ margin-right: 4px;
+}
+
+.phuix-autocomplete-list a.jx-result:hover {
+ text-decoration: none;
+ background: {$hoverblue};
+}
+
+.phuix-autocomplete-list a.jx-result.focused,
+.phuix-autocomplete-list a.jx-result.focused:hover {
+ background: {$hoverblue};
+}
diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css
--- a/webroot/rsrc/css/core/z-index.css
+++ b/webroot/rsrc/css/core/z-index.css
@@ -150,6 +150,10 @@
z-index: 20;
}
+.phuix-autocomplete {
+ z-index: 21;
+}
+
.phuix-dropdown-menu {
z-index: 32;
}
diff --git a/webroot/rsrc/js/core/Prefab.js b/webroot/rsrc/js/core/Prefab.js
--- a/webroot/rsrc/js/core/Prefab.js
+++ b/webroot/rsrc/js/core/Prefab.js
@@ -325,7 +325,8 @@
sprite: fields[10],
color: fields[11],
tokenType: fields[12],
- unique: fields[13] || false
+ unique: fields[13] || false,
+ autocomplete: fields[14]
};
},
diff --git a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js
--- a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js
+++ b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js
@@ -7,6 +7,7 @@
* phabricator-textareautils
* javelin-workflow
* javelin-vector
+ * phuix-autocomplete
*/
JX.behavior('phabricator-remarkup-assist', function(config) {
@@ -293,4 +294,13 @@
assist(area, data.action, root, e.getNode('remarkup-assist'));
});
+ var autocomplete = new JX.PHUIXAutocomplete()
+ .setArea(area);
+
+ for (var k in config.autocompleteMap) {
+ autocomplete.addAutocomplete(k, config.autocompleteMap[k]);
+ }
+
+ autocomplete.start();
+
});
diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
@@ -0,0 +1,504 @@
+/**
+ * @provides phuix-autocomplete
+ * @requires javelin-install
+ * javelin-dom
+ * phuix-icon-view
+ * phabricator-prefab
+ */
+
+JX.install('PHUIXAutocomplete', {
+
+ construct: function() {
+ this._map = {};
+ this._datasources = {};
+ this._listNodes = [];
+ },
+
+ members: {
+ _area: null,
+ _active: false,
+ _cursorHead: null,
+ _cursorTail: null,
+ _pixelHead: null,
+ _pixelTail: null,
+ _map: null,
+ _datasource: null,
+ _datasources: null,
+ _value: null,
+ _node: null,
+ _echoNode: null,
+ _listNode: null,
+ _promptNode: null,
+ _focus: null,
+ _focusRef: null,
+ _listNodes: null,
+
+ setArea: function(area) {
+ this._area = area;
+ return this;
+ },
+
+ addAutocomplete: function(code, spec) {
+ this._map[code] = spec;
+ return this;
+ },
+
+ start: function() {
+ var area = this._area;
+
+ JX.DOM.listen(area, 'keypress', null, JX.bind(this, this._onkeypress));
+
+ JX.DOM.listen(
+ area,
+ ['click', 'keyup', 'keydown', 'keypress'],
+ null,
+ JX.bind(this, this._update));
+
+ var select = JX.bind(this, this._onselect);
+ JX.DOM.listen(this._getNode(), 'click', 'typeahead-result', select);
+
+ var device = JX.bind(this, this._ondevice);
+ JX.Stratcom.listen('phabricator-device-change', null, device);
+
+ // When the user clicks away from the textarea, deactivate.
+ var deactivate = JX.bind(this, this._deactivate);
+ JX.DOM.listen(area, 'blur', null, deactivate);
+ },
+
+ _getSpec: function() {
+ return this._map[this._active];
+ },
+
+ _ondevice: function() {
+ if (JX.Device.getDevice() != 'desktop') {
+ this._deactivate();
+ }
+ },
+
+ _activate: function(code) {
+ if (JX.Device.getDevice() != 'desktop') {
+ return;
+ }
+
+ if (!this._map[code]) {
+ return;
+ }
+
+ var area = this._area;
+ var range = JX.TextAreaUtils.getSelectionRange(area);
+
+ // Check the character immediately before the trigger character. We'll
+ // only activate the typeahead if it's something that we think a user
+ // might reasonably want to autocomplete after, like a space, newline,
+ // or open parenthesis. For example, if a user types "alincoln@",
+ // the prior letter will be the last "n" in "alincoln". They are probably
+ // typing an email address, not a username, so we don't activate the
+ // autocomplete.
+ var head = range.start;
+ var prior;
+ if (head > 1) {
+ prior = area.value.substring(head - 2, head - 1);
+ } else {
+ prior = '<start>';
+ }
+
+ switch (prior) {
+ case '<start>':
+ case ' ':
+ case '\n':
+ case '\t':
+ case '(': // Might be "(@username, what do you think?)".
+ case '-': // Might be an unnumbered list.
+ case '.': // Might be a numbered list.
+ case '|': // Might be a table cell.
+ // We'll let these autocomplete.
+ break;
+ default:
+ // We bail out on anything else, since the user is probably not
+ // typing a username or project tag.
+ return;
+ }
+
+ this._cursorHead = head;
+ this._cursorTail = range.end;
+ this._pixelHead = JX.TextAreaUtils.getPixelDimensions(
+ area,
+ range.start,
+ range.end);
+
+ var spec = this._map[code];
+ if (!this._datasources[code]) {
+ var datasource = new JX.TypeaheadOnDemandSource(spec.datasourceURI);
+ datasource.listen(
+ 'resultsready',
+ JX.bind(this, this._onresults, code));
+
+ datasource.setTransformer(JX.bind(this, this._transformresult));
+
+ this._datasources[code] = datasource;
+ }
+
+ this._datasource = this._datasources[code];
+ this._active = code;
+
+ var head_icon = new JX.PHUIXIconView()
+ .setIcon(spec.headerIcon)
+ .getNode();
+ var head_text = spec.headerText;
+
+ var node = this._getPromptNode();
+ JX.DOM.setContent(node, [head_icon, head_text]);
+ },
+
+ _transformresult: function(fields) {
+ var map = JX.Prefab.transformDatasourceResults(fields);
+
+ var icon;
+ if (map.icon) {
+ icon = new JX.PHUIXIconView()
+ .setIcon(map.icon)
+ .getNode();
+ }
+
+ map.display = [icon, map.displayName];
+
+ return map;
+ },
+
+ _deactivate: function() {
+ var node = this._getNode();
+ JX.DOM.hide(node);
+
+ this._active = false;
+ },
+
+ _onkeypress: function(e) {
+ var r = e.getRawEvent();
+
+ if (r.metaKey || r.altKey || r.ctrlKey) {
+ return;
+ }
+
+ var code = r.charCode;
+ if (this._map[code]) {
+ setTimeout(JX.bind(this, this._activate, code), 0);
+ }
+ },
+
+ _onresults: function(code, nodes, value) {
+ if (code !== this._active) {
+ return;
+ }
+
+ if (value !== this._value) {
+ return;
+ }
+
+ var list = this._getListNode();
+ JX.DOM.setContent(list, nodes);
+
+ this._listNodes = nodes;
+
+ var old_ref = this._focusRef;
+ this._clearFocus();
+
+ for (var ii = 0; ii < nodes.length; ii++) {
+ if (nodes[ii].rel == old_ref) {
+ this._setFocus(ii);
+ break;
+ }
+ }
+
+ if (this._focus === null && nodes.length) {
+ this._setFocus(0);
+ }
+ },
+
+ _setFocus: function(idx) {
+ if (!this._listNodes[idx]) {
+ this._clearFocus();
+ return false;
+ }
+
+ if (this._focus !== null) {
+ JX.DOM.alterClass(this._listNodes[this._focus], 'focused', false);
+ }
+
+ this._focus = idx;
+ this._focusRef = this._listNodes[idx].rel;
+ JX.DOM.alterClass(this._listNodes[idx], 'focused', true);
+
+ return true;
+ },
+
+ _changeFocus: function(delta) {
+ if (this._focus === null) {
+ return false;
+ }
+
+ return this._setFocus(this._focus + delta);
+ },
+
+ _clearFocus: function() {
+ this._focus = null;
+ this._focusRef = null;
+ },
+
+ _onselect: function (e) {
+ var target = e.getNode('typeahead-result');
+
+ for (var ii = 0; ii < this._listNodes.length; ii++) {
+ if (this._listNodes[ii] === target) {
+ this._setFocus(ii);
+ this._autocomplete();
+ break;
+ }
+ }
+
+ this._deactivate();
+ e.kill();
+ },
+
+ _getSuffixes: function() {
+ return[' ', ':', ','];
+ },
+
+ _trim: function(str) {
+ var suffixes = this._getSuffixes();
+ for (var ii = 0; ii < suffixes.length; ii++) {
+ if (str.substring(str.length - suffixes[ii].length) == suffixes[ii]) {
+ str = str.substring(0, str.length - suffixes[ii].length);
+ }
+ }
+ return str;
+ },
+
+ _update: function(e) {
+ if (!this._active) {
+ return;
+ }
+
+ var special = e.getSpecialKey();
+
+ // Deactivate if the user types escape.
+ if (special == 'esc') {
+ this._deactivate();
+ e.kill();
+ return;
+ }
+
+ var area = this._area;
+
+ if (e.getType() == 'keydown') {
+ if (special == 'up' || special == 'down') {
+ var delta = (special == 'up') ? -1 : +1;
+ if (!this._changeFocus(delta)) {
+ this._deactivate();
+ }
+ e.kill();
+ return;
+ }
+ }
+
+ if (special == 'tab' || special == 'return') {
+ var r = e.getRawEvent();
+ if (r.shiftKey && special == 'tab') {
+ // Don't treat "Shift + Tab" as an autocomplete action. Instead,
+ // let it through normally so the focus shifts to the previous
+ // control.
+ this._deactivate();
+ return;
+ }
+
+ // If we autocomplete, we're done. Otherwise, just eat the event. This
+ // happens if you type too fast and try to tab complete before results
+ // load.
+ if (this._autocomplete()) {
+ this._deactivate();
+ }
+
+ e.kill();
+ return;
+ }
+
+ // Deactivate if the user moves the cursor to the left of the assist
+ // range. For example, they might press the "left" arrow to move the
+ // cursor to the left, or click in the textarea prior to the active
+ // range.
+ var range = JX.TextAreaUtils.getSelectionRange(area);
+ if (range.start < this._cursorHead) {
+ this._deactivate();
+ return;
+ }
+
+ // Deactivate if the user moves the cursor to the right of the assist
+ // range. For example, they might click later in the document. If the user
+ // is pressing the "right" arrow key, they are not allowed to move the
+ // cursor beyond the existing end of the text range. If they are pressing
+ // other keys, assume they're typing and allow the tail to move forward
+ // one character.
+ var margin;
+ if (special == 'right') {
+ margin = 0;
+ } else {
+ margin = 1;
+ }
+
+ var tail = this._cursorTail;
+
+ if ((range.start > tail + margin) || (range.end > tail + margin)) {
+ this._deactivate();
+ return;
+ }
+
+ this._cursorTail = Math.max(this._cursorTail, range.end);
+
+ var text = area.value.substring(
+ this._cursorHead,
+ this._cursorTail);
+
+ this._value = text;
+
+ var pixels = JX.TextAreaUtils.getPixelDimensions(
+ area,
+ range.start,
+ range.end);
+
+ var x = this._pixelHead.start.x;
+ var y = Math.max(this._pixelHead.end.y, pixels.end.y) + 24;
+
+ var trim = this._trim(text);
+
+ this._datasource.didChange(trim);
+
+ var node = this._getNode();
+ node.style.left = x + 'px';
+ node.style.top = y + 'px';
+ JX.DOM.show(node);
+
+ var echo = this._getEchoNode();
+ var hint = trim;
+ if (!hint.length) {
+ hint = this._getSpec().hintText;
+ }
+
+ JX.DOM.setContent(echo, hint);
+ },
+
+ _autocomplete: function() {
+ if (this._focus === null) {
+ return false;
+ }
+
+ var area = this._area;
+ var head = this._cursorHead;
+ var tail = this._cursorTail;
+
+ var text = area.value;
+
+ var ref = this._focusRef;
+ var result = this._datasource.getResult(ref);
+ if (!result) {
+ return false;
+ }
+
+ ref = result.autocomplete;
+ if (!ref || !ref.length) {
+ return false;
+ }
+
+ // If the user types a string like "@username:" (with a trailing comma),
+ // then presses tab or return to pick the completion, don't destroy the
+ // trailing character.
+ var suffixes = this._getSuffixes();
+ var value = this._value;
+ for (var ii = 0; ii < suffixes.length; ii++) {
+ var last = value.substring(value.length - suffixes[ii].length);
+ if (last == suffixes[ii]) {
+ ref += suffixes[ii];
+ break;
+ }
+ }
+
+ area.value = text.substring(0, head - 1) + ref + text.substring(tail);
+
+ var end = head + ref.length;
+ JX.TextAreaUtils.setSelectionRange(area, end, end);
+
+ return true;
+ },
+
+ _getNode: function() {
+ if (!this._node) {
+ var head = this._getHeadNode();
+ var list = this._getListNode();
+
+ this._node = JX.$N(
+ 'div',
+ {
+ className: 'phuix-autocomplete',
+ style: {
+ display: 'none'
+ }
+ },
+ [head, list]);
+
+ JX.DOM.hide(this._node);
+
+ document.body.appendChild(this._node);
+ }
+ return this._node;
+ },
+
+ _getHeadNode: function() {
+ if (!this._headNode) {
+ this._headNode = JX.$N(
+ 'div',
+ {
+ className: 'phuix-autocomplete-head'
+ },
+ [
+ this._getPromptNode(),
+ this._getEchoNode()
+ ]);
+ }
+
+ return this._headNode;
+ },
+
+ _getPromptNode: function() {
+ if (!this._promptNode) {
+ this._promptNode = JX.$N(
+ 'span',
+ {
+ className: 'phuix-autocomplete-prompt',
+ });
+ }
+ return this._promptNode;
+ },
+
+ _getEchoNode: function() {
+ if (!this._echoNode) {
+ this._echoNode = JX.$N(
+ 'span',
+ {
+ className: 'phuix-autocomplete-echo'
+ });
+ }
+ return this._echoNode;
+ },
+
+ _getListNode: function() {
+ if (!this._listNode) {
+ this._listNode = JX.$N(
+ 'div',
+ {
+ className: 'phuix-autocomplete-list'
+ });
+ }
+ return this._listNode;
+ }
+
+ }
+
+});

File Metadata

Mime Type
text/plain
Expires
Sat, Nov 9, 7:09 AM (1 w, 2 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6712831
Default Alt Text
D15029.diff (25 KB)

Event Timeline