diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -113,17 +113,49 @@ $this->requireResource('typeahead-browse-css'); $this->initBehavior('typeahead-browse'); - $markup = phutil_tag( + $input_id = celerity_generate_unique_node_id(); + $frame_id = celerity_generate_unique_node_id(); + + $config = array( + 'inputID' => $input_id, + 'frameID' => $frame_id, + 'uri' => (string)$request->getRequestURI(), + ); + $this->initBehavior('typeahead-search', $config); + + $search = javelin_tag( + 'input', + array( + 'type' => 'text', + 'id' => $input_id, + 'class' => 'typeahead-browse-input', + 'autocomplete' => 'off', + 'placeholder' => $source->getPlaceholderText(), + )); + + $frame = phutil_tag( 'div', array( 'class' => 'typeahead-browse-frame', + 'id' => $frame_id, ), $markup); + $browser = array( + phutil_tag( + 'div', + array( + 'class' => 'typeahead-browse-header', + ), + $search), + $frame, + ); + return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setRenderDialogAsDiv(true) ->setTitle(get_class($source)) // TODO: Provide nice names. - ->appendChild($markup) + ->appendChild($browser) ->addCancelButton('/', pht('Close')); } diff --git a/webroot/rsrc/css/aphront/typeahead-browse.css b/webroot/rsrc/css/aphront/typeahead-browse.css --- a/webroot/rsrc/css/aphront/typeahead-browse.css +++ b/webroot/rsrc/css/aphront/typeahead-browse.css @@ -32,3 +32,16 @@ height: 260px; border: 1px solid {$lightgreyborder}; } + +.typeahead-browse-frame.loading { + opacity: 0.8; +} + +.typeahead-browse-header { + padding: 4px 0; +} + +input.typeahead-browse-input { + margin: 0; + width: 100%; +} diff --git a/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js b/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js @@ -0,0 +1,56 @@ +/** + * @provides javelin-behavior-typeahead-search + * @requires javelin-behavior + * javelin-stratcom + * javelin-workflow + * javelin-dom + */ + +JX.behavior('typeahead-search', function(config) { + var input = JX.$(config.inputID); + var frame = JX.$(config.frameID); + var last = input.value; + + function update() { + if (input.value == last) { + // This is some kind of non-input keypress like an arrow key. Don't + // send a query to the server. + return; + } + + // Call load() in a little while. If the user hasn't typed anything else, + // we'll send a request to get results. + setTimeout(JX.bind(null, load, input.value), 100); + } + + function load(value) { + if (value != input.value) { + // The user has typed some more text, so don't send a request yet. We + // want to wait for them to stop typing. + return; + } + + JX.DOM.alterClass(frame, 'loading', true); + new JX.Workflow(config.uri, {q: value}) + .setHandler(function(r) { + if (value != input.value) { + // The user typed some more stuff while the request was in flight, + // so ignore the response. + return; + } + + last = input.value; + JX.DOM.setContent(frame, JX.$H(r.markup)); + JX.DOM.alterClass(frame, 'loading', false); + }) + .start(); + } + + JX.DOM.listen(input, ['keydown', 'keypress', 'keyup'], null, function() { + // We need to delay this to actually read the value after the keypress. + setTimeout(update, 0); + }); + + JX.DOM.focus(input); + +});