Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php
index 07f979a06f..f3f406e8d5 100644
--- a/src/__celerity_resource_map__.php
+++ b/src/__celerity_resource_map__.php
@@ -1,154 +1,195 @@
<?php
/**
* This file is automatically generated. Use 'celerity_mapper.php' to rebuild
* it.
* @generated
*/
celerity_register_resource_map(array(
'aphront-dialog-view-css' =>
array(
'path' => '/res/771b987d/rsrc/css/aphront/dialog-view.css',
'type' => 'css',
'requires' =>
array(
),
),
'aphront-form-view-css' =>
array(
- 'path' => '/res/20ebc99b/rsrc/css/aphront/form-view.css',
+ 'path' => '/res/17285e65/rsrc/css/aphront/form-view.css',
'type' => 'css',
'requires' =>
array(
),
),
'aphront-panel-view-css' =>
array(
'path' => '/res/d1ce0c3d/rsrc/css/aphront/panel-view.css',
'type' => 'css',
'requires' =>
array(
),
),
'aphront-side-nav-view-css' =>
array(
'path' => '/res/1a16f19a/rsrc/css/aphront/side-nav-view.css',
'type' => 'css',
'requires' =>
array(
),
),
'aphront-table-view-css' =>
array(
'path' => '/res/52b0191f/rsrc/css/aphront/table-view.css',
'type' => 'css',
'requires' =>
array(
),
),
+ 'aphront-tokenizer-control-css' =>
+ array(
+ 'path' => '/res/a3d23074/rsrc/css/aphront/tokenizer.css',
+ 'type' => 'css',
+ 'requires' =>
+ array(
+ 0 => 'aphront-typeahead-control-css',
+ ),
+ ),
+ 'aphront-typeahead-control-css' =>
+ array(
+ 'path' => '/res/928df9f0/rsrc/css/aphront/typeahead.css',
+ 'type' => 'css',
+ 'requires' =>
+ array(
+ ),
+ ),
'phabricator-standard-page-view' =>
array(
'path' => '/res/0eef6905/rsrc/css/application/base/standard-page-view.css',
'type' => 'css',
'requires' =>
array(
),
),
'differential-changeset-view-css' =>
array(
'path' => '/res/921d3a0c/rsrc/css/application/differential/changeset-view.css',
'type' => 'css',
'requires' =>
array(
),
),
'differential-core-view-css' =>
array(
'path' => '/res/f750b85d/rsrc/css/application/differential/core.css',
'type' => 'css',
'requires' =>
array(
),
),
'differential-table-of-contents-css' =>
array(
'path' => '/res/ebf6641c/rsrc/css/application/differential/table-of-contents.css',
'type' => 'css',
'requires' =>
array(
),
),
'phabricator-directory-css' =>
array(
'path' => '/res/6a000601/rsrc/css/application/directory/phabricator-directory.css',
'type' => 'css',
'requires' =>
array(
),
),
'phabricator-core-buttons-css' =>
array(
'path' => '/res/6e348ba4/rsrc/css/core/buttons.css',
'type' => 'css',
'requires' =>
array(
),
),
'phabricator-core-css' =>
array(
'path' => '/res/39ce37c2/rsrc/css/core/core.css',
'type' => 'css',
'requires' =>
array(
),
),
'syntax-highlighting-css' =>
array(
'path' => '/res/fb673ece/rsrc/css/core/syntax.css',
'type' => 'css',
'requires' =>
array(
),
),
+ 'javelin-behavior-aphront-basic-tokenizer' =>
+ array(
+ 'path' => '/res/12de8502/rsrc/js/application/core/behavior-tokenizer.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ ),
+ ),
'javelin-behavior-differential-populate' =>
array(
'path' => '/res/b419291a/rsrc/js/application/differential/behavior-populate.js',
'type' => 'js',
'requires' =>
array(
),
),
'javelin-init-dev' =>
array(
'path' => '/res/c57a9e89/rsrc/js/javelin/init.dev.js',
'type' => 'js',
'requires' =>
array(
),
),
'javelin-init-prod' =>
array(
'path' => '/res/f0172c54/rsrc/js/javelin/init.min.js',
'type' => 'js',
'requires' =>
array(
),
),
'javelin-lib-dev' =>
array(
'path' => '/res/3e747182/rsrc/js/javelin/javelin.dev.js',
'type' => 'js',
'requires' =>
array(
),
),
'javelin-lib-prod' =>
array(
'path' => '/res/9438670e/rsrc/js/javelin/javelin.min.js',
'type' => 'js',
'requires' =>
array(
),
),
+ 'javelin-typeahead-dev' =>
+ array(
+ 'path' => '/res/c81c0f01/rsrc/js/javelin/typeahead.dev.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ ),
+ ),
+ 'javelin-typeahead-prod' =>
+ array(
+ 'path' => '/res/871c9b0f/rsrc/js/javelin/typeahead.min.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ ),
+ ),
));
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 6d9c2ecb28..1d3f88e9d0 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,242 +1,248 @@
<?php
/**
* This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
* @generated
*/
phutil_register_library_map(array(
'class' =>
array(
'Aphront404Response' => 'aphront/response/404',
'AphrontAjaxResponse' => 'aphront/response/ajax',
'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration',
'AphrontController' => 'aphront/controller',
'AphrontDatabaseConnection' => 'storage/connection/base',
'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration',
'AphrontDefaultApplicationController' => 'aphront/default/controller',
'AphrontDialogResponse' => 'aphront/response/dialog',
'AphrontDialogView' => 'view/dialog',
'AphrontErrorView' => 'view/form/error',
'AphrontFileResponse' => 'aphront/response/file',
'AphrontFormControl' => 'view/form/control/base',
'AphrontFormFileControl' => 'view/form/control/file',
'AphrontFormMarkupControl' => 'view/form/control/markup',
'AphrontFormSelectControl' => 'view/form/control/select',
'AphrontFormStaticControl' => 'view/form/control/static',
'AphrontFormSubmitControl' => 'view/form/control/submit',
'AphrontFormTextAreaControl' => 'view/form/control/textarea',
'AphrontFormTextControl' => 'view/form/control/text',
+ 'AphrontFormTokenizerControl' => 'view/form/control/tokenizer',
'AphrontFormView' => 'view/form/base',
'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql',
'AphrontNullView' => 'view/null',
'AphrontPageView' => 'view/page/base',
'AphrontPanelView' => 'view/layout/panel',
'AphrontQueryConnectionException' => 'storage/exception/connection',
'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost',
'AphrontQueryCountException' => 'storage/exception/count',
'AphrontQueryException' => 'storage/exception/base',
'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing',
'AphrontQueryParameterException' => 'storage/exception/parameter',
'AphrontQueryRecoverableException' => 'storage/exception/recoverable',
'AphrontRedirectResponse' => 'aphront/response/redirect',
'AphrontRequest' => 'aphront/request',
'AphrontResponse' => 'aphront/response/base',
'AphrontSideNavView' => 'view/layout/sidenav',
'AphrontTableView' => 'view/control/table',
'AphrontURIMapper' => 'aphront/mapper',
'AphrontView' => 'view/base',
'AphrontWebpageResponse' => 'aphront/response/webpage',
'CelerityAPI' => 'infratructure/celerity/api',
'CelerityResourceController' => 'infratructure/celerity/controller',
'CelerityResourceMap' => 'infratructure/celerity/map',
'CelerityStaticResourceResponse' => 'infratructure/celerity/response',
'ConduitAPIMethod' => 'applications/conduit/method/base',
'ConduitAPIRequest' => 'applications/conduit/protocol/request',
'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect',
'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/creatediff',
'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/setdiffproperty',
'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload',
'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find',
'ConduitException' => 'applications/conduit/protocol/exception',
'DifferentialAction' => 'applications/differential/constants/action',
'DifferentialChangeType' => 'applications/differential/constants/changetype',
'DifferentialChangeset' => 'applications/differential/storage/changeset',
'DifferentialChangesetDetailView' => 'applications/differential/view/changesetdetailview',
'DifferentialChangesetListView' => 'applications/differential/view/changesetlistview',
'DifferentialChangesetParser' => 'applications/differential/parser/changeset',
'DifferentialChangesetViewController' => 'applications/differential/controller/changesetview',
'DifferentialController' => 'applications/differential/controller/base',
'DifferentialDAO' => 'applications/differential/storage/base',
'DifferentialDiff' => 'applications/differential/storage/diff',
'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty',
'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents',
'DifferentialDiffViewController' => 'applications/differential/controller/diffview',
'DifferentialHunk' => 'applications/differential/storage/hunk',
'DifferentialLintStatus' => 'applications/differential/constants/lintstatus',
'DifferentialRevision' => 'applications/differential/storage/revision',
'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem',
'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit',
'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus',
'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus',
'Javelin' => 'infratructure/javelin/api',
'LiskDAO' => 'storage/lisk/dao',
'PhabricatorConduitAPIController' => 'applications/conduit/controller/api',
'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog',
'PhabricatorConduitConsoleController' => 'applications/conduit/controller/console',
'PhabricatorConduitController' => 'applications/conduit/controller/base',
'PhabricatorConduitDAO' => 'applications/conduit/storage/base',
'PhabricatorConduitLogController' => 'applications/conduit/controller/log',
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog',
'PhabricatorController' => 'applications/base/controller/base',
'PhabricatorDirectoryCategory' => 'applications/directory/storage/category',
'PhabricatorDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete',
'PhabricatorDirectoryCategoryEditController' => 'applications/directory/controller/categoryedit',
'PhabricatorDirectoryCategoryListController' => 'applications/directory/controller/categorylist',
'PhabricatorDirectoryController' => 'applications/directory/controller/base',
'PhabricatorDirectoryDAO' => 'applications/directory/storage/base',
'PhabricatorDirectoryItem' => 'applications/directory/storage/item',
'PhabricatorDirectoryItemDeleteController' => 'applications/directory/controller/itemdelete',
'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit',
'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist',
'PhabricatorDirectoryMainController' => 'applications/directory/controller/main',
'PhabricatorFile' => 'applications/files/storage/file',
'PhabricatorFileController' => 'applications/files/controller/base',
'PhabricatorFileDAO' => 'applications/files/storage/base',
'PhabricatorFileListController' => 'applications/files/controller/list',
'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob',
'PhabricatorFileURI' => 'applications/files/uri',
'PhabricatorFileUploadController' => 'applications/files/controller/upload',
'PhabricatorFileViewController' => 'applications/files/controller/view',
'PhabricatorLiskDAO' => 'applications/base/storage/lisk',
'PhabricatorPHID' => 'applications/phid/storage/phid',
'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate',
'PhabricatorPHIDController' => 'applications/phid/controller/base',
'PhabricatorPHIDDAO' => 'applications/phid/storage/base',
'PhabricatorPHIDListController' => 'applications/phid/controller/list',
'PhabricatorPHIDType' => 'applications/phid/storage/type',
'PhabricatorPHIDTypeEditController' => 'applications/phid/controller/typeedit',
'PhabricatorPHIDTypeListController' => 'applications/phid/controller/typelist',
'PhabricatorPeopleController' => 'applications/people/controller/base',
'PhabricatorPeopleEditController' => 'applications/people/controller/edit',
'PhabricatorPeopleListController' => 'applications/people/controller/list',
'PhabricatorPeopleProfileController' => 'applications/people/controller/profile',
'PhabricatorStandardPageView' => 'view/page/standard',
+ 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common',
+ 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base',
'PhabricatorUser' => 'applications/people/storage/user',
'PhabricatorUserDAO' => 'applications/people/storage/base',
),
'function' =>
array(
'_qsprintf_check_scalar_type' => 'storage/qsprintf',
'_qsprintf_check_type' => 'storage/qsprintf',
'celerity_generate_unique_node_id' => 'infratructure/celerity/api',
'celerity_register_resource_map' => 'infratructure/celerity/map',
'javelin_render_tag' => 'infratructure/javelin/markup',
'qsprintf' => 'storage/qsprintf',
'queryfx' => 'storage/queryfx',
'queryfx_all' => 'storage/queryfx',
'queryfx_one' => 'storage/queryfx',
'require_celerity_resource' => 'infratructure/celerity/api',
'vqsprintf' => 'storage/qsprintf',
'vqueryfx' => 'storage/queryfx',
'xsprintf_query' => 'storage/qsprintf',
),
'requires_class' =>
array(
'Aphront404Response' => 'AphrontResponse',
'AphrontAjaxResponse' => 'AphrontResponse',
'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
'AphrontDefaultApplicationController' => 'AphrontController',
'AphrontDialogResponse' => 'AphrontResponse',
'AphrontDialogView' => 'AphrontView',
'AphrontErrorView' => 'AphrontView',
'AphrontFileResponse' => 'AphrontResponse',
'AphrontFormControl' => 'AphrontView',
'AphrontFormFileControl' => 'AphrontFormControl',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormSelectControl' => 'AphrontFormControl',
'AphrontFormStaticControl' => 'AphrontFormControl',
'AphrontFormSubmitControl' => 'AphrontFormControl',
'AphrontFormTextAreaControl' => 'AphrontFormControl',
'AphrontFormTextControl' => 'AphrontFormControl',
+ 'AphrontFormTokenizerControl' => 'AphrontFormControl',
'AphrontFormView' => 'AphrontView',
'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontNullView' => 'AphrontView',
'AphrontPageView' => 'AphrontView',
'AphrontPanelView' => 'AphrontView',
'AphrontQueryConnectionException' => 'AphrontQueryException',
'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
'AphrontQueryCountException' => 'AphrontQueryException',
'AphrontQueryObjectMissingException' => 'AphrontQueryException',
'AphrontQueryParameterException' => 'AphrontQueryException',
'AphrontQueryRecoverableException' => 'AphrontQueryException',
'AphrontRedirectResponse' => 'AphrontResponse',
'AphrontSideNavView' => 'AphrontView',
'AphrontTableView' => 'AphrontView',
'AphrontWebpageResponse' => 'AphrontResponse',
'CelerityResourceController' => 'AphrontController',
'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod',
'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod',
'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod',
'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod',
'ConduitAPI_user_find_Method' => 'ConduitAPIMethod',
'DifferentialChangeset' => 'DifferentialDAO',
'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetListView' => 'AphrontView',
'DifferentialChangesetViewController' => 'DifferentialController',
'DifferentialController' => 'PhabricatorController',
'DifferentialDAO' => 'PhabricatorLiskDAO',
'DifferentialDiff' => 'DifferentialDAO',
'DifferentialDiffProperty' => 'DifferentialDAO',
'DifferentialDiffTableOfContentsView' => 'AphrontView',
'DifferentialDiffViewController' => 'DifferentialController',
'DifferentialHunk' => 'DifferentialDAO',
'DifferentialRevision' => 'DifferentialDAO',
'DifferentialRevisionEditController' => 'DifferentialController',
'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO',
'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
'PhabricatorConduitController' => 'PhabricatorController',
'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
'PhabricatorConduitLogController' => 'PhabricatorConduitController',
'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO',
'PhabricatorController' => 'AphrontController',
'PhabricatorDirectoryCategory' => 'PhabricatorDirectoryDAO',
'PhabricatorDirectoryCategoryDeleteController' => 'PhabricatorDirectoryController',
'PhabricatorDirectoryCategoryEditController' => 'PhabricatorDirectoryController',
'PhabricatorDirectoryCategoryListController' => 'PhabricatorDirectoryController',
'PhabricatorDirectoryController' => 'PhabricatorController',
'PhabricatorDirectoryDAO' => 'PhabricatorLiskDAO',
'PhabricatorDirectoryItem' => 'PhabricatorDirectoryDAO',
'PhabricatorDirectoryItemDeleteController' => 'PhabricatorDirectoryController',
'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController',
'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController',
'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController',
'PhabricatorFile' => 'PhabricatorFileDAO',
'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileViewController' => 'PhabricatorFileController',
'PhabricatorLiskDAO' => 'LiskDAO',
'PhabricatorPHID' => 'PhabricatorPHIDDAO',
'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
'PhabricatorPHIDController' => 'PhabricatorController',
'PhabricatorPHIDDAO' => 'PhabricatorLiskDAO',
'PhabricatorPHIDListController' => 'PhabricatorPHIDController',
'PhabricatorPHIDType' => 'PhabricatorPHIDDAO',
'PhabricatorPHIDTypeEditController' => 'PhabricatorPHIDController',
'PhabricatorPHIDTypeListController' => 'PhabricatorPHIDController',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleEditController' => 'PhabricatorPeopleController',
'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
'PhabricatorStandardPageView' => 'AphrontPageView',
+ 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
+ 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
'PhabricatorUser' => 'PhabricatorUserDAO',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
),
'requires_interface' =>
array(
),
));
diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
index 31d9e89bf6..bab44a6b2e 100644
--- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
@@ -1,138 +1,143 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group aphront
*/
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
public function getApplicationName() {
return 'aphront-default';
}
public function getURIMap() {
return array(
'/repository/' => array(
'$' => 'RepositoryListController',
'new/$' => 'RepositoryEditController',
'edit/(?<id>\d+)/$' => 'RepositoryEditController',
'delete/(?<id>\d+)/$' => 'RepositoryDeleteController',
),
'/' => array(
'$' => 'PhabricatorDirectoryMainController',
),
'/directory/' => array(
'item/$'
=> 'PhabricatorDirectoryItemListController',
'item/edit/(?:(?<id>\d+)/)?$'
=> 'PhabricatorDirectoryItemEditController',
'item/delete/(?<id>\d+)/'
=> 'PhabricatorDirectoryItemDeleteController',
'category/$'
=> 'PhabricatorDirectoryCategoryListController',
'category/edit/(?:(?<id>\d+)/)?$'
=> 'PhabricatorDirectoryCategoryEditController',
'category/delete/(?<id>\d+)/'
=> 'PhabricatorDirectoryCategoryDeleteController',
),
'/file/' => array(
'$' => 'PhabricatorFileListController',
'upload/$' => 'PhabricatorFileUploadController',
'(?<view>info)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
'(?<view>view)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
'(?<view>download)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
),
'/phid/' => array(
'$' => 'PhabricatorPHIDListController',
'type/$' => 'PhabricatorPHIDTypeListController',
'type/edit/(?:(?<id>\d+)/)?$' => 'PhabricatorPHIDTypeEditController',
'new/$' => 'PhabricatorPHIDAllocateController',
),
'/people/' => array(
'$' => 'PhabricatorPeopleListController',
'edit/(?:(?<username>\w+)/)?$' => 'PhabricatorPeopleEditController',
),
'/p/(?<username>\w+)/$' => 'PhabricatorPeopleProfileController',
'/conduit/' => array(
'$' => 'PhabricatorConduitConsoleController',
'method/(?<method>[^/]+)$' => 'PhabricatorConduitConsoleController',
'log/$' => 'PhabricatorConduitLogController',
),
'/api/(?<method>[^/]+)$' => 'PhabricatorConduitAPIController',
'/differential/' => array(
'diff/(?<id>\d+)/$' => 'DifferentialDiffViewController',
'changeset/(?<id>\d+)/$' => 'DifferentialChangesetViewController',
'revision/edit/(?:(?<id>\d+)/)?$'
=> 'DifferentialRevisionEditController',
),
'/res/' => array(
'(?<hash>[a-f0-9]{8})/(?<path>.+\.(?:css|js))$'
=> 'CelerityResourceController',
),
+
+ '/typeahead/' => array(
+ 'common/(?<type>\w+)/$'
+ => 'PhabricatorTypeaheadCommonDatasourceController',
+ ),
);
}
public function buildRequest() {
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($_GET + $_POST);
return $request;
}
public function handleException(Exception $ex) {
$class = phutil_escape_html(get_class($ex));
$message = phutil_escape_html($ex->getMessage());
$content =
'<div class="aphront-unhandled-exception">'.
'<h1>Unhandled Exception "'.$class.'": '.$message.'</h1>'.
'<code>'.phutil_escape_html((string)$ex).'</code>'.
'</div>';
$view = new PhabricatorStandardPageView();
$view->appendChild($content);
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
}
public function willSendResponse(AphrontResponse $response) {
$request = $this->getRequest();
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax()) {
$view = new PhabricatorStandardPageView();
$view->appendChild(
'<div style="padding: 2em 0;">'.
$response->buildResponseString().
'</div>');
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
}
}
return $response;
}
}
diff --git a/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php b/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
index 2bab3f81c5..1f9a1679dc 100644
--- a/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
+++ b/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
@@ -1,135 +1,145 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialRevisionEditController extends DifferentialController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
if ($this->id) {
$revision = id(new DifferentialRevision())->load($this->id);
if (!$revision) {
return new Aphront404Response();
}
} else {
$revision = new DifferentialRevision();
}
/*
$e_name = true;
$errors = array();
$request = $this->getRequest();
if ($request->isFormPost()) {
$category->setName($request->getStr('name'));
$category->setSequence($request->getStr('sequence'));
if (!strlen($category->getName())) {
$errors[] = 'Category name is required.';
$e_name = 'Required';
}
if (!$errors) {
$category->save();
return id(new AphrontRedirectResponse())
->setURI('/directory/category/');
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
*/
$e_name = true;
+ $e_testplan = true;
$form = new AphrontFormView();
if ($revision->getID()) {
- $form->setAction('/differential/revision/edit/'.$category->getID().'/');
+ $form->setAction('/differential/revision/edit/'.$revision->getID().'/');
} else {
$form->setAction('/differential/revision/edit/');
}
+ $reviewer_map = array(
+ 1 => 'A Zebra',
+ 2 => 'Pie Messenger',
+ );
+
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Name')
->setName('name')
->setValue($revision->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Summary')
->setName('summary')
->setValue($revision->getSummary()))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Test Plan')
->setName('testplan')
->setValue($revision->getTestPlan())
->setError($e_testplan))
->appendChild(
- id(new AphrontFormTextAreaControl())
+ id(new AphrontFormTokenizerControl())
->setLabel('Reviewers')
- ->setName('reviewers'))
+ ->setName('reviewers')
+ ->setDatasource('/typeahead/common/user/')
+ ->setValue($reviewer_map))
->appendChild(
- id(new AphrontFormTextAreaControl())
+ id(new AphrontFormTokenizerControl())
->setLabel('CC')
- ->setName('cc'))
+ ->setName('cc')
+ ->setDatasource('/typeahead/common/user/')
+ ->setValue($reviewer_map))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Blame Revision')
->setName('blame')
->setValue($revision->getBlameRevision())
->setCaption('Revision which broke the stuff which this '.
'change fixes.'))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Revert')
->setName('revert')
->setValue($revision->getRevertPlan())
->setCaption('Special steps required to safely revert this change.'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$panel = new AphrontPanelView();
if ($revision->getID()) {
$panel->setHeader('Edit Differential Revision');
} else {
$panel->setHeader('Create New Differential Revision');
}
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$error_view = null;
return $this->buildStandardPageResponse(
array($error_view, $panel),
array(
'title' => 'Edit Differential Revision',
));
}
}
diff --git a/src/applications/typeahead/controller/base/PhabricatorTypeaheadDatasourceController.php b/src/applications/typeahead/controller/base/PhabricatorTypeaheadDatasourceController.php
new file mode 100644
index 0000000000..a58587bb9c
--- /dev/null
+++ b/src/applications/typeahead/controller/base/PhabricatorTypeaheadDatasourceController.php
@@ -0,0 +1,26 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+abstract class PhabricatorTypeaheadDatasourceController
+ extends PhabricatorController {
+
+ public function getApplicationName() {
+ return 'typeahead';
+ }
+
+}
diff --git a/src/applications/typeahead/controller/base/__init__.php b/src/applications/typeahead/controller/base/__init__.php
new file mode 100644
index 0000000000..4ae92b113c
--- /dev/null
+++ b/src/applications/typeahead/controller/base/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/base/controller/base');
+
+
+phutil_require_source('PhabricatorTypeaheadDatasourceController.php');
diff --git a/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php
new file mode 100644
index 0000000000..1758a1a948
--- /dev/null
+++ b/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php
@@ -0,0 +1,45 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class PhabricatorTypeaheadCommonDatasourceController
+ extends PhabricatorTypeaheadDatasourceController {
+
+ public function willProcessRequest(array $data) {
+ $this->type = $data['type'];
+ }
+
+ public function processRequest() {
+
+ $data = array();
+
+ $users = id(new PhabricatorUser())->loadAll();
+
+ $data = array();
+ foreach ($users as $user) {
+ $data[] = array(
+ $user->getUsername().' ('.$user->getRealName().')',
+ '/p/'.$user->getUsername(),
+ $user->getPHID(),
+ );
+ }
+
+ return id(new AphrontAjaxResponse())
+ ->setContent($data);
+ }
+
+}
diff --git a/src/applications/typeahead/controller/common/__init__.php b/src/applications/typeahead/controller/common/__init__.php
new file mode 100644
index 0000000000..34f6ae27c4
--- /dev/null
+++ b/src/applications/typeahead/controller/common/__init__.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'aphront/response/ajax');
+phutil_require_module('phabricator', 'applications/people/storage/user');
+phutil_require_module('phabricator', 'applications/typeahead/controller/base');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('PhabricatorTypeaheadCommonDatasourceController.php');
diff --git a/src/view/form/control/tokenizer/AphrontFormTokenizerControl.php b/src/view/form/control/tokenizer/AphrontFormTokenizerControl.php
new file mode 100755
index 0000000000..c51e56f44a
--- /dev/null
+++ b/src/view/form/control/tokenizer/AphrontFormTokenizerControl.php
@@ -0,0 +1,106 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class AphrontFormTokenizerControl extends AphrontFormControl {
+
+ private $datasource;
+
+ public function setDatasource($datasource) {
+ $this->datasource = $datasource;
+ return $this;
+ }
+
+ protected function getCustomControlClass() {
+ return 'aphront-form-control-tokenizer';
+ }
+
+ protected function renderInput() {
+ require_celerity_resource('aphront-tokenizer-control-css');
+ require_celerity_resource('javelin-typeahead-dev');
+
+ $tokens = array();
+ $values = nonempty($this->getValue(), array());
+ foreach ($values as $key => $value) {
+ $tokens[] = $this->renderToken($key, $value);
+ }
+
+ $name = $this->getName();
+
+ $input = javelin_render_tag(
+ 'input',
+ array(
+ 'mustcapture' => true,
+ 'name' => $name,
+ 'class' => 'jx-tokenizer-input',
+ 'sigil' => 'tokenizer',
+ 'style' => 'width: 0px;',
+ 'disabled' => 'disabled',
+ 'type' => 'text',
+ ));
+
+ $id = celerity_generate_unique_node_id();
+
+ Javelin::initBehavior('aphront-basic-tokenizer', array(
+ 'id' => $id,
+ 'src' => $this->datasource,
+ 'value' => $values,
+ ));
+
+ return phutil_render_tag(
+ 'div',
+ array(
+ 'id' => $id,
+ 'class' => 'jx-tokenizer-container',
+ ),
+ implode('', $tokens).
+ $input.
+ '<div style="clear: both;"></div>');
+
+ return phutil_render_tag(
+ 'input',
+ array(
+ 'type' => 'text',
+ 'name' => $this->getName(),
+ 'value' => $this->getValue(),
+ 'disabled' => $this->getDisabled() ? 'disabled' : null,
+ ));
+ }
+
+ private function renderToken($key, $value) {
+ $input_name = $this->getName();
+ if ($input_name) {
+ $input_name .= '[]';
+ }
+ return phutil_render_tag(
+ 'a',
+ array(
+ 'class' => 'jx-tokenizer-token',
+ ),
+ phutil_escape_html($value).
+ phutil_render_tag(
+ 'input',
+ array(
+ 'type' => 'hidden',
+ 'name' => $input_name,
+ 'value' => $key,
+ )).
+ '<span class="jx-tokenizer-x-placeholder"></span>');
+ }
+
+
+}
diff --git a/src/view/form/control/tokenizer/__init__.php b/src/view/form/control/tokenizer/__init__.php
new file mode 100644
index 0000000000..85ffaeed29
--- /dev/null
+++ b/src/view/form/control/tokenizer/__init__.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'infratructure/celerity/api');
+phutil_require_module('phabricator', 'infratructure/javelin/api');
+phutil_require_module('phabricator', 'infratructure/javelin/markup');
+phutil_require_module('phabricator', 'view/form/control/base');
+
+phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('AphrontFormTokenizerControl.php');
diff --git a/webroot/rsrc/css/aphront/tokenizer.css b/webroot/rsrc/css/aphront/tokenizer.css
new file mode 100644
index 0000000000..b479875527
--- /dev/null
+++ b/webroot/rsrc/css/aphront/tokenizer.css
@@ -0,0 +1,64 @@
+/**
+ * @provides aphront-tokenizer-control-css
+ * @requires aphront-typeahead-control-css
+ */
+
+div.jx-tokenizer {
+ background: transparent;
+ position: relative;
+ display: block;
+ width: 100%;
+}
+
+div.jx-tokenizer-container {
+ background: #fff;
+ border: 1px solid #96A6C5;
+ position: relative;
+}
+
+var.jx-tokenizer-metrics {
+ position: absolute;
+ left: 20px;
+ top: 20px;
+}
+
+input.jx-tokenizer-input {
+ border: 1px solid transparent;
+ border-width: 1px 0px;
+ padding: 4px 4px 4px 3px;
+ outline: none;
+ float: left;
+}
+
+span.jx-tokenizer-x-placeholder {
+ padding: 0 2px;
+}
+
+a.jx-tokenizer-token {
+ padding: 1px 0 1px 4px;
+ border: 1px solid #a4bdec;
+ margin: 3px 2px 0 4px;
+ background: #dee7f8;
+ float: left;
+ cursor: text;
+ font-size: 12px;
+}
+
+a.jx-tokenizer-token:hover {
+ text-decoration: none;
+ background: #bbcef1;
+ cursor: text;
+}
+
+a.jx-tokenizer-token a.jx-tokenizer-x {
+ color: #627aad;
+ font-family: Arial, sans-serif;
+ font-weight: normal;
+ cursor: pointer;
+ padding: 0 4px;
+}
+
+a.jx-tokenizer-token a.jx-tokenizer-x:hover {
+ text-decoration: none;
+}
+
diff --git a/webroot/rsrc/css/aphront/typeahead.css b/webroot/rsrc/css/aphront/typeahead.css
new file mode 100644
index 0000000000..b3a7b18142
--- /dev/null
+++ b/webroot/rsrc/css/aphront/typeahead.css
@@ -0,0 +1,44 @@
+/**
+ * @provides aphront-typeahead-control-css
+ */
+
+div.jx-typeahead-hardpoint {
+ position: relative;
+ _zoom: 1; /* Some kind of IE6 fix? */
+}
+
+div.jx-typeahead-results {
+ z-index: 8;
+ position: absolute;
+ border-bottom: 1px solid #203b75;
+ padding: 0;
+ background: #ffffff;
+ width: 100%;
+}
+
+div.jx-typeahead-results a.jx-result {
+ color: #222;
+ display: block;
+ padding: .4em .5em .45em;
+ font-size: 11px;
+ border-width: 0px 1px 1px;
+ border-style: solid;
+ border-color: #dddddd #203b75;
+}
+
+div.jx-typeahead-results a.jx-result:hover,
+div.jx-typeahead-results a.focused {
+ display: block;
+ background: #3b5998;
+ color: #ffffff;
+ text-decoration: none;
+}
+
+table.jx-typeahead button {
+ margin-left: 3px;
+}
+
+table.jx-typeahead input {
+ font-size: 13px;
+ padding: 2px;
+}
diff --git a/webroot/rsrc/js/application/core/behavior-tokenizer.js b/webroot/rsrc/js/application/core/behavior-tokenizer.js
new file mode 100644
index 0000000000..78fd251977
--- /dev/null
+++ b/webroot/rsrc/js/application/core/behavior-tokenizer.js
@@ -0,0 +1,27 @@
+/**
+ * @provides javelin-behavior-aphront-basic-tokenizer
+ */
+
+JX.behavior('aphront-basic-tokenizer', function(config) {
+ var root = JX.$(config.id);
+
+ var datasource = new JX.TypeaheadPreloadedSource(config.src);
+
+ var typeahead = new JX.Typeahead(
+ root,
+ JX.DOM.find(root, 'input', 'tokenizer'));
+ typeahead.setDatasource(datasource);
+
+ var tokenizer = new JX.Tokenizer(root);
+ tokenizer.setTypeahead(typeahead);
+
+ if (config.limit) {
+ tokenizer.setLimit(config.limit);
+ }
+
+ if (config.value) {
+ tokenizer.setInitialValue(config.value);
+ }
+
+ tokenizer.start();
+});
diff --git a/webroot/rsrc/js/javelin/typeahead.dev.js b/webroot/rsrc/js/javelin/typeahead.dev.js
new file mode 100644
index 0000000000..df62d80aee
--- /dev/null
+++ b/webroot/rsrc/js/javelin/typeahead.dev.js
@@ -0,0 +1,1110 @@
+/** @provides javelin-typeahead-dev */
+
+/**
+ * @requires javelin-install
+ * javelin-dom
+ * javelin-vector
+ * javelin-util
+ * @provides javelin-typeahead
+ * @javelin
+ */
+
+/**
+ * A typeahead is a UI component similar to a text input, except that it
+ * suggests some set of results (like friends' names, common searches, or
+ * repository paths) as the user types them. Familiar examples of this UI
+ * include Google Suggest, the Facebook search box, and OS X's Spotlight
+ * feature.
+ *
+ * To build a @{JX.Typeahead}, you need to do four things:
+ *
+ * 1. Construct it, passing some DOM nodes for it to attach to. See the
+ * constructor for more information.
+ * 2. Attach a datasource by calling setDatasource() with a valid datasource,
+ * often a @{JX.TypeaheadPreloadedSource}.
+ * 3. Configure any special options that you want.
+ * 4. Call start().
+ *
+ * If you do this correctly, a dropdown menu should appear under the input as
+ * the user types, suggesting matching results.
+ *
+ * @task build Building a Typeahead
+ * @task datasource Configuring a Datasource
+ * @task config Configuring Options
+ * @task start Activating a Typeahead
+ * @task control Controlling Typeaheads from Javascript
+ * @task internal Internal Methods
+ */
+JX.install('Typeahead', {
+ /**
+ * Construct a new Typeahead on some "hardpoint". At a minimum, the hardpoint
+ * should be a ##<div>## with "position: relative;" wrapped around a text
+ * ##<input>##. The typeahead's dropdown suggestions will be appended to the
+ * hardpoint in the DOM. Basically, this is the bare minimum requirement:
+ *
+ * LANG=HTML
+ * <div style="position: relative;">
+ * <input type="text" />
+ * </div>
+ *
+ * Then get a reference to the ##<div>## and pass it as 'hardpoint', and pass
+ * the ##<input>## as 'control'. This will enhance your boring old
+ * ##<input />## with amazing typeahead powers.
+ *
+ * On the Facebook/Tools stack, ##<javelin:typeahead-template />## can build
+ * this for you.
+ *
+ * @param Node "Hardpoint", basically an anchorpoint in the document which
+ * the typeahead can append its suggestion menu to.
+ * @param Node? Actual ##<input />## to use; if not provided, the typeahead
+ * will just look for a (solitary) input inside the hardpoint.
+ * @task build
+ */
+ construct : function(hardpoint, control) {
+ this._hardpoint = hardpoint;
+ this._control = control || JX.DOM.find(hardpoint, 'input');
+
+ this._root = JX.$N(
+ 'div',
+ {className: 'jx-typeahead-results'});
+ this._display = [];
+
+ JX.DOM.listen(
+ this._control,
+ ['focus', 'blur', 'keypress', 'keydown'],
+ null,
+ JX.bind(this, this.handleEvent));
+
+ JX.DOM.listen(
+ this._root,
+ ['mouseover', 'mouseout'],
+ null,
+ JX.bind(this, this._onmouse));
+
+ JX.DOM.listen(
+ this._root,
+ 'mousedown',
+ 'tag:a',
+ JX.bind(this, function(e) {
+ this._choose(e.getTarget());
+ e.prevent();
+ }));
+
+ },
+
+ events : ['choose', 'query', 'start', 'change'],
+
+ properties : {
+
+ /**
+ * Boolean. If true (default), the user is permitted to submit the typeahead
+ * with a custom or empty selection. This is a good behavior if the
+ * typeahead is attached to something like a search input, where the user
+ * might type a freeform query or select from a list of suggestions.
+ * However, sometimes you require a specific input (e.g., choosing which
+ * user owns something), in which case you can prevent null selections.
+ *
+ * @task config
+ */
+ allowNullSelection : true,
+
+ /**
+ * Function. Allows you to reconfigure the Typeahead's normalizer, which is
+ * @{JX.TypeaheadNormalizer} by default. The normalizer is used to convert
+ * user input into strings suitable for matching, e.g. by lowercasing all
+ * input and removing punctuation. See @{JX.TypeaheadNormalizer} for more
+ * details. Any replacement function should accept an arbitrary user-input
+ * string and emit a normalized string suitable for tokenization and
+ * matching.
+ *
+ * @task config
+ */
+ normalizer : null
+ },
+
+ members : {
+ _root : null,
+ _control : null,
+ _hardpoint : null,
+ _value : null,
+ _stop : false,
+ _focus : -1,
+ _display : null,
+
+ /**
+ * Activate your properly configured typeahead. It won't do anything until
+ * you call this method!
+ *
+ * @task start
+ * @return void
+ */
+ start : function() {
+ this.invoke('start');
+ },
+
+
+ /**
+ * Configure a datasource, which is where the Typeahead gets suggestions
+ * from. See @{JX.TypeaheadDatasource} for more information. You must
+ * provide a datasource.
+ *
+ * @task datasource
+ * @param JX.TypeaheadDatasource The datasource which the typeahead will
+ * draw from.
+ */
+ setDatasource : function(datasource) {
+ datasource.bindToTypeahead(this);
+ },
+
+
+ /**
+ * Override the <input /> selected in the constructor with some other input.
+ * This is primarily useful when building a control on top of the typeahead,
+ * like @{JX.Tokenizer}.
+ *
+ * @task config
+ * @param node An <input /> node to use as the primary control.
+ */
+ setInputNode : function(input) {
+ this._control = input;
+ return this;
+ },
+
+
+ /**
+ * Hide the typeahead's dropdown suggestion menu.
+ *
+ * @task control
+ * @return void
+ */
+ hide : function() {
+ this._changeFocus(Number.NEGATIVE_INFINITY);
+ this._display = [];
+ this._moused = false;
+ JX.DOM.setContent(this._root, '');
+ JX.DOM.remove(this._root);
+ },
+
+
+ /**
+ * Show a given result set in the typeahead's dropdown suggestion menu.
+ * Normally, you only call this method if you are implementing a datasource.
+ * Otherwise, the datasource you have configured calls it for you in
+ * response to the user's actions.
+ *
+ * @task control
+ * @param list List of ##<a />## tags to show as suggestions/results.
+ * @return void
+ */
+ showResults : function(results) {
+ this._display = results;
+ if (results.length) {
+ JX.DOM.setContent(this._root, results);
+ this._changeFocus(Number.NEGATIVE_INFINITY);
+ var d = JX.$V.getDim(this._hardpoint);
+ d.x = 0;
+ d.setPos(this._root);
+ this._hardpoint.appendChild(this._root);
+ } else {
+ this.hide();
+ }
+ },
+
+ refresh : function() {
+ if (this._stop) {
+ return;
+ }
+
+ this._value = this._control.value;
+ if (!this.invoke('change', this._value).getPrevented()) {
+ if (__DEV__) {
+ throw new Error(
+ "JX.Typeahead._update(): " +
+ "No listener responded to Typeahead 'change' event. Create a " +
+ "datasource and call setDatasource().");
+ }
+ }
+ },
+ /**
+ * Show a "waiting for results" UI in place of the typeahead's dropdown
+ * suggestion menu. NOTE: currently there's no such UI, lolol.
+ *
+ * @task control
+ * @return void
+ */
+ waitForResults : function() {
+ // TODO: Build some sort of fancy spinner or "..." type UI here to
+ // visually indicate that we're waiting on the server.
+ this.hide();
+ },
+
+
+ /**
+ * @task internal
+ */
+ _onmouse : function(event) {
+ this._moused = (event.getType() == 'mouseover');
+ this._drawFocus();
+ },
+
+
+ /**
+ * @task internal
+ */
+ _changeFocus : function(d) {
+ var n = Math.min(Math.max(-1, this._focus + d), this._display.length - 1);
+ if (!this.getAllowNullSelection()) {
+ n = Math.max(0, n);
+ }
+ if (this._focus >= 0 && this._focus < this._display.length) {
+ JX.DOM.alterClass(this._display[this._focus], 'focused', 0);
+ }
+ this._focus = n;
+ this._drawFocus();
+ return true;
+ },
+
+
+ /**
+ * @task internal
+ */
+ _drawFocus : function() {
+ var f = this._display[this._focus];
+ if (f) {
+ JX.DOM.alterClass(f, 'focused', !this._moused);
+ }
+ },
+
+
+ /**
+ * @task internal
+ */
+ _choose : function(target) {
+ var result = this.invoke('choose', target);
+ if (result.getPrevented()) {
+ return;
+ }
+
+ this._control.value = target.name;
+ this.hide();
+ },
+
+
+ /**
+ * @task control
+ */
+ clear : function() {
+ this._control.value = '';
+ this.hide();
+ },
+
+
+ /**
+ * @task control
+ */
+ disable : function() {
+ this._control.blur();
+ this._control.disabled = true;
+ this._stop = true;
+ },
+
+
+ /**
+ * @task control
+ */
+ submit : function() {
+ if (this._focus >= 0 && this._display[this._focus]) {
+ this._choose(this._display[this._focus]);
+ return true;
+ } else {
+ result = this.invoke('query', this._control.value);
+ if (result.getPrevented()) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ setValue : function(value) {
+ this._control.value = value;
+ },
+
+ getValue : function() {
+ return this._control.value;
+ },
+
+ /**
+ * @task internal
+ */
+ _update : function(event) {
+ var k = event && event.getSpecialKey();
+ if (k && event.getType() == 'keydown') {
+ switch (k) {
+ case 'up':
+ if (this._display.length && this._changeFocus(-1)) {
+ event.prevent();
+ }
+ break;
+ case 'down':
+ if (this._display.length && this._changeFocus(1)) {
+ event.prevent();
+ }
+ break;
+ case 'return':
+ if (this.submit()) {
+ event.prevent();
+ return;
+ }
+ break;
+ case 'esc':
+ if (this._display.length && this.getAllowNullSelection()) {
+ this.hide();
+ event.prevent();
+ }
+ break;
+ case 'tab':
+ // If the user tabs out of the field, don't refresh.
+ return;
+ }
+ }
+
+ // We need to defer because the keystroke won't be present in the input's
+ // value field yet.
+ JX.defer(JX.bind(this, function() {
+ if (this._value == this._control.value) {
+ // The typeahead value hasn't changed.
+ return;
+ }
+ this.refresh();
+ }));
+ },
+
+ /**
+ * This method is pretty much internal but @{JX.Tokenizer} needs access to
+ * it for delegation. You might also need to delegate events here if you
+ * build some kind of meta-control.
+ *
+ * Reacts to user events in accordance to configuration.
+ *
+ * @task internal
+ * @param JX.Event User event, like a click or keypress.
+ * @return void
+ */
+ handleEvent : function(e) {
+ if (this._stop || e.getPrevented()) {
+ return;
+ }
+ var type = e.getType();
+ if (type == 'blur') {
+ this.hide();
+ } else {
+ this._update(e);
+ }
+ }
+ }
+});
+/**
+ * @requires javelin-install
+ * @provides javelin-typeahead-normalizer
+ * @javelin
+ */
+
+JX.install('TypeaheadNormalizer', {
+ statics : {
+ normalize : function(str) {
+ return ('' + str)
+ .toLowerCase()
+ .replace(/[^a-z0-9 ]/g, '')
+ .replace(/ +/g, ' ')
+ .replace(/^\s*|\s*$/g, '');
+ }
+ }
+});
+/**
+ * @requires javelin-install
+ * javelin-util
+ * javelin-dom
+ * javelin-typeahead-normalizer
+ * @provides javelin-typeahead-source
+ * @javelin
+ */
+
+JX.install('TypeaheadSource', {
+ construct : function() {
+ this._raw = {};
+ this._lookup = {};
+ this.setNormalizer(JX.TypeaheadNormalizer.normalize);
+ },
+
+ properties : {
+
+ /**
+ * Allows you to specify a function which will be used to normalize strings.
+ * Strings are normalized before being tokenized, and before being sent to
+ * the server. The purpose of normalization is to strip out irrelevant data,
+ * like uppercase/lowercase, extra spaces, or punctuation. By default,
+ * the @{JX.TypeaheadNormalizer} is used to normalize strings, but you may
+ * want to provide a different normalizer, particiularly if there are
+ * special characters with semantic meaning in your object names.
+ *
+ * @param function
+ */
+ normalizer : null,
+
+ /**
+ * Transformers convert data from a wire format to a runtime format. The
+ * transformation mechanism allows you to choose an efficient wire format
+ * and then expand it on the client side, rather than duplicating data
+ * over the wire. The transformation is applied to objects passed to
+ * addResult(). It should accept whatever sort of object you ship over the
+ * wire, and produce a dictionary with these keys:
+ *
+ * - **id**: a unique id for each object.
+ * - **name**: the string used for matching against user input.
+ * - **uri**: the URI corresponding with the object (must be present
+ * but need not be meaningful)
+ * - **display**: the text or nodes to show in the DOM. Usually just the
+ * same as ##name##.
+ *
+ * The default transformer expects a three element list with elements
+ * [name, uri, id]. It assigns the first element to both ##name## and
+ * ##display##.
+ *
+ * @param function
+ */
+ transformer : null,
+
+ /**
+ * Configures the maximum number of suggestions shown in the typeahead
+ * dropdown.
+ *
+ * @param int
+ */
+ maximumResultCount : 5
+
+ },
+
+ members : {
+ _raw : null,
+ _lookup : null,
+ _typeahead : null,
+ _normalizer : null,
+
+ bindToTypeahead : function(typeahead) {
+ this._typeahead = typeahead;
+ typeahead.listen('change', JX.bind(this, this.didChange));
+ typeahead.listen('start', JX.bind(this, this.didStart));
+ },
+
+ didChange : function(value) {
+ return;
+ },
+
+ didStart : function() {
+ return;
+ },
+
+ addResult : function(obj) {
+ obj = (this.getTransformer() || this._defaultTransformer)(obj);
+
+ if (obj.id in this._raw) {
+ // We're already aware of this result. This will happen if someone
+ // searches for "zeb" and then for "zebra" with a
+ // TypeaheadRequestSource, for example, or the datasource just doesn't
+ // dedupe things properly. Whatever the case, just ignore it.
+ return;
+ }
+
+ if (__DEV__) {
+ for (var k in {name : 1, id : 1, display : 1, uri : 1}) {
+ if (!(k in obj)) {
+ throw new Error(
+ "JX.TypeaheadSource.addResult(): " +
+ "result must have properties 'name', 'id', 'uri' and 'display'.");
+ }
+ }
+ }
+
+ this._raw[obj.id] = obj;
+ var t = this.tokenize(obj.name);
+ for (var jj = 0; jj < t.length; ++jj) {
+ this._lookup[t[jj]] = this._lookup[t[jj]] || [];
+ this._lookup[t[jj]].push(obj.id);
+ }
+ },
+
+ waitForResults : function() {
+ this._typeahead.waitForResults();
+ return this;
+ },
+
+ matchResults : function(value) {
+
+ // This table keeps track of the number of tokens each potential match
+ // has actually matched. When we're done, the real matches are those
+ // which have matched every token (so the value is equal to the token
+ // list length).
+ var match_count = {};
+
+ // This keeps track of distinct matches. If the user searches for
+ // something like "Chris C" against "Chris Cox", the "C" will match
+ // both fragments. We need to make sure we only count distinct matches.
+ var match_fragments = {};
+
+ var matched = {};
+ var seen = {};
+
+ var t = this.tokenize(value);
+
+ // Sort tokens by longest-first. We match each name fragment with at
+ // most one token.
+ t.sort(function(u, v) { return v.length - u.length; });
+
+ for (var ii = 0; ii < t.length; ++ii) {
+ // Do something reasonable if the user types the same token twice; this
+ // is sort of stupid so maybe kill it?
+ if (t[ii] in seen) {
+ t.splice(ii--, 1);
+ continue;
+ }
+ seen[t[ii]] = true;
+ var fragment = t[ii];
+ for (var name_fragment in this._lookup) {
+ if (name_fragment.substr(0, fragment.length) === fragment) {
+ if (!(name_fragment in matched)) {
+ matched[name_fragment] = true;
+ } else {
+ continue;
+ }
+ var l = this._lookup[name_fragment];
+ for (var jj = 0; jj < l.length; ++jj) {
+ var match_id = l[jj];
+ if (!match_fragments[match_id]) {
+ match_fragments[match_id] = {};
+ }
+ if (!(fragment in match_fragments[match_id])) {
+ match_fragments[match_id][fragment] = true;
+ match_count[match_id] = (match_count[match_id] || 0) + 1;
+ }
+ }
+ }
+ }
+ }
+
+ var hits = [];
+ for (var k in match_count) {
+ if (match_count[k] == t.length) {
+ hits.push(k);
+ }
+ }
+
+ var n = Math.min(this.getMaximumResultCount(), hits.length);
+ var nodes = [];
+ for (var kk = 0; kk < n; kk++) {
+ var data = this._raw[hits[kk]];
+ nodes.push(JX.$N(
+ 'a',
+ {
+ href: data.uri,
+ name: data.name,
+ rel: data.id,
+ className: 'jx-result'
+ },
+ data.display));
+ }
+
+ this._typeahead.showResults(nodes);
+ },
+ normalize : function(str) {
+ return (this.getNormalizer() || JX.bag())(str);
+ },
+ tokenize : function(str) {
+ str = this.normalize(str);
+ if (!str.length) {
+ return [];
+ }
+ return str.split(/ /g);
+ },
+ _defaultTransformer : function(object) {
+ return {
+ name : object[0],
+ display : object[0],
+ uri : object[1],
+ id : object[2]
+ };
+ }
+ }
+});
+
+
+/**
+ * @requires javelin-install
+ * javelin-util
+ * javelin-stratcom
+ * javelin-request
+ * javelin-typeahead-source
+ * @provides javelin-typeahead-preloaded-source
+ * @javelin
+ */
+
+/**
+ * Simple datasource that loads all possible results from a single call to a
+ * URI. This is appropriate if the total data size is small (up to perhaps a
+ * few thousand items). If you have more items so you can't ship them down to
+ * the client in one repsonse, use @{JX.TypeaheadOnDemandSource}.
+ */
+JX.install('TypeaheadPreloadedSource', {
+
+ extend : 'TypeaheadSource',
+
+ construct : function(uri) {
+ this.__super__.call(this);
+ this.uri = uri;
+ },
+
+ members : {
+
+ ready : false,
+ uri : null,
+ lastValue : null,
+
+ didChange : function(value) {
+ if (this.ready) {
+ this.matchResults(value);
+ } else {
+ this.lastValue = value;
+ this.waitForResults();
+ }
+ JX.Stratcom.context().kill();
+ },
+
+ didStart : function() {
+ var r = new JX.Request(this.uri, JX.bind(this, this.ondata));
+ r.setMethod('GET');
+ r.send();
+ },
+
+ ondata : function(results) {
+ for (var ii = 0; ii < results.length; ++ii) {
+ this.addResult(results[ii]);
+ }
+ if (this.lastValue !== null) {
+ this.matchResults(this.lastValue);
+ }
+ this.ready = true;
+ }
+ }
+});
+
+
+
+/**
+ * @requires javelin-install
+ * javelin-util
+ * javelin-stratcom
+ * javelin-request
+ * javelin-typeahead-source
+ * @provides javelin-typeahead-ondemand-source
+ * @javelin
+ */
+
+JX.install('TypeaheadOnDemandSource', {
+
+ extend : 'TypeaheadSource',
+
+ construct : function(uri) {
+ this.__super__.call(this);
+ this.uri = uri;
+ this.haveData = {
+ '' : true
+ };
+ },
+
+ properties : {
+ /**
+ * Configures how many milliseconds we wait after the user stops typing to
+ * send a request to the server. Setting a value of 250 means "wait 250
+ * milliseconds after the user stops typing to request typeahead data".
+ * Higher values reduce server load but make the typeahead less responsive.
+ */
+ queryDelay : 125,
+ /**
+ * Auxiliary data to pass along when sending the query for server results.
+ */
+ auxiliaryData : {}
+ },
+
+ members : {
+ uri : null,
+ lastChange : null,
+ haveData : null,
+
+ didChange : function(value) {
+ if (JX.Stratcom.pass()) {
+ return;
+ }
+ this.lastChange = new Date().getTime();
+ value = this.normalize(value);
+
+ if (this.haveData[value]) {
+ this.matchResults(value);
+ } else {
+ this.waitForResults();
+ JX.defer(
+ JX.bind(this, this.sendRequest, this.lastChange, value),
+ this.getQueryDelay());
+ }
+
+ JX.Stratcom.context().kill();
+ },
+
+ sendRequest : function(when, value) {
+ if (when != this.lastChange) {
+ return;
+ }
+ var r = new JX.Request(
+ this.uri,
+ JX.bind(this, this.ondata, this.lastChange, value));
+ r.setMethod('GET');
+ r.setData(JX.copy(this.getAuxiliaryData(), {q : value}));
+ r.send();
+ },
+
+ ondata : function(when, value, results) {
+ for (var ii = 0; ii < results.length; ii++) {
+ this.addResult(results[ii]);
+ }
+ this.haveData[value] = true;
+ if (when != this.lastChange) {
+ return;
+ }
+ this.matchResults(value);
+ }
+ }
+});
+
+
+/**
+ * @requires javelin-typeahead javelin-dom javelin-util
+ * javelin-stratcom javelin-vector javelin-install
+ * javelin-typeahead-preloaded-source
+ * @provides javelin-tokenizer
+ * @javelin
+ */
+
+/**
+ * A tokenizer is a UI component similar to a text input, except that it
+ * allows the user to input a list of items ("tokens"), generally from a fixed
+ * set of results. A familiar example of this UI is the "To:" field of most
+ * email clients, where the control autocompletes addresses from the user's
+ * address book.
+ *
+ * @{JX.Tokenizer} is built on top of @{JX.Typeahead}, and primarily adds the
+ * ability to choose multiple items.
+ *
+ * To build a @{JX.Tokenizer}, you need to do four things:
+ *
+ * 1. Construct it, padding a DOM node for it to attach to. See the constructor
+ * for more information.
+ * 2. Build a {@JX.Typeahead} and configure it with setTypeahead().
+ * 3. Configure any special options you want.
+ * 4. Call start().
+ *
+ * If you do this correctly, the input should suggest items and enter them as
+ * tokens as the user types.
+ */
+JX.install('Tokenizer', {
+ construct : function(containerNode) {
+ this._containerNode = containerNode;
+ },
+
+ properties : {
+ limit : null,
+ nextInput : null
+ },
+
+ members : {
+ _containerNode : null,
+ _root : null,
+ _focus : null,
+ _orig : null,
+ _typeahead : null,
+ _tokenid : 0,
+ _tokens : null,
+ _tokenMap : null,
+ _initialValue : null,
+ _seq : 0,
+ _lastvalue : null,
+
+ start : function() {
+ if (__DEV__) {
+ if (!this._typeahead) {
+ throw new Error(
+ 'JX.Tokenizer.start(): ' +
+ 'No typeahead configured! Use setTypeahead() to provide a ' +
+ 'typeahead.');
+ }
+ }
+
+ this._orig = JX.DOM.find(this._containerNode, 'input', 'tokenizer');
+ this._tokens = [];
+ this._tokenMap = {};
+
+ var focus = JX.$N('input', {
+ className: 'jx-tokenizer-input',
+ type: 'text',
+ value: this._orig.value
+ });
+ this._focus = focus;
+
+ JX.DOM.listen(
+ focus,
+ ['click', 'focus', 'blur', 'keydown'],
+ null,
+ JX.bind(this, this.handleEvent));
+
+ JX.DOM.listen(
+ this._containerNode,
+ 'click',
+ null,
+ JX.bind(
+ this,
+ function(e) {
+ if (e.getNodes().remove) {
+ this._remove(e.getData().token.key);
+ } else if (e.getTarget() == this._root) {
+ this.focus();
+ }
+ }));
+
+ var root = JX.$N('div');
+ root.id = this._orig.id;
+ JX.DOM.alterClass(root, 'jx-tokenizer', true);
+ root.style.cursor = 'text';
+ this._root = root;
+
+ root.appendChild(focus);
+
+ var typeahead = this._typeahead;
+ typeahead.setInputNode(this._focus);
+ typeahead.start();
+
+ JX.defer(
+ JX.bind(
+ this,
+ function() {
+ JX.DOM.setContent(this._orig.parentNode, root);
+ var map = this._initialValue || {};
+ for (var k in map) {
+ this.addToken(k, map[k]);
+ }
+ this._redraw();
+ }));
+ },
+
+ setInitialValue : function(map) {
+ this._initialValue = map;
+ return this;
+ },
+
+ setTypeahead : function(typeahead) {
+
+ typeahead.setAllowNullSelection(false);
+
+ typeahead.listen(
+ 'choose',
+ JX.bind(
+ this,
+ function(result) {
+ JX.Stratcom.context().prevent();
+ if (this.addToken(result.rel, result.name)) {
+ this._typeahead.hide();
+ this._focus.value = '';
+ this._redraw();
+ this.focus();
+ }
+ }));
+
+ typeahead.listen(
+ 'query',
+ JX.bind(
+ this,
+ function(query) {
+
+ // TODO: We should emit a 'query' event here to allow the caller to
+ // generate tokens on the fly, e.g. email addresses or other freeform
+ // or algorithmic tokens.
+
+ // Then do this if something handles the event.
+ // this._focus.value = '';
+ // this._redraw();
+ // this.focus();
+
+ if (query.length) {
+ // Prevent this event if there's any text, so that we don't submit
+ // the form (either we created a token or we failed to create a
+ // token; in either case we shouldn't submit). If the query is
+ // empty, allow the event so that the form submission takes place.
+ JX.Stratcom.context().prevent();
+ }
+ }));
+
+ this._typeahead = typeahead;
+
+ return this;
+ },
+
+ handleEvent : function(e) {
+
+ this._typeahead.handleEvent(e);
+ if (e.getPrevented()) {
+ return;
+ }
+
+ if (e.getType() == 'click') {
+ if (e.getTarget() == this._root) {
+ this.focus();
+ e.prevent();
+ return;
+ }
+ } else if (e.getType() == 'keydown') {
+ this._onkeydown(e);
+ } else if (e.getType() == 'blur') {
+ this._redraw();
+ }
+ },
+
+ refresh : function() {
+ this._redraw(true);
+ return this;
+ },
+
+ _redraw : function(force) {
+ var focus = this._focus;
+
+ if (focus.value === this._lastvalue && !force) {
+ return;
+ }
+ this._lastvalue = focus.value;
+
+ var root = this._root;
+ var metrics = JX.DOM.textMetrics(
+ this._focus,
+ 'jx-tokenizer-metrics');
+ metrics.y = null;
+ metrics.x += 24;
+ metrics.setDim(focus);
+
+ // This is a pretty ugly hack to force a redraw after copy/paste in
+ // Firefox. If we don't do this, it doesn't redraw the input so pasting
+ // in an email address doesn't give you a very good behavior.
+ focus.value = focus.value;
+
+ var h = JX.$V(focus).add(JX.$V.getDim(focus)).y - JX.$V(root).y;
+ root.style.height = h + 'px';
+ },
+
+ addToken : function(key, value) {
+ if (key in this._tokenMap) {
+ return false;
+ }
+
+ var focus = this._focus;
+ var root = this._root;
+
+ var token = JX.$N('a', {
+ className: 'jx-tokenizer-token'
+ }, value);
+
+ var input = JX.$N('input', {
+ type: 'hidden',
+ value: key,
+ name: this._orig.name+'['+(this._seq++)+']'
+ });
+
+ var remove = JX.$N('a', {
+ className: 'jx-tokenizer-x'
+ }, JX.HTML('&times;'));
+
+ this._tokenMap[key] = {
+ value : value,
+ key : key,
+ node : token
+ };
+ this._tokens.push(key);
+
+ JX.Stratcom.sigilize(token, 'token', {key : key});
+ JX.Stratcom.sigilize(remove, 'remove');
+
+ token.appendChild(input);
+ token.appendChild(remove);
+
+ root.insertBefore(token, focus);
+
+ return true;
+ },
+
+ getTokens : function() {
+ var result = {};
+ for (var key in this._tokenMap) {
+ result[key] = this._tokenMap[key].value;
+ }
+ return result;
+ },
+
+ _onkeydown : function(e) {
+ var focus = this._focus;
+ var root = this._root;
+ switch (e.getSpecialKey()) {
+ case 'tab':
+ var completed = this._typeahead.submit();
+ if (this.getNextInput()) {
+ if (!completed) {
+ this._focus.value = '';
+ }
+ JX.defer(JX.bind(this, function() {
+ this.getNextInput().focus();
+ }));
+ }
+ break;
+ case 'delete':
+ if (!this._focus.value.length) {
+ var tok;
+ while (tok = this._tokens.pop()) {
+ if (this._remove(tok)) {
+ break;
+ }
+ }
+ }
+ break;
+ case 'return':
+ // Don't subject this to token limits.
+ break;
+ default:
+ if (this.getLimit() &&
+ JX.keys(this._tokenMap).length == this.getLimit()) {
+ e.prevent();
+ }
+ JX.defer(JX.bind(this, this._redraw));
+ break;
+ }
+ },
+
+ _remove : function(index) {
+ if (!this._tokenMap[index]) {
+ return false;
+ }
+ JX.DOM.remove(this._tokenMap[index].node);
+ delete this._tokenMap[index];
+ this._redraw(true);
+ this.focus();
+ return true;
+ },
+
+ focus : function() {
+ var focus = this._focus;
+ JX.DOM.show(focus);
+ JX.defer(function() { JX.DOM.focus(focus); });
+ }
+ }
+});
diff --git a/webroot/rsrc/js/javelin/typeahead.min.js b/webroot/rsrc/js/javelin/typeahead.min.js
new file mode 100644
index 0000000000..07ab5a6f22
--- /dev/null
+++ b/webroot/rsrc/js/javelin/typeahead.min.js
@@ -0,0 +1,3 @@
+/** @provides javelin-typeahead-prod */
+
+JX.install('Typeahead',{construct:function(b,a){this._a=b;this._b=a||JX.DOM.find(b,'input');this._c=JX.$N('div',{className:'jx-typeahead-results'});this._d=[];JX.DOM.listen(this._b,['focus','blur','keypress','keydown'],null,JX.bind(this,this.handleEvent));JX.DOM.listen(this._c,['mouseover','mouseout'],null,JX.bind(this,this._e));JX.DOM.listen(this._c,'mousedown','tag:a',JX.bind(this,function(c){this._f(c.getTarget());c.prevent();}));},events:['choose','query','start','change'],properties:{allowNullSelection:true,normalizer:null},members:{_c:null,_b:null,_a:null,_g:null,_h:false,_i:-1,_d:null,start:function(){this.invoke('start');},setDatasource:function(a){a.bindToTypeahead(this);},setInputNode:function(a){this._b=a;return this;},hide:function(){this._j(Number.NEGATIVE_INFINITY);this._d=[];this._k=false;JX.DOM.setContent(this._c,'');JX.DOM.remove(this._c);},showResults:function(b){this._d=b;if(b.length){JX.DOM.setContent(this._c,b);this._j(Number.NEGATIVE_INFINITY);var a=JX.$V.getDim(this._a);a.x=0;a.setPos(this._c);this._a.appendChild(this._c);}else this.hide();},refresh:function(){if(this._h)return;this._g=this._b.value;!this.invoke('change',this._g).getPrevented();},waitForResults:function(){this.hide();},_e:function(event){this._k=(event.getType()=='mouseover');this._l();},_j:function(a){var b=Math.min(Math.max(-1,this._i+a),this._d.length-1);if(!this.getAllowNullSelection())b=Math.max(0,b);if(this._i>=0&&this._i<this._d.length)JX.DOM.alterClass(this._d[this._i],'focused',0);this._i=b;this._l();return true;},_l:function(){var a=this._d[this._i];if(a)JX.DOM.alterClass(a,'focused',!this._k);},_f:function(b){var a=this.invoke('choose',b);if(a.getPrevented())return;this._b.value=b.name;this.hide();},clear:function(){this._b.value='';this.hide();},disable:function(){this._b.blur();this._b.disabled=true;this._h=true;},submit:function(){if(this._i>=0&&this._d[this._i]){this._f(this._d[this._i]);return true;}else{result=this.invoke('query',this._b.value);if(result.getPrevented())return true;}return false;},setValue:function(a){this._b.value=a;},getValue:function(){return this._b.value;},_m:function(event){var a=event&&event.getSpecialKey();if(a&&event.getType()=='keydown')switch(a){case 'up':if(this._d.length&&this._j(-1))event.prevent();break;case 'down':if(this._d.length&&this._j(1))event.prevent();break;case 'return':if(this.submit()){event.prevent();return;}break;case 'esc':if(this._d.length&&this.getAllowNullSelection()){this.hide();event.prevent();}break;case 'tab':return;}JX.defer(JX.bind(this,function(){if(this._g==this._b.value)return;this.refresh();}));},handleEvent:function(a){if(this._h||a.getPrevented())return;var b=a.getType();if(b=='blur'){this.hide();}else this._m(a);}}});JX.install('TypeaheadNormalizer',{statics:{normalize:function(a){return (''+a).toLowerCase().replace(/[^a-z0-9 ]/g,'').replace(/ +/g,' ').replace(/^\s*|\s*$/g,'');}}});JX.install('TypeaheadSource',{construct:function(){this._n={};this._o={};this.setNormalizer(JX.TypeaheadNormalizer.normalize);},properties:{normalizer:null,transformer:null,maximumResultCount:5},members:{_n:null,_o:null,_p:null,_q:null,bindToTypeahead:function(a){this._p=a;a.listen('change',JX.bind(this,this.didChange));a.listen('start',JX.bind(this,this.didStart));},didChange:function(a){return;},didStart:function(){return;},addResult:function(b){b=(this.getTransformer()||this._r)(b);if(b.id in this._n)return;this._n[b.id]=b;var c=this.tokenize(b.name);for(var a=0;a<c.length;++a){this._o[c[a]]=this._o[c[a]]||[];this._o[c[a]].push(b.id);}},waitForResults:function(){this._p.waitForResults();return this;},matchResults:function(r){var i={};var j={};var l={};var p={};var q=this.tokenize(r);q.sort(function(s,t){return t.length-s.length;});for(var d=0;d<q.length;++d){if(q[d] in p){q.splice(d--,1);continue;}p[q[d]]=true;var b=q[d];for(var n in this._o)if(n.substr(0,b.length)===b){if(!(n in l)){l[n]=true;}else continue;var h=this._o[n];for(var e=0;e<h.length;++e){var k=h[e];if(!j[k])j[k]={};if(!(b in j[k])){j[k][b]=true;i[k]=(i[k]||0)+1;}}}}var c=[];for(var f in i)if(i[f]==q.length)c.push(f);var m=Math.min(this.getMaximumResultCount(),c.length);var o=[];for(var g=0;g<m;g++){var a=this._n[c[g]];o.push(JX.$N('a',{href:a.uri,name:a.name,rel:a.id,className:'jx-result'},a.display));}this._p.showResults(o);},normalize:function(a){return (this.getNormalizer()||JX.bag())(a);},tokenize:function(a){a=this.normalize(a);if(!a.length)return [];return a.split(/ /g);},_r:function(a){return {name:a[0],display:a[0],uri:a[1],id:a[2]};}}});JX.install('TypeaheadPreloadedSource',{extend:'TypeaheadSource',construct:function(a){this.__super__.call(this);this.uri=a;},members:{ready:false,uri:null,lastValue:null,didChange:function(a){if(this.ready){this.matchResults(a);}else{this.lastValue=a;this.waitForResults();}JX.Stratcom.context().kill();},didStart:function(){var a=new JX.Request(this.uri,JX.bind(this,this.ondata));a.setMethod('GET');a.send();},ondata:function(b){for(var a=0;a<b.length;++a)this.addResult(b[a]);if(this.lastValue!==null)this.matchResults(this.lastValue);this.ready=true;}}});JX.install('TypeaheadOnDemandSource',{extend:'TypeaheadSource',construct:function(a){this.__super__.call(this);this.uri=a;this.haveData={'':true};},properties:{queryDelay:125,auxiliaryData:{}},members:{uri:null,lastChange:null,haveData:null,didChange:function(a){if(JX.Stratcom.pass())return;this.lastChange=new Date().getTime();a=this.normalize(a);if(this.haveData[a]){this.matchResults(a);}else{this.waitForResults();JX.defer(JX.bind(this,this.sendRequest,this.lastChange,a),this.getQueryDelay());}JX.Stratcom.context().kill();},sendRequest:function(c,b){if(c!=this.lastChange)return;var a=new JX.Request(this.uri,JX.bind(this,this.ondata,this.lastChange,b));a.setMethod('GET');a.setData(JX.copy(this.getAuxiliaryData(),{q:b}));a.send();},ondata:function(d,c,b){for(var a=0;a<b.length;a++)this.addResult(b[a]);this.haveData[c]=true;if(d!=this.lastChange)return;this.matchResults(c);}}});JX.install('Tokenizer',{construct:function(a){this._s=a;},properties:{limit:null,nextInput:null},members:{_s:null,_c:null,_i:null,_t:null,_p:null,_u:0,_v:null,_w:null,_x:null,_y:0,_z:null,start:function(){this._t=JX.DOM.find(this._s,'input','tokenizer');this._v=[];this._w={};var a=JX.$N('input',{className:'jx-tokenizer-input',type:'text',value:this._t.value});this._i=a;JX.DOM.listen(a,['click','focus','blur','keydown'],null,JX.bind(this,this.handleEvent));JX.DOM.listen(this._s,'click',null,JX.bind(this,function(d){if(d.getNodes().remove){this._za(d.getData().token.key);}else if(d.getTarget()==this._c)this.focus();}));var b=JX.$N('div');b.id=this._t.id;JX.DOM.alterClass(b,'jx-tokenizer',true);b.style.cursor='text';this._c=b;b.appendChild(a);var c=this._p;c.setInputNode(this._i);c.start();JX.defer(JX.bind(this,function(){JX.DOM.setContent(this._t.parentNode,b);var e=this._x||{};for(var d in e)this.addToken(d,e[d]);this._zb();}));},setInitialValue:function(a){this._x=a;return this;},setTypeahead:function(a){a.setAllowNullSelection(false);a.listen('choose',JX.bind(this,function(b){JX.Stratcom.context().prevent();if(this.addToken(b.rel,b.name)){this._p.hide();this._i.value='';this._zb();this.focus();}}));a.listen('query',JX.bind(this,function(b){if(b.length)JX.Stratcom.context().prevent();}));this._p=a;return this;},handleEvent:function(a){this._p.handleEvent(a);if(a.getPrevented())return;if(a.getType()=='click'){if(a.getTarget()==this._c){this.focus();a.prevent();return;}}else if(a.getType()=='keydown'){this._zc(a);}else if(a.getType()=='blur')this._zb();},refresh:function(){this._zb(true);return this;},_zb:function(b){var a=this._i;if(a.value===this._z&&!b)return;this._z=a.value;var e=this._c;var d=JX.DOM.textMetrics(this._i,'jx-tokenizer-metrics');d.y=null;d.x+=24;d.setDim(a);a.value=a.value;var c=JX.$V(a).add(JX.$V.getDim(a)).y-JX.$V(e).y;e.style.height=c+'px';},addToken:function(c,g){if(c in this._w)return false;var a=this._i;var e=this._c;var f=JX.$N('a',{className:'jx-tokenizer-token'},g);var b=JX.$N('input',{type:'hidden',value:c,name:this._t.name+'['+(this._y++)+']'});var d=JX.$N('a',{className:'jx-tokenizer-x'},JX.HTML('&times;'));this._w[c]={value:g,key:c,node:f};this._v.push(c);JX.Stratcom.sigilize(f,'token',{key:c});JX.Stratcom.sigilize(d,'remove');f.appendChild(b);f.appendChild(d);e.insertBefore(f,a);return true;},getTokens:function(){var b={};for(var a in this._w)b[a]=this._w[a].value;return b;},_zc:function(b){var c=this._i;var d=this._c;switch(b.getSpecialKey()){case 'tab':var a=this._p.submit();if(this.getNextInput()){if(!a)this._i.value='';JX.defer(JX.bind(this,function(){this.getNextInput().focus();}));}break;case 'delete':if(!this._i.value.length){var e;while(e=this._v.pop())if(this._za(e))break;}break;case 'return':break;default:if(this.getLimit()&&JX.keys(this._w).length==this.getLimit())b.prevent();JX.defer(JX.bind(this,this._zb));break;}},_za:function(a){if(!this._w[a])return false;JX.DOM.remove(this._w[a].node);delete this._w[a];this._zb(true);this.focus();return true;},focus:function(){var a=this._i;JX.DOM.show(a);JX.defer(function(){JX.DOM.focus(a);});}}});

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jul 29, 6:49 AM (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
188159
Default Alt Text
(80 KB)

Event Timeline