mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-18 12:52:42 +01:00
Add a basic remarkup typeahead for users and projects
Summary: Ref T3725. This probably has 900,000 bugs. This will need updates for subprojects/milestones. Test Plan: - Tested very gently in Safari, Firefox and Chrome. - Reasonable inputs appear to work. - Clicking, escape, tab, return, arrow keys work OK? Reviewers: chad Reviewed By: chad Maniphest Tasks: T3725 Differential Revision: https://secure.phabricator.com/D15029
This commit is contained in:
parent
aadc1b7bf0
commit
5d6dd7df7d
10 changed files with 648 additions and 35 deletions
|
@ -7,8 +7,8 @@
|
||||||
*/
|
*/
|
||||||
return array(
|
return array(
|
||||||
'names' => array(
|
'names' => array(
|
||||||
'core.pkg.css' => '1eed0b4f',
|
'core.pkg.css' => 'f30d5cbd',
|
||||||
'core.pkg.js' => '6ae03393',
|
'core.pkg.js' => '1f5f365a',
|
||||||
'darkconsole.pkg.js' => 'e7393ebb',
|
'darkconsole.pkg.js' => 'e7393ebb',
|
||||||
'differential.pkg.css' => '2de124c9',
|
'differential.pkg.css' => '2de124c9',
|
||||||
'differential.pkg.js' => 'f83532f8',
|
'differential.pkg.js' => 'f83532f8',
|
||||||
|
@ -102,9 +102,9 @@ return array(
|
||||||
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
|
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
|
||||||
'rsrc/css/application/uiexample/example.css' => '528b19de',
|
'rsrc/css/application/uiexample/example.css' => '528b19de',
|
||||||
'rsrc/css/core/core.css' => 'a76cefc9',
|
'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/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/diviner/diviner-shared.css' => 'aa3656aa',
|
||||||
'rsrc/css/font/font-aleo.css' => '8bdb2835',
|
'rsrc/css/font/font-aleo.css' => '8bdb2835',
|
||||||
'rsrc/css/font/font-awesome.css' => 'c43323c5',
|
'rsrc/css/font/font-awesome.css' => 'c43323c5',
|
||||||
|
@ -457,7 +457,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' => '666c80c5',
|
'rsrc/js/core/Prefab.js' => 'a15cbd65',
|
||||||
'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',
|
||||||
|
@ -487,7 +487,7 @@ return array(
|
||||||
'rsrc/js/core/behavior-object-selector.js' => '49b73b36',
|
'rsrc/js/core/behavior-object-selector.js' => '49b73b36',
|
||||||
'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
|
'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
|
||||||
'rsrc/js/core/behavior-phabricator-nav.js' => '56a1ca03',
|
'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-refresh-csrf.js' => 'ab2f381b',
|
||||||
'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e',
|
'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e',
|
||||||
'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e',
|
'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e',
|
||||||
|
@ -506,6 +506,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/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',
|
||||||
|
@ -639,7 +640,7 @@ return array(
|
||||||
'javelin-behavior-phabricator-notification-example' => '8ce821c5',
|
'javelin-behavior-phabricator-notification-example' => '8ce821c5',
|
||||||
'javelin-behavior-phabricator-object-selector' => '49b73b36',
|
'javelin-behavior-phabricator-object-selector' => '49b73b36',
|
||||||
'javelin-behavior-phabricator-oncopy' => '2926fff2',
|
'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-reveal-content' => '60821bc7',
|
||||||
'javelin-behavior-phabricator-search-typeahead' => '0b7a4f6e',
|
'javelin-behavior-phabricator-search-typeahead' => '0b7a4f6e',
|
||||||
'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6',
|
'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6',
|
||||||
|
@ -758,8 +759,8 @@ 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' => '666c80c5',
|
'phabricator-prefab' => 'a15cbd65',
|
||||||
'phabricator-remarkup-css' => 'b6ad82e4',
|
'phabricator-remarkup-css' => 'b748dc17',
|
||||||
'phabricator-search-results-css' => '7dea472c',
|
'phabricator-search-results-css' => '7dea472c',
|
||||||
'phabricator-shaped-request' => '7cbe244b',
|
'phabricator-shaped-request' => '7cbe244b',
|
||||||
'phabricator-side-menu-view-css' => '91b7a42c',
|
'phabricator-side-menu-view-css' => '91b7a42c',
|
||||||
|
@ -780,7 +781,7 @@ return array(
|
||||||
'phabricator-uiexample-reactor-select' => 'a155550f',
|
'phabricator-uiexample-reactor-select' => 'a155550f',
|
||||||
'phabricator-uiexample-reactor-sendclass' => '1def2711',
|
'phabricator-uiexample-reactor-sendclass' => '1def2711',
|
||||||
'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
|
'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
|
||||||
'phabricator-zindex-css' => '57ddcaa2',
|
'phabricator-zindex-css' => 'a36a45da',
|
||||||
'phame-css' => 'dac8fdf2',
|
'phame-css' => 'dac8fdf2',
|
||||||
'pholio-css' => '95174bdd',
|
'pholio-css' => '95174bdd',
|
||||||
'pholio-edit-css' => '3ad9d1ee',
|
'pholio-edit-css' => '3ad9d1ee',
|
||||||
|
@ -833,6 +834,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-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',
|
||||||
|
@ -1050,6 +1052,16 @@ return array(
|
||||||
'331b1611' => array(
|
'331b1611' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
),
|
),
|
||||||
|
'340c8eff' => array(
|
||||||
|
'javelin-behavior',
|
||||||
|
'javelin-stratcom',
|
||||||
|
'javelin-dom',
|
||||||
|
'phabricator-phtize',
|
||||||
|
'phabricator-textareautils',
|
||||||
|
'javelin-workflow',
|
||||||
|
'javelin-vector',
|
||||||
|
'phuix-autocomplete',
|
||||||
|
),
|
||||||
'3ab51e2c' => array(
|
'3ab51e2c' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-behavior-device',
|
'javelin-behavior-device',
|
||||||
|
@ -1293,18 +1305,6 @@ return array(
|
||||||
'javelin-vector',
|
'javelin-vector',
|
||||||
'differential-inline-comment-editor',
|
'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(
|
'66dd6e9e' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-behavior-device',
|
'javelin-behavior-device',
|
||||||
|
@ -1587,6 +1587,18 @@ 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',
|
||||||
|
@ -1742,15 +1754,6 @@ return array(
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
'javelin-util',
|
'javelin-util',
|
||||||
),
|
),
|
||||||
'b60b6d9b' => array(
|
|
||||||
'javelin-behavior',
|
|
||||||
'javelin-stratcom',
|
|
||||||
'javelin-dom',
|
|
||||||
'phabricator-phtize',
|
|
||||||
'phabricator-textareautils',
|
|
||||||
'javelin-workflow',
|
|
||||||
'javelin-vector',
|
|
||||||
),
|
|
||||||
'b6993408' => array(
|
'b6993408' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-stratcom',
|
'javelin-stratcom',
|
||||||
|
@ -1794,6 +1797,12 @@ 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',
|
||||||
|
|
|
@ -59,12 +59,15 @@ final class PhabricatorPeopleDatasource
|
||||||
$closed = pht('Mailing List');
|
$closed = pht('Mailing List');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$username = $user->getUsername();
|
||||||
|
|
||||||
$result = id(new PhabricatorTypeaheadResult())
|
$result = id(new PhabricatorTypeaheadResult())
|
||||||
->setName($user->getFullName())
|
->setName($user->getFullName())
|
||||||
->setURI('/p/'.$user->getUsername())
|
->setURI('/p/'.$username.'/')
|
||||||
->setPHID($user->getPHID())
|
->setPHID($user->getPHID())
|
||||||
->setPriorityString($user->getUsername())
|
->setPriorityString($username)
|
||||||
->setPriorityType('user')
|
->setPriorityType('user')
|
||||||
|
->setAutocomplete('@'.$username)
|
||||||
->setClosed($closed);
|
->setClosed($closed);
|
||||||
|
|
||||||
if ($user->getIsMailingList()) {
|
if ($user->getIsMailingList()) {
|
||||||
|
|
|
@ -65,13 +65,18 @@ final class PhabricatorProjectDatasource
|
||||||
->setName($all_strings)
|
->setName($all_strings)
|
||||||
->setDisplayName($proj->getName())
|
->setDisplayName($proj->getName())
|
||||||
->setDisplayType(pht('Project'))
|
->setDisplayType(pht('Project'))
|
||||||
->setURI('/tag/'.$proj->getPrimarySlug().'/')
|
->setURI($proj->getURI())
|
||||||
->setPHID($proj->getPHID())
|
->setPHID($proj->getPHID())
|
||||||
->setIcon($proj->getDisplayIconIcon())
|
->setIcon($proj->getDisplayIconIcon())
|
||||||
->setColor($proj->getColor())
|
->setColor($proj->getColor())
|
||||||
->setPriorityType('proj')
|
->setPriorityType('proj')
|
||||||
->setClosed($closed);
|
->setClosed($closed);
|
||||||
|
|
||||||
|
$slug = $proj->getPrimarySlug();
|
||||||
|
if (strlen($slug)) {
|
||||||
|
$proj_result->setAutocomplete('#'.$slug);
|
||||||
|
}
|
||||||
|
|
||||||
$proj_result->setImageURI($proj->getProfileImageURI());
|
$proj_result->setImageURI($proj->getProfileImageURI());
|
||||||
|
|
||||||
$results[] = $proj_result;
|
$results[] = $proj_result;
|
||||||
|
|
|
@ -16,6 +16,7 @@ final class PhabricatorTypeaheadResult extends Phobject {
|
||||||
private $closed;
|
private $closed;
|
||||||
private $tokenType;
|
private $tokenType;
|
||||||
private $unique;
|
private $unique;
|
||||||
|
private $autocomplete;
|
||||||
|
|
||||||
public function setIcon($icon) {
|
public function setIcon($icon) {
|
||||||
$this->icon = $icon;
|
$this->icon = $icon;
|
||||||
|
@ -114,6 +115,15 @@ final class PhabricatorTypeaheadResult extends Phobject {
|
||||||
return $this->color;
|
return $this->color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setAutocomplete($autocomplete) {
|
||||||
|
$this->autocomplete = $autocomplete;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAutocomplete() {
|
||||||
|
return $this->autocomplete;
|
||||||
|
}
|
||||||
|
|
||||||
public function getSortKey() {
|
public function getSortKey() {
|
||||||
// Put unique results (special parameter functions) ahead of other
|
// Put unique results (special parameter functions) ahead of other
|
||||||
// results.
|
// results.
|
||||||
|
@ -142,6 +152,7 @@ final class PhabricatorTypeaheadResult extends Phobject {
|
||||||
$this->color,
|
$this->color,
|
||||||
$this->tokenType,
|
$this->tokenType,
|
||||||
$this->unique ? 1 : null,
|
$this->unique ? 1 : null,
|
||||||
|
$this->autocomplete,
|
||||||
);
|
);
|
||||||
while (end($data) === null) {
|
while (end($data) === null) {
|
||||||
array_pop($data);
|
array_pop($data);
|
||||||
|
|
|
@ -44,6 +44,9 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
|
||||||
|
|
||||||
$root_id = celerity_generate_unique_node_id();
|
$root_id = celerity_generate_unique_node_id();
|
||||||
|
|
||||||
|
$user_datasource = new PhabricatorPeopleDatasource();
|
||||||
|
$proj_datasource = new PhabricatorProjectDatasource();
|
||||||
|
|
||||||
Javelin::initBehavior(
|
Javelin::initBehavior(
|
||||||
'phabricator-remarkup-assist',
|
'phabricator-remarkup-assist',
|
||||||
array(
|
array(
|
||||||
|
@ -59,6 +62,20 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
|
||||||
),
|
),
|
||||||
'disabled' => $this->getDisabled(),
|
'disabled' => $this->getDisabled(),
|
||||||
'rootID' => $root_id,
|
'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());
|
Javelin::initBehavior('phabricator-tooltips', array());
|
||||||
|
|
||||||
|
|
|
@ -561,3 +561,52 @@ var.remarkup-assist-textarea {
|
||||||
.device .remarkup-assist-nodevice {
|
.device .remarkup-assist-nodevice {
|
||||||
display: none;
|
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};
|
||||||
|
}
|
||||||
|
|
|
@ -150,6 +150,10 @@ div.jx-typeahead-results {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phuix-autocomplete {
|
||||||
|
z-index: 21;
|
||||||
|
}
|
||||||
|
|
||||||
.phuix-dropdown-menu {
|
.phuix-dropdown-menu {
|
||||||
z-index: 32;
|
z-index: 32;
|
||||||
}
|
}
|
||||||
|
|
|
@ -325,7 +325,8 @@ JX.install('Prefab', {
|
||||||
sprite: fields[10],
|
sprite: fields[10],
|
||||||
color: fields[11],
|
color: fields[11],
|
||||||
tokenType: fields[12],
|
tokenType: fields[12],
|
||||||
unique: fields[13] || false
|
unique: fields[13] || false,
|
||||||
|
autocomplete: fields[14]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
* phabricator-textareautils
|
* phabricator-textareautils
|
||||||
* javelin-workflow
|
* javelin-workflow
|
||||||
* javelin-vector
|
* javelin-vector
|
||||||
|
* phuix-autocomplete
|
||||||
*/
|
*/
|
||||||
|
|
||||||
JX.behavior('phabricator-remarkup-assist', function(config) {
|
JX.behavior('phabricator-remarkup-assist', function(config) {
|
||||||
|
@ -293,4 +294,13 @@ JX.behavior('phabricator-remarkup-assist', function(config) {
|
||||||
assist(area, data.action, root, e.getNode('remarkup-assist'));
|
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();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
504
webroot/rsrc/js/phuix/PHUIXAutocomplete.js
Normal file
504
webroot/rsrc/js/phuix/PHUIXAutocomplete.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in a new issue