Page MenuHomestyx hydra

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/conf/default.conf.php b/conf/default.conf.php
index ee9cbf31bb..a668c27d79 100644
--- a/conf/default.conf.php
+++ b/conf/default.conf.php
@@ -1,77 +1,77 @@
<?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.
*/
return array(
// The root URI which Phabricator is installed on.
// Example: "http://phabricator.example.com/"
'phabricator.base-uri' => null,
'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3',
// -- MySQL --------------------------------------------------------------- //
// The username to use when connecting to MySQL.
'mysql.user' => 'root',
-
+
// The password to use when connecting to MySQL.
'mysql.pass' => '',
-
+
// The MySQL server to connect to.
'mysql.host' => 'localhost',
// -- Facebook ------------------------------------------------------------ //
// Can users use Facebook credentials to login to Phabricator?
'facebook.auth-enabled' => false,
// The Facebook "Application ID" to use for Facebook API access.
'facebook.application-id' => null,
// The Facebook "Application Secret" to use for Facebook API access.
'facebook.application-secret' => null,
// -- Recaptcha ------------------------------------------------------------- //
// Is Recaptcha enabled? If disabled, captchas will not appear.
'recaptcha.enabled' => false,
-
+
// Your Recaptcha public key, obtained from Recaptcha.
'recaptcha.public-key' => null,
-
+
// Your Recaptcha private key, obtained from Recaptcha.
'recaptcha.private-key' => null,
'user.default-profile-image-phid' => 'PHID-FILE-f57aaefce707fc4060ef',
-
+
// When email is sent, try to hand it off to the MTA immediately. The only
// reason to disable this is if your MTA infrastructure is completely
// terrible. If you disable this option, you must run the 'metamta_mta.php'
// daemon or mail won't be handed off to the MTA.
'metamta.send-immediately' => true,
-
+
);
diff --git a/scripts/daemons/metamta/metamta_mta.php b/scripts/daemons/metamta/metamta_mta.php
index e3906efd34..6e6b7b9d0f 100644
--- a/scripts/daemons/metamta/metamta_mta.php
+++ b/scripts/daemons/metamta/metamta_mta.php
@@ -1,3 +1,19 @@
<?php
-// Placeholder so I don't forget about this, hopefully.
\ No newline at end of file
+/*
+ * 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.
+ */
+
+// Placeholder so I don't forget about this, hopefully.
diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php
index 7d01e98ab8..d50c0b928e 100644
--- a/src/__celerity_resource_map__.php
+++ b/src/__celerity_resource_map__.php
@@ -1,360 +1,397 @@
<?php
/**
* This file is automatically generated. Use 'celerity_mapper.php' to rebuild
* it.
* @generated
*/
celerity_register_resource_map(array(
'aphront-dialog-view-css' =>
array(
- 'uri' => '/res/771b987d/rsrc/css/aphront/dialog-view.css',
+ 'uri' => '/res/d98e6292/rsrc/css/aphront/dialog-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/dialog-view.css',
),
'aphront-form-view-css' =>
array(
'uri' => '/res/785ac1c6/rsrc/css/aphront/form-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/form-view.css',
),
'aphront-panel-view-css' =>
array(
'uri' => '/res/63672373/rsrc/css/aphront/panel-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/panel-view.css',
),
'aphront-request-failure-view-css' =>
array(
'uri' => '/res/97b8337a/rsrc/css/aphront/request-failure-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/request-failure-view.css',
),
'aphront-side-nav-view-css' =>
array(
'uri' => '/res/0fc0545c/rsrc/css/aphront/side-nav-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/side-nav-view.css',
),
'aphront-table-view-css' =>
array(
'uri' => '/res/52b0191f/rsrc/css/aphront/table-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/table-view.css',
),
'aphront-tokenizer-control-css' =>
array(
'uri' => '/res/a3d23074/rsrc/css/aphront/tokenizer.css',
'type' => 'css',
'requires' =>
array(
0 => 'aphront-typeahead-control-css',
),
'disk' => '/rsrc/css/aphront/tokenizer.css',
),
'aphront-typeahead-control-css' =>
array(
'uri' => '/res/928df9f0/rsrc/css/aphront/typeahead.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/aphront/typeahead.css',
),
'phabricator-standard-page-view' =>
array(
'uri' => '/res/fb02fb0e/rsrc/css/application/base/standard-page-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/base/standard-page-view.css',
),
'differential-revision-add-comment-css' =>
array(
'uri' => '/res/623fef21/rsrc/css/application/differential/add-comment.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/add-comment.css',
),
'differential-changeset-view-css' =>
array(
'uri' => '/res/658d181a/rsrc/css/application/differential/changeset-view.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/changeset-view.css',
),
'differential-core-view-css' =>
array(
'uri' => '/res/525d1a12/rsrc/css/application/differential/core.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/core.css',
),
'differential-revision-comment-list-css' =>
array(
'uri' => '/res/a1c117db/rsrc/css/application/differential/revision-comment-list.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/revision-comment-list.css',
),
'differential-revision-comment-css' =>
array(
'uri' => '/res/bf6369c6/rsrc/css/application/differential/revision-comment.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/revision-comment.css',
),
'differential-revision-detail-css' =>
array(
'uri' => '/res/230a67c6/rsrc/css/application/differential/revision-detail.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/revision-detail.css',
),
'differential-revision-history-css' =>
array(
'uri' => '/res/755f3da3/rsrc/css/application/differential/revision-history.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/revision-history.css',
),
'differential-table-of-contents-css' =>
array(
'uri' => '/res/a4a7b2b5/rsrc/css/application/differential/table-of-contents.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/differential/table-of-contents.css',
),
'phabricator-directory-css' =>
array(
'uri' => '/res/6a000601/rsrc/css/application/directory/phabricator-directory.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/directory/phabricator-directory.css',
),
'phabricator-core-buttons-css' =>
array(
'uri' => '/res/6e348ba4/rsrc/css/core/buttons.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/core/buttons.css',
),
'phabricator-core-css' =>
array(
'uri' => '/res/39ce37c2/rsrc/css/core/core.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/core/core.css',
),
+ 'phabricator-core-dialog-css' =>
+ array(
+ 'uri' => '/res/d9580553/rsrc/css/core/dialog.css',
+ 'type' => 'css',
+ 'requires' =>
+ array(
+ ),
+ 'disk' => '/rsrc/css/core/dialog.css',
+ ),
'phabricator-remarkup-css' =>
array(
'uri' => '/res/786989c3/rsrc/css/core/remarkup.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/core/remarkup.css',
),
'syntax-highlighting-css' =>
array(
'uri' => '/res/fb673ece/rsrc/css/core/syntax.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/core/syntax.css',
),
'javelin-behavior-aphront-basic-tokenizer' =>
array(
'uri' => '/res/8317d761/rsrc/js/application/core/behavior-tokenizer.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-lib-dev',
),
'disk' => '/rsrc/js/application/core/behavior-tokenizer.js',
),
'javelin-behavior-differential-feedback-preview' =>
array(
'uri' => '/res/34fbb670/rsrc/js/application/differential/behavior-comment-preview.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-lib-dev',
),
'disk' => '/rsrc/js/application/differential/behavior-comment-preview.js',
),
+ 'javelin-behavior-differential-edit-inline-comments' =>
+ array(
+ 'uri' => '/res/51d7da98/rsrc/js/application/differential/behavior-edit-inline-comments.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ 0 => 'javelin-lib-dev',
+ ),
+ 'disk' => '/rsrc/js/application/differential/behavior-edit-inline-comments.js',
+ ),
'javelin-behavior-differential-populate' =>
array(
'uri' => '/res/f7efbf62/rsrc/js/application/differential/behavior-populate.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-lib-dev',
),
'disk' => '/rsrc/js/application/differential/behavior-populate.js',
),
'javelin-behavior-differential-show-more' =>
array(
'uri' => '/res/d26ebcae/rsrc/js/application/differential/behavior-show-more.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-lib-dev',
),
'disk' => '/rsrc/js/application/differential/behavior-show-more.js',
),
- 'javelin-init-dev' =>
+ 'javelin-magical-init' =>
array(
- 'uri' => '/res/c57a9e89/rsrc/js/javelin/init.dev.js',
+ 'uri' => '/res/76614f84/rsrc/js/javelin/init.dev.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/init.dev.js',
),
'javelin-init-prod' =>
array(
- 'uri' => '/res/f0172c54/rsrc/js/javelin/init.min.js',
+ 'uri' => '/res/ce6bff38/rsrc/js/javelin/init.min.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/init.min.js',
),
'javelin-lib-dev' =>
array(
- 'uri' => '/res/3e747182/rsrc/js/javelin/javelin.dev.js',
+ 'uri' => '/res/29c9b6b4/rsrc/js/javelin/javelin.dev.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/javelin.dev.js',
),
'javelin-lib-prod' =>
array(
- 'uri' => '/res/9438670e/rsrc/js/javelin/javelin.min.js',
+ 'uri' => '/res/ef13c830/rsrc/js/javelin/javelin.min.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/javelin.min.js',
),
'javelin-typeahead-dev' =>
array(
- 'uri' => '/res/c81c0f01/rsrc/js/javelin/typeahead.dev.js',
+ 'uri' => '/res/6de6ae59/rsrc/js/javelin/typeahead.dev.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/typeahead.dev.js',
),
'javelin-typeahead-prod' =>
array(
- 'uri' => '/res/1da2d984/rsrc/js/javelin/typeahead.min.js',
+ 'uri' => '/res/593d9bb8/rsrc/js/javelin/typeahead.min.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/typeahead.min.js',
),
+ 'javelin-workflow-dev' =>
+ array(
+ 'uri' => '/res/2d740661/rsrc/js/javelin/workflow.dev.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ ),
+ 'disk' => '/rsrc/js/javelin/workflow.dev.js',
+ ),
+ 'javelin-workflow-prod' =>
+ array(
+ 'uri' => '/res/b758e0a0/rsrc/js/javelin/workflow.min.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ ),
+ 'disk' => '/rsrc/js/javelin/workflow.min.js',
+ ),
), array (
'packages' =>
array (
- 'd348c79d' =>
+ 'dc2390af' =>
array (
'name' => 'core.pkg.css',
'symbols' =>
array (
0 => 'phabricator-core-css',
1 => 'phabricator-core-buttons-css',
2 => 'phabricator-standard-page-view',
3 => 'aphront-dialog-view-css',
4 => 'aphront-form-view-css',
5 => 'aphront-panel-view-css',
6 => 'aphront-side-nav-view-css',
7 => 'aphront-table-view-css',
8 => 'aphront-tokenizer-control-css',
9 => 'aphront-typeahead-control-css',
10 => 'phabricator-directory-css',
),
- 'uri' => '/res/pkg/d348c79d/core.pkg.css',
+ 'uri' => '/res/pkg/dc2390af/core.pkg.css',
'type' => 'css',
),
'69b11588' =>
array (
'name' => 'differential.pkg.css',
'symbols' =>
array (
0 => 'differential-core-view-css',
1 => 'differential-changeset-view-css',
2 => 'differential-revision-detail-css',
3 => 'differential-revision-history-css',
4 => 'differential-table-of-contents-css',
),
'uri' => '/res/pkg/69b11588/differential.pkg.css',
'type' => 'css',
),
),
'reverse' =>
array (
- 'phabricator-core-css' => 'd348c79d',
- 'phabricator-core-buttons-css' => 'd348c79d',
- 'phabricator-standard-page-view' => 'd348c79d',
- 'aphront-dialog-view-css' => 'd348c79d',
- 'aphront-form-view-css' => 'd348c79d',
- 'aphront-panel-view-css' => 'd348c79d',
- 'aphront-side-nav-view-css' => 'd348c79d',
- 'aphront-table-view-css' => 'd348c79d',
- 'aphront-tokenizer-control-css' => 'd348c79d',
- 'aphront-typeahead-control-css' => 'd348c79d',
- 'phabricator-directory-css' => 'd348c79d',
+ 'phabricator-core-css' => 'dc2390af',
+ 'phabricator-core-buttons-css' => 'dc2390af',
+ 'phabricator-standard-page-view' => 'dc2390af',
+ 'aphront-dialog-view-css' => 'dc2390af',
+ 'aphront-form-view-css' => 'dc2390af',
+ 'aphront-panel-view-css' => 'dc2390af',
+ 'aphront-side-nav-view-css' => 'dc2390af',
+ 'aphront-table-view-css' => 'dc2390af',
+ 'aphront-tokenizer-control-css' => 'dc2390af',
+ 'aphront-typeahead-control-css' => 'dc2390af',
+ 'phabricator-directory-css' => 'dc2390af',
'differential-core-view-css' => '69b11588',
'differential-changeset-view-css' => '69b11588',
'differential-revision-detail-css' => '69b11588',
'differential-revision-history-css' => '69b11588',
'differential-table-of-contents-css' => '69b11588',
),
));
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 9a5aacd4d4..b40da8afca 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,344 +1,346 @@
<?php
/**
* This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
* @generated
*/
phutil_register_library_map(array(
'class' =>
array(
'Aphront400Response' => 'aphront/response/400',
'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',
'AphrontException' => 'aphront/exception/base',
'AphrontFileResponse' => 'aphront/response/file',
'AphrontFormCheckboxControl' => 'view/form/control/checkbox',
'AphrontFormControl' => 'view/form/control/base',
'AphrontFormFileControl' => 'view/form/control/file',
'AphrontFormMarkupControl' => 'view/form/control/markup',
'AphrontFormPasswordControl' => 'view/form/control/password',
'AphrontFormRecaptchaControl' => 'view/form/control/recaptcha',
'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',
'AphrontQueryDuplicateKeyException' => 'storage/exception/duplicatekey',
'AphrontQueryException' => 'storage/exception/base',
'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing',
'AphrontQueryParameterException' => 'storage/exception/parameter',
'AphrontQueryRecoverableException' => 'storage/exception/recoverable',
'AphrontRedirectException' => 'aphront/exception/redirect',
'AphrontRedirectResponse' => 'aphront/response/redirect',
'AphrontRequest' => 'aphront/request',
'AphrontRequestFailureView' => 'view/page/failure',
'AphrontResponse' => 'aphront/response/base',
'AphrontSideNavView' => 'view/layout/sidenav',
'AphrontTableView' => 'view/control/table',
'AphrontURIMapper' => 'aphront/mapper',
'AphrontView' => 'view/base',
'AphrontWebpageResponse' => 'aphront/response/webpage',
'CelerityAPI' => 'infrastructure/celerity/api',
'CelerityResourceController' => 'infrastructure/celerity/controller',
'CelerityResourceMap' => 'infrastructure/celerity/map',
'CelerityStaticResourceResponse' => 'infrastructure/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',
'DifferentialAddCommentView' => 'applications/differential/view/addcomment',
'DifferentialCCWelcomeMail' => 'applications/differential/mail/ccwelcome',
'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',
'DifferentialComment' => 'applications/differential/storage/comment',
'DifferentialCommentEditor' => 'applications/differential/editor/comment',
'DifferentialCommentMail' => 'applications/differential/mail/comment',
'DifferentialCommentPreviewController' => 'applications/differential/controller/commentpreview',
'DifferentialCommentSaveController' => 'applications/differential/controller/commentsave',
'DifferentialController' => 'applications/differential/controller/base',
'DifferentialDAO' => 'applications/differential/storage/base',
'DifferentialDiff' => 'applications/differential/storage/diff',
'DifferentialDiffContentMail' => 'applications/differential/mail/diffcontent',
'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty',
'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents',
'DifferentialDiffViewController' => 'applications/differential/controller/diffview',
'DifferentialHunk' => 'applications/differential/storage/hunk',
+ 'DifferentialInlineComment' => 'applications/differential/storage/inlinecomment',
'DifferentialLintStatus' => 'applications/differential/constants/lintstatus',
'DifferentialMail' => 'applications/differential/mail/base',
'DifferentialMarkupEngineFactory' => 'applications/differential/parser/markup',
'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff',
'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest',
'DifferentialRevision' => 'applications/differential/storage/revision',
'DifferentialRevisionCommentListView' => 'applications/differential/view/revisioncommentlist',
'DifferentialRevisionCommentView' => 'applications/differential/view/revisioncomment',
'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem',
'DifferentialRevisionDetailView' => 'applications/differential/view/revisiondetail',
'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit',
'DifferentialRevisionEditor' => 'applications/differential/editor/revision',
'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist',
'DifferentialRevisionListData' => 'applications/differential/data/revisionlist',
'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus',
'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/revisionupdatehistory',
'DifferentialRevisionViewController' => 'applications/differential/controller/revisionview',
'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus',
'Javelin' => 'infrastructure/javelin/api',
'LiskDAO' => 'storage/lisk/dao',
'Phabricator404Controller' => 'applications/base/controller/404',
'PhabricatorAuthController' => 'applications/auth/controller/base',
'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',
'PhabricatorEmailLoginController' => 'applications/auth/controller/email',
'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken',
'PhabricatorEnv' => 'infrastructure/env',
'PhabricatorFacebookAuthController' => 'applications/auth/controller/facebookauth',
'PhabricatorFacebookAuthDiagnosticsController' => 'applications/auth/controller/facebookauth/diagnostics',
'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',
'PhabricatorLoginController' => 'applications/auth/controller/login',
'PhabricatorLogoutController' => 'applications/auth/controller/logout',
'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/base',
'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite',
'PhabricatorMetaMTAController' => 'applications/metamta/controller/base',
'PhabricatorMetaMTADAO' => 'applications/metamta/storage/base',
'PhabricatorMetaMTAListController' => 'applications/metamta/controller/list',
'PhabricatorMetaMTAMail' => 'applications/metamta/storage/mail',
'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist',
'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit',
'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists',
'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send',
'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
'PhabricatorObjectHandle' => 'applications/phid/handle',
'PhabricatorObjectHandleData' => 'applications/phid/handle/data',
'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',
'PhabricatorPHIDLookupController' => 'applications/phid/controller/lookup',
'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' => 'infrastructure/celerity/api',
'celerity_register_resource_map' => 'infrastructure/celerity/map',
'javelin_render_tag' => 'infrastructure/javelin/markup',
'phabricator_format_relative_time' => 'view/utils',
'phabricator_format_timestamp' => 'view/utils',
'phabricator_format_units_generic' => 'view/utils',
'qsprintf' => 'storage/qsprintf',
'queryfx' => 'storage/queryfx',
'queryfx_all' => 'storage/queryfx',
'queryfx_one' => 'storage/queryfx',
'require_celerity_resource' => 'infrastructure/celerity/api',
'vqsprintf' => 'storage/qsprintf',
'vqueryfx' => 'storage/queryfx',
'vqueryfx_all' => 'storage/queryfx',
'xsprintf_query' => 'storage/qsprintf',
),
'requires_class' =>
array(
'Aphront400Response' => 'AphrontResponse',
'Aphront404Response' => 'AphrontResponse',
'AphrontAjaxResponse' => 'AphrontResponse',
'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
'AphrontDefaultApplicationController' => 'AphrontController',
'AphrontDialogResponse' => 'AphrontResponse',
'AphrontDialogView' => 'AphrontView',
'AphrontErrorView' => 'AphrontView',
'AphrontFileResponse' => 'AphrontResponse',
'AphrontFormCheckboxControl' => 'AphrontFormControl',
'AphrontFormControl' => 'AphrontView',
'AphrontFormFileControl' => 'AphrontFormControl',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormPasswordControl' => 'AphrontFormControl',
'AphrontFormRecaptchaControl' => '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',
'AphrontQueryDuplicateKeyException' => 'AphrontQueryException',
'AphrontQueryObjectMissingException' => 'AphrontQueryException',
'AphrontQueryParameterException' => 'AphrontQueryException',
'AphrontQueryRecoverableException' => 'AphrontQueryException',
'AphrontRedirectException' => 'AphrontException',
'AphrontRedirectResponse' => 'AphrontResponse',
'AphrontRequestFailureView' => 'AphrontView',
'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',
'DifferentialAddCommentView' => 'AphrontView',
'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail',
'DifferentialChangeset' => 'DifferentialDAO',
'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetListView' => 'AphrontView',
'DifferentialChangesetViewController' => 'DifferentialController',
'DifferentialComment' => 'DifferentialDAO',
'DifferentialCommentMail' => 'DifferentialMail',
'DifferentialCommentPreviewController' => 'DifferentialController',
'DifferentialCommentSaveController' => 'DifferentialController',
'DifferentialController' => 'PhabricatorController',
'DifferentialDAO' => 'PhabricatorLiskDAO',
'DifferentialDiff' => 'DifferentialDAO',
'DifferentialDiffContentMail' => 'DifferentialMail',
'DifferentialDiffProperty' => 'DifferentialDAO',
'DifferentialDiffTableOfContentsView' => 'AphrontView',
'DifferentialDiffViewController' => 'DifferentialController',
'DifferentialHunk' => 'DifferentialDAO',
+ 'DifferentialInlineComment' => 'DifferentialDAO',
'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail',
'DifferentialReviewRequestMail' => 'DifferentialMail',
'DifferentialRevision' => 'DifferentialDAO',
'DifferentialRevisionCommentListView' => 'AphrontView',
'DifferentialRevisionCommentView' => 'AphrontView',
'DifferentialRevisionDetailView' => 'AphrontView',
'DifferentialRevisionEditController' => 'DifferentialController',
'DifferentialRevisionListController' => 'DifferentialController',
'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
'DifferentialRevisionViewController' => 'DifferentialController',
'Phabricator404Controller' => 'PhabricatorController',
'PhabricatorAuthController' => 'PhabricatorController',
'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',
'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
'PhabricatorEmailTokenController' => 'PhabricatorAuthController',
'PhabricatorFacebookAuthController' => 'PhabricatorAuthController',
'PhabricatorFacebookAuthDiagnosticsController' => 'PhabricatorAuthController',
'PhabricatorFile' => 'PhabricatorFileDAO',
'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileViewController' => 'PhabricatorFileController',
'PhabricatorLiskDAO' => 'LiskDAO',
'PhabricatorLoginController' => 'PhabricatorAuthController',
'PhabricatorLogoutController' => 'PhabricatorAuthController',
'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter',
'PhabricatorMetaMTAController' => 'PhabricatorController',
'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
'PhabricatorMetaMTAListController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO',
'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO',
'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
'PhabricatorPHID' => 'PhabricatorPHIDDAO',
'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
'PhabricatorPHIDController' => 'PhabricatorController',
'PhabricatorPHIDDAO' => 'PhabricatorLiskDAO',
'PhabricatorPHIDListController' => 'PhabricatorPHIDController',
'PhabricatorPHIDLookupController' => '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/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
index ef077224d0..75bac79d0e 100644
--- a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
@@ -1,1729 +1,1731 @@
<?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 DifferentialRevisionViewController extends DifferentialController {
private $revisionID;
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$revision = id(new DifferentialRevision())->load($this->revisionID);
if (!$revision) {
return new Aphront404Response();
}
$revision->loadRelationships();
$diffs = $revision->loadDiffs();
$target = end($diffs);
$changesets = $target->loadChangesets();
$comments = $revision->loadComments();
$comments = array_merge(
$this->getImplicitComments($revision),
$comments);
$object_phids = array_merge(
$revision->getReviewers(),
$revision->getCCPHIDs(),
array(
$revision->getAuthorPHID(),
$request->getUser()->getPHID(),
),
mpull($comments, 'getAuthorPHID'));
$handles = id(new PhabricatorObjectHandleData($object_phids))
->loadHandles();
$revision_detail = new DifferentialRevisionDetailView();
$revision_detail->setRevision($revision);
$properties = $this->getRevisionProperties($revision, $target, $handles);
$revision_detail->setProperties($properties);
$actions = $this->getRevisionActions($revision);
$revision_detail->setActions($actions);
$comment_view = new DifferentialRevisionCommentListView();
$comment_view->setComments($comments);
$comment_view->setHandles($handles);
$diff_history = new DifferentialRevisionUpdateHistoryView();
$diff_history->setDiffs($diffs);
$toc_view = new DifferentialDiffTableOfContentsView();
$toc_view->setChangesets($changesets);
$changeset_view = new DifferentialChangesetListView();
$changeset_view->setChangesets($changesets);
+ $changeset_view->setEditable(true);
+ $changeset_view->setRevision($revision);
$comment_form = new DifferentialAddCommentView();
$comment_form->setRevision($revision);
$comment_form->setActions($this->getRevisionCommentActions($revision));
$comment_form->setActionURI('/differential/comment/save/');
$comment_form->setUser($request->getUser());
return $this->buildStandardPageResponse(
'<div class="differential-primary-pane">'.
$revision_detail->render().
$comment_view->render().
$diff_history->render().
$toc_view->render().
$changeset_view->render().
$comment_form->render().
'</div>',
array(
'title' => $revision->getTitle(),
));
}
private function getImplicitComments(DifferentialRevision $revision) {
$template = new DifferentialComment();
$template->setAuthorPHID($revision->getAuthorPHID());
$template->setRevisionID($revision->getID());
$template->setDateCreated($revision->getDateCreated());
$comments = array();
if (strlen($revision->getSummary())) {
$summary_comment = clone $template;
$summary_comment->setContent($revision->getSummary());
$summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE);
$comments[] = $summary_comment;
}
if (strlen($revision->getTestPlan())) {
$testplan_comment = clone $template;
$testplan_comment->setContent($revision->getTestPlan());
$testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN);
$comments[] = $testplan_comment;
}
return $comments;
}
private function getRevisionProperties(
DifferentialRevision $revision,
DifferentialDiff $diff,
array $handles) {
$properties = array();
$status = $revision->getStatus();
$status = DifferentialRevisionStatus::getNameForRevisionStatus($status);
$properties['Revision Status'] = '<strong>'.$status.'</strong>';
$author = $handles[$revision->getAuthorPHID()];
$properties['Author'] = $author->renderLink();
$properties['Reviewers'] = $this->renderHandleLinkList(
array_select_keys(
$handles,
$revision->getReviewers()));
$properties['CCs'] = $this->renderHandleLinkList(
array_select_keys(
$handles,
$revision->getCCPHIDs()));
$path = $diff->getSourcePath();
if ($path) {
$branch = $diff->getBranch() ? ' (' . $diff->getBranch() . ')' : '';
$host = $diff->getSourceMachine();
if ($host) {
$host .= ':';
}
$properties['Path'] = phutil_escape_html("{$host}{$path} {$branch}");
}
$properties['Lint'] = 'TODO';
$properties['Unit'] = 'TODO';
return $properties;
}
private function getRevisionActions(DifferentialRevision $revision) {
$viewer_phid = $this->getRequest()->getUser()->getPHID();
$viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid);
$viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
$viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs());
$status = $revision->getStatus();
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
$links = array();
if ($viewer_is_owner) {
$links[] = array(
'class' => 'revision-edit',
'href' => "/differential/revision/edit/{$revision_id}/",
'name' => 'Edit Revision',
);
}
if (!$viewer_is_owner && !$viewer_is_reviewer) {
$action = $viewer_is_cc ? 'rem' : 'add';
$links[] = array(
'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add',
'href' => "/differential/subscribe/{$action}/{$revision_id}/",
'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe',
);
} else {
$links[] = array(
'class' => 'subscribe-rem unavailable',
'name' => 'Automatically Subscribed',
);
}
$links[] = array(
'class' => 'transcripts-metamta',
'name' => 'MetaMTA Transcripts',
'href' => "/mail/?phid={$revision_phid}",
);
return $links;
}
private function renderHandleLinkList(array $list) {
if (empty($list)) {
return '<em>None</em>';
}
return implode(', ', mpull($list, 'renderLink'));
}
private function getRevisionCommentActions(DifferentialRevision $revision) {
$actions = array(
DifferentialAction::ACTION_COMMENT => true,
);
$viewer_phid = $this->getRequest()->getUser()->getPHID();
$viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID());
if ($viewer_is_owner) {
switch ($revision->getStatus()) {
case DifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ABANDON] = true;
break;
case DifferentialRevisionStatus::NEEDS_REVISION:
case DifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
break;
case DifferentialRevisionStatus::COMMITTED:
break;
case DifferentialRevisionStatus::ABANDONED:
$actions[DifferentialAction::ACTION_RECLAIM] = true;
break;
}
} else {
switch ($revision->getStatus()) {
case DifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_REJECT] = true;
break;
case DifferentialRevisionStatus::NEEDS_REVISION:
$actions[DifferentialAction::ACTION_ACCEPT] = true;
break;
case DifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_REJECT] = true;
break;
case DifferentialRevisionStatus::COMMITTED:
case DifferentialRevisionStatus::ABANDONED:
break;
}
}
$actions[DifferentialAction::ACTION_ADDREVIEWERS] = true;
return array_keys($actions);
}
}
/*
protected function getRevisionActions(DifferentialRevision $revision) {
$viewer_id = $this->getRequest()->getViewerContext()->getUserID();
$viewer_is_owner = ($viewer_id == $revision->getOwnerID());
$viewer_is_reviewer =
((array_search($viewer_id, $revision->getReviewers())) !== false);
$viewer_is_cc =
((array_search($viewer_id, $revision->getCCFBIDs())) !== false);
$status = $revision->getStatus();
$links = array();
if (!$viewer_is_owner && !$viewer_is_reviewer) {
$action = $viewer_is_cc
? 'rem'
: 'add';
$revision_id = $revision->getID();
$href = "/differential/subscribe/{$action}/{$revision_id}";
$links[] = array(
$viewer_is_cc ? 'subscribe-disabled' : 'subscribe-enabled',
<a href={$href}>{$viewer_is_cc ? 'Unsubscribe' : 'Subscribe'}</a>,
);
} else {
$links[] = array(
'subscribe-disabled unavailable',
<a>Automatically Subscribed</a>,
);
}
$blast_uri = RedirectURI(
'/intern/differential/?action=tasks&fbid='.$revision->getFBID())
->setTier('intern');
$links[] = array(
'tasks',
<a href={$blast_uri}>Edit Tasks</a>,
);
$engineering_repository_id = RepositoryRef::getByCallsign('E')->getID();
$svn_revision = $revision->getSVNRevision();
if ($status == DifferentialConstants::COMMITTED &&
$svn_revision &&
$revision->getRepositoryID() == $engineering_repository_id) {
$href = '/intern/push/request.php?rev='.$svn_revision;
$href = RedirectURI($href)->setTier('intern');
$links[] = array(
'merge',
<a href={$href} id="ask_for_merge_link">Ask for Merge</a>,
);
}
$links[] = array(
'herald-transcript',
<a href={"/herald/transcript/?fbid=".$revision->getFBID()}
>Herald Transcripts</a>,
);
$links[] = array(
'metamta-transcript',
<a href={"/mail/?view=all&fbid=".$revision->getFBID()}
>MetaMTA Transcripts</a>,
);
$list = <ul class="differential-actions" />;
foreach ($links as $link) {
list($class, $tag) = $link;
$list->appendChild(<li class={$class}>{$tag}</li>);
}
return $list;
/*
// TODO
// $sandcastle = $this->getSandcastleURI($diff);
// if ($sandcastle) {
// $fields['Sandcastle'] = <a href={$sandcastle}>{$sandcastle}</a>;
// }
$path = $diff->getSourcePath();
if ($path) {
$host = $diff->getSourceMachine();
$branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : '';
if ($host) {
// TODO
// $user = $handles[$this->getRequest()->getViewerContext()->getUserID()]
// ->getName();
$user = 'TODO';
$fields['Path'] =
<x:frag>
<a href={"ssh://{$user}@{$host}"}>{$host}</a>:{$path}{$branch}
</x:frag>;
} else {
$fields['Path'] = $path;
}
}
$reviewer_links = array();
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_links[] = <tools:handle handle={$handles[$reviewer]}
link={true} />;
}
if ($reviewer_links) {
$fields['Reviewers'] = array_implode(', ', $reviewer_links);
} else {
$fields['Reviewers'] = <em>None</em>;
}
$ccs = $revision->getCCFBIDs();
if ($ccs) {
$links = array();
foreach ($ccs as $cc) {
$links[] = <tools:handle handle={$handles[$cc]}
link={true} />;
}
$fields['CCs'] = array_implode(', ', $links);
}
$blame_rev = $revision->getSvnBlameRevision();
if ($blame_rev) {
if ($revision->getRepositoryRef() && is_numeric($blame_rev)) {
$ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev);
$fields['Blame Revision'] =
<a href={URI($ref->getDetailURL())}>
{$ref->getName()}
</a>;
} else {
$fields['Blame Revision'] = $blame_rev;
}
}
$tasks = $revision->getTaskHandles();
if ($tasks) {
$links = array();
foreach ($tasks as $task) {
$links[] = <tools:handle handle={$task} link={true} />;
}
$fields['Tasks'] = array_implode(<br />, $links);
}
$bugzilla_id = $revision->getBugzillaID();
if ($bugzilla_id) {
$href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='.
$bugzilla_id;
$fields['Bugzilla'] = <a href={$href}>{'#'.$bugzilla_id}</a>;
}
$fields['Apply Patch'] = <tt>arc patch --revision {$revision->getID()}</tt>;
if ($diff->getParentRevisionID()) {
$parent = id(new DifferentialRevision())->load(
$diff->getParentRevisionID());
if ($parent) {
$fields['Depends On'] =
<a href={$parent->getURI()}>
D{$parent->getID()}: {$parent->getName()}
</a>;
}
}
$star = <span class="star">{"\xE2\x98\x85"}</span>;
Javelin::initBehavior('differential-star-more');
switch ($diff->getLinted()) {
case Diff::LINT_FAIL:
$more = $this->renderDiffPropertyMoreLink($diff, 'lint');
$fields['Lint'] =
<x:frag>
<span class="star-warn">{$star} Lint Failures</span>
{$more}
</x:frag>;
break;
case Diff::LINT_WARNINGS:
$more = $this->renderDiffPropertyMoreLink($diff, 'lint');
$fields['Lint'] =
<x:frag>
<span class="star-warn">{$star} Lint Warnings</span>
{$more}
</x:frag>;
break;
case Diff::LINT_OKAY:
$fields['Lint'] =
<span class="star-okay">{$star} Lint Free</span>;
break;
default:
case Diff::LINT_NO:
$fields['Lint'] =
<span class="star-none">{$star} Not Linted</span>;
break;
}
$unit_details = false;
switch ($diff->getUnitTested()) {
case Diff::UNIT_FAIL:
$fields['Unit Tests'] =
<span class="star-warn">{$star} Unit Test Failures</span>;
$unit_details = true;
break;
case Diff::UNIT_WARN:
$fields['Unit Tests'] =
<span class="star-warn">{$star} Unit Test Warnings</span>;
$unit_details = true;
break;
case Diff::UNIT_OKAY:
$fields['Unit Tests'] =
<span class="star-okay">{$star} Unit Tests Passed</span>;
$unit_details = true;
break;
case Diff::UNIT_NO_TESTS:
$fields['Unit Tests'] =
<span class="star-none">{$star} No Test Coverage</span>;
break;
case Diff::UNIT_NO:
default:
$fields['Unit Tests'] =
<span class="star-none">{$star} Not Unit Tested</span>;
break;
}
if ($unit_details) {
$fields['Unit Tests'] =
<x:frag>
{$fields['Unit Tests']}
{$this->renderDiffPropertyMoreLink($diff, 'unit')}
</x:frag>;
}
$platform_impact = $revision->getPlatformImpact();
if ($platform_impact) {
$fields['Platform Impact'] =
<text linebreaks="true">{$platform_impact}</text>;
}
return $fields;
}
}
/*
protected function getSandcastleURI(Diff $diff) {
$uri = $this->getDiffProperty($diff, 'facebook:sandcastle_uri');
if (!$uri) {
$uri = $diff->getSandboxURL();
}
return $uri;
}
protected function getDiffProperty(Diff $diff, $property, $default = null) {
$diff_id = $diff->getID();
if (empty($this->diffProperties[$diff_id])) {
$props = id(new DifferentialDiffProperty())
->loadAllWhere('diffID = %s', $diff_id);
$dict = array_pull($props, 'getData', 'getName');
$this->diffProperties[$diff_id] = $dict;
}
return idx($this->diffProperties[$diff_id], $property, $default);
}
public function process() {
$uri = $this->getRequest()->getPath();
if (starts_with($uri, '/d')) {
return <alite:redirect uri={strtoupper($uri)}/>;
}
$revision = id(new DifferentialRevision())->load($this->revisionID);
if (!$revision) {
throw new Exception("Bad revision ID.");
}
$diffs = id(new Diff())->loadAllWhere(
'revisionID = %d',
$revision->getID());
$diffs = array_psort($diffs, 'getID');
$request = $this->getRequest();
$new = $request->getInt('new');
$old = $request->getInt('old');
if (($new || $old) && $new <= $old) {
throw new Exception(
"You can only view the diff of an older update relative to a newer ".
"update.");
}
if ($new && empty($diffs[$new])) {
throw new Exception(
"The 'new' diff does not exist.");
} else if ($new) {
$diff = $diffs[$new];
} else {
$diff = end($diffs);
if (!$diff) {
throw new Exception("No diff attached to this revision?");
}
$new = $diff->getID();
}
$target_diff = $diff;
if ($old && empty($diffs[$old])) {
throw new Exception(
"The 'old' diff does not exist.");
}
$rows = array(array('Base', '', true, false, null,
$diff->getSourceControlBaseRevision()
? $diff->getSourceControlBaseRevision()
: <em>Master</em>));
$idx = 0;
foreach ($diffs as $cdiff) {
$rows[] = array(
'Diff '.(++$idx),
$cdiff->getID(),
$cdiff->getID() != max(array_pull($diffs, 'getID')),
true,
$cdiff->getDateCreated(),
$cdiff->getDescription()
? $cdiff->getDescription()
: <em>No description available.</em>,
$cdiff->getUnitTested(),
$cdiff->getLinted());
}
$diff_table =
<table class="differential-diff-differ">
<tr>
<th>Diff</th>
<th>Diff ID</th>
<th>Description</th>
<th>Age</th>
<th>Lint</th>
<th>Unit</th>
</tr>
</table>;
$ii = 0;
$old_ids = array();
foreach ($rows as $row) {
$xold = null;
if ($row[2]) {
$lradio = <input name="old" value={$row[1]} type="radio"
disabled={$row[1] >= $new}
checked={$old == $row[1]} />;
if ($old == $row[1]) {
$xold = 'old-now';
}
$old_ids[] = $lradio->requireUniqueID();
} else {
$lradio = null;
}
$xnew = null;
if ($row[3]) {
$rradio = <input name="new" value={$row[1]} type="radio"
sigil="new-radio"
checked={$new == $row[1]} />;
if ($new == $row[1]) {
$xnew = 'new-now';
}
} else {
$rradio = null;
}
if ($row[3]) {
$unit_star = 'star-none';
switch ($row[6]) {
case Diff::UNIT_FAIL:
case Diff::UNIT_WARN: $unit_star = 'star-warn'; break;
case Diff::UNIT_OKAY: $unit_star = 'star-okay'; break;
}
$lint_star = 'star-none';
switch ($row[7]) {
case Diff::LINT_FAIL:
case Diff::LINT_WARNINGS: $lint_star = 'star-warn'; break;
case Diff::LINT_OKAY: $lint_star = 'star-okay'; break;
}
$star = "\xE2\x98\x85";
$unit_star =
<span class={$unit_star}>
<span class="star">{$star}</span>
</span>;
$lint_star =
<span class={$lint_star}>
<span class="star">{$star}</span>
</span>;
} else {
$unit_star = null;
$lint_star = null;
}
$diff_table->appendChild(
<tr class={++$ii % 2 ? 'alt' : null}>
<td class="name">{$row[0]}</td>
<td class="diffid">{$row[1]}</td>
<td class="desc">{$row[5]}</td>
<td class="age">{$row[4] ? ago(time() - $row[4]) : null}</td>
<td class="star">{$lint_star}</td>
<td class="star">{$unit_star}</td>
<td class={"old {$xold}"}>{$lradio}</td>
<td class={"new {$xnew}"}>{$rradio}</td>
</tr>);
}
Javelin::initBehavior('differential-diff-radios', array(
'radios' => $old_ids,
));
$diff_table->appendChild(
<tr>
<td colspan="8" class="diff-differ-submit">
<label>Whitespace Changes:</label>
{id(<select name="whitespace" />)->setOptions(
array(
'ignore-all' => 'Ignore All',
'ignore-trailing' => 'Ignore Trailing',
'show-all' => 'Show All',
), $request->getStr('whitespace'))}{' '}
<button type="submit">Show Diff</button>
</td>
</tr>);
$diff_table =
<div class="differential-table-of-contents">
<h1>Revision Update History</h1>
<form action={URI::getRequestURI()} method="get">
{$diff_table}
</form>
</div>;
$load_ids = array_filter(array($old, $diff->getID()));
$viewer_id = $this->getRequest()->getViewerContext()->getUserID();
$raw_objects = queryfx_all(
smc_get_db('cdb.differential', 'r'),
'SELECT * FROM changeset WHERE changeset.diffID IN (%Ld)',
$load_ids);
$raw_objects = array_group($raw_objects, 'diffID');
$objects = $raw_objects[$diff->getID()];
if (!$objects) {
$changesets = array();
} else {
$changesets = id(new DifferentialChangeset())->loadAllFromArray($objects);
}
$against_warn = null;
$against_map = array();
$visible_changesets = array();
if ($old) {
$old_diff = $diffs[$old];
$new_diff = $diff;
$old_path = $old_diff->getSourcePath();
$new_path = $new_diff->getSourcePath();
$old_prefix = null;
$new_prefix = null;
if ((strlen($old_path) < strlen($new_path)) &&
(!strncmp($old_path, $new_path, strlen($old_path)))) {
$old_prefix = substr($new_path, strlen($old_path));
}
if ((strlen($new_path) < strlen($old_path)) &&
(!strncmp($old_path, $new_path, strlen($new_path)))) {
$new_prefix = substr($old_path, strlen($new_path));
}
$old_changesets = id(new DifferentialChangeset())
->loadAllFromArray($raw_objects[$old]);
$old_changesets = array_pull($old_changesets, null, 'getFilename');
if ($new_prefix) {
$rekeyed_map = array();
foreach ($old_changesets as $key => $value) {
$rekeyed_map[$new_prefix.$key] = $value;
}
$old_changesets = $rekeyed_map;
}
foreach ($changesets as $key => $changeset) {
$file = $old_prefix.$changeset->getFilename();
if (isset($old_changesets[$file])) {
$checksum = $changeset->getChecksum();
if ($checksum !== null &&
$checksum == $old_changesets[$file]->getChecksum()) {
unset($changesets[$key]);
unset($old_changesets[$file]);
} else {
$against_map[$changeset->getID()] = $old_changesets[$file]->getID();
unset($old_changesets[$file]);
}
}
}
foreach ($old_changesets as $changeset) {
$changesets[$changeset->getID()] = $changeset;
$against_map[$changeset->getID()] = -1;
}
$against_warn =
<tools:notice title="NOTE - Diff of Diffs">
You are viewing a synthetic diff between two previous diffs in this
revision. You can not add new inline comments (for now).
</tools:notice>;
} else {
$visible_changesets = array_pull($changesets, 'getID');
}
$changesets = array_psort($changesets, 'getSortKey');
$all_changesets = $changesets;
$warning = null;
$limit = 100;
if (count($changesets) > $limit && !$this->getRequest()->getStr('large')) {
$count = number_format(count($changesets));
$warning =
<tools:notice title="Very Large Diff">
This diff is extremely large and affects {$count} files. Only the
first {number_format($limit)} files are shown.
<strong>
<a href={$revision->getURI().'?large=true'}>Show All Files</a>
</strong>
</tools:notice>;
$changesets = array_slice($changesets, 0, $limit);
if (!$old) {
$visible_changesets = array_pull($changesets, 'getID');
}
}
$detail_view =
<differential:changeset-detail-view
changesets={$changesets}
revision={$revision}
against={$against_map}
edit={empty($against_map)}
whitespace={$request->getStr('whitespace')} />;
$table_of_contents =
<differential:changeset-table-of-contents
changesets={$all_changesets} />;
$implied_feedback = array();
foreach (array(
'summarize' => $revision->getSummary(),
'testplan' => $revision->getTestPlan(),
'annotate' => $revision->getNotes(),
) as $type => $text) {
if (!strlen($text)) {
continue;
}
$implied_feedback[] = id(new DifferentialFeedback())
->setUserID($revision->getOwnerID())
->setAction($type)
->setDateCreated($revision->getDateCreated())
->setContent($text);
}
$feedback = id(new DifferentialFeedback())->loadAllWithRevision($revision);
$feedback = array_merge($implied_feedback, $feedback);
$inline_comments = $this->loadInlineComments($feedback, $changesets);
$diff_map = array();
$diffs = array_psort($diffs, 'getID');
foreach ($diffs as $diff) {
$diff_map[$diff->getID()] = count($diff_map) + 1;
}
$visible_changesets = array_fill_keys($visible_changesets, true);
$hidden_changesets = array();
foreach ($changesets as $changeset) {
$id = $changeset->getID();
if (isset($visible_changesets[$id])) {
continue;
}
$hidden_changesets[$id] = $diff_map[$changeset->getDiffID()];
}
$revision->loadRelationships();
$ccs = $revision->getCCFBIDs();
$reviewers = $revision->getReviewers();
$actors = array_pull($feedback, 'getUserID');
$actors[] = $revision->getOwnerID();
$tasks = array();
assoc_get_by_type(
$revision->getFBID(),
22284182462, // TODO: include issue, DIFFCAMP_TASK_ASSOC
$start = null,
$limit = null,
$pending = true,
$tasks);
memcache_dispatch();
$tasks = array_keys($tasks);
$preparer = new Preparer();
$fbids = array_merge_fast(
array($actors, array($viewer_id), $reviewers, $ccs, $tasks),
true);
$handles = array();
$handle_data = id(new ToolsHandleData($fbids, $handles))
->needNames()
->needAlternateNames()
->needAlternateIDs()
->needThumbnails();
$preparer->waitFor($handle_data);
$preparer->go();
$revision->attachTaskHandles(array_select_keys($handles, $tasks));
$inline_comments = array_group($inline_comments, 'getFeedbackID');
$engine = new RemarkupEngine();
$engine->enableFeature(RemarkupEngine::FEATURE_GUESS_IMAGES);
$engine->enableFeature(RemarkupEngine::FEATURE_YOUTUBE);
$engine->setCurrentSandcastle($this->getSandcastleURI($target_diff));
$feed = array();
foreach ($feedback as $comment) {
$inlines = null;
if (isset($inline_comments[$comment->getID()])) {
$inlines = $inline_comments[$comment->getID()];
}
$feed[] =
<differential:feedback
feedback={$comment}
handle={$handles[$comment->getUserID()]}
engine={$engine}
inline={$inlines}
changesets={$changesets}
hidden={$hidden_changesets} />;
}
$feed = $this->renderFeedbackList($feed, $feedback, $viewer_id);
$fields = $this->getDetailFields($revision, $diff, $handles);
$table = <table class="differential-revision-properties" />;
foreach ($fields as $key => $value) {
$table->appendChild(
<tr>
<th>{$key}:</th><td>{$value}</td>
</tr>);
}
$quick_links = $this->getQuickLinks($revision);
$edit_link = null;
if ($revision->getOwnerID() == $viewer_id) {
$edit_link = '/differential/revision/edit/'.$revision->getID().'/';
$edit_link =
<x:frag>
{' '}(<a href={$edit_link}>Edit Revision</a>)
</x:frag>;
}
$info =
<div class="differential-revision-information">
<div class="differential-revision-actions">
{$quick_links}
</div>
<div class="differential-revision-detail">
<h1>{$revision->getName()}{$edit_link}</h1>
{$table}
</div>
</div>;
$actions = $this->getRevisionActions($revision);
$revision_id = $revision->getID();
Javelin::initBehavior(
'differential-feedback-preview',
array(
'uri' => '/differential/preview/'.$revision->getFBID().'/',
'preview' => 'overall-feedback-preview',
'action' => 'feedback-action',
'content' => 'feedback-content',
));
Javelin::initBehavior(
'differential-inline-comment-preview',
array(
'uri' => '/differential/inline-preview/'.$revision_id.'/'.$new.'/',
'preview' => 'inline-comment-preview',
));
$content = SavedCopy::loadData(
$viewer_id,
SavedCopy::Type_DifferentialRevisionFeedback,
$revision->getFBID());
$inline_comment_container =
<div id="inline-comment-preview"><p>Loading...</p></div>;
$feedback = id(new DifferentialFeedback())
->setAction('none')
->setUserID($viewer_id)
->setContent($content);
$preview =
<div class="differential-feedback differential-feedback-preview">
<div id="overall-feedback-preview">
<differential:feedback
feedback={$feedback}
engine={$engine}
preview={true}
handle={$handles[$viewer_id]} />
</div>
{$inline_comment_container}
</div>;
$syntax_link =
<a href={'http://www.intern.facebook.com/intern/wiki/index.php' .
'/Articles/Remarkup_Syntax_Reference'}
target="_blank"
tabindex="4">Remarkup Reference</a>;
Javelin::initBehavior(
'differential-add-reviewers',
array(
'src' => redirect_str('/datasource/employee/', 'tools'),
'tokenizer' => 'reviewer-tokenizer',
'select' => 'feedback-action',
'row' => 'reviewer-tokenizer-row',
));
$feedback_form =
<x:frag>
<div class="differential-feedback-form">
<tools:form
method="post"
action={"/differential/revision/feedback/{$revision_id}/"}>
<h1>Provide Feedback</h1>
<tools:fieldset>
<tools:control type="select" label="Action">
{id(<select name="action" id="feedback-action"
tabindex="1" />)
->setOptions($actions)}
</tools:control>
<tools:control type="text" label="Reviewers"
style="display: none;"
id="reviewer-tokenizer-row">
<javelin:tokenizer-template
id="reviewer-tokenizer"
name="reviewers" />
</tools:control>
<tools:control type="textarea" label="Feedback"
caption={$syntax_link}>
<tools:droppable-textarea id="feedback-content" name="feedback"
tabindex="2">
{$content}
</tools:droppable-textarea>
</tools:control>
<tools:control type="submit">
<button type="submit"
tabindex="3">Clowncopterize</button>
</tools:control>
</tools:fieldset>
</tools:form>
</div>
{$preview}
</x:frag>;
$notice = null;
if ($this->getRequest()->getBool('diff_changed')) {
$notice =
<tools:notice title="Revision Updated Recently">
This revision was updated with a <strong>new diff</strong> while you
were providing feedback. Your inline comments appear on the
<strong>old diff</strong>.
</tools:notice>;
}
return
<differential:standard-page title={$revision->getName()}>
<div class="differential-primary-pane">
{$warning}
{$notice}
{$info}
<div class="differential-feedback">
{$feed}
</div>
{$diff_table}
{$table_of_contents}
{$against_warn}
{$detail_view}
{$feedback_form}
</div>
</differential:standard-page>;
}
protected function getQuickLinks(DifferentialRevision $revision) {
$viewer_id = $this->getRequest()->getViewerContext()->getUserID();
$viewer_is_owner = ($viewer_id == $revision->getOwnerID());
$viewer_is_reviewer =
((array_search($viewer_id, $revision->getReviewers())) !== false);
$viewer_is_cc =
((array_search($viewer_id, $revision->getCCFBIDs())) !== false);
$status = $revision->getStatus();
$links = array();
if (!$viewer_is_owner && !$viewer_is_reviewer) {
$action = $viewer_is_cc
? 'rem'
: 'add';
$revision_id = $revision->getID();
$href = "/differential/subscribe/{$action}/{$revision_id}";
$links[] = array(
$viewer_is_cc ? 'subscribe-disabled' : 'subscribe-enabled',
<a href={$href}>{$viewer_is_cc ? 'Unsubscribe' : 'Subscribe'}</a>,
);
} else {
$links[] = array(
'subscribe-disabled unavailable',
<a>Automatically Subscribed</a>,
);
}
$blast_uri = RedirectURI(
'/intern/differential/?action=blast&fbid='.$revision->getFBID())
->setTier('intern');
$links[] = array(
'blast',
<a href={$blast_uri}>Blast Revision</a>,
);
$blast_uri = RedirectURI(
'/intern/differential/?action=tasks&fbid='.$revision->getFBID())
->setTier('intern');
$links[] = array(
'tasks',
<a href={$blast_uri}>Edit Tasks</a>,
);
if ($viewer_is_owner && false) {
$perflab_uri = RedirectURI(
'/intern/differential/?action=perflab&fbid='.$revision->getFBID())
->setTier('intern');
$links[] = array(
'perflab',
<a href={$perflab_uri}>Run in Perflab</a>,
);
}
$engineering_repository_id = RepositoryRef::getByCallsign('E')->getID();
$svn_revision = $revision->getSVNRevision();
if ($status == DifferentialConstants::COMMITTED &&
$svn_revision &&
$revision->getRepositoryID() == $engineering_repository_id) {
$href = '/intern/push/request.php?rev='.$svn_revision;
$href = RedirectURI($href)->setTier('intern');
$links[] = array(
'merge',
<a href={$href} id="ask_for_merge_link">Ask for Merge</a>,
);
}
$links[] = array(
'herald-transcript',
<a href={"/herald/transcript/?fbid=".$revision->getFBID()}
>Herald Transcripts</a>,
);
$links[] = array(
'metamta-transcript',
<a href={"/mail/?view=all&fbid=".$revision->getFBID()}
>MetaMTA Transcripts</a>,
);
$list = <ul class="differential-actions" />;
foreach ($links as $link) {
list($class, $tag) = $link;
$list->appendChild(<li class={$class}>{$tag}</li>);
}
return $list;
}
protected function getDetailFields(
DifferentialRevision $revision,
Diff $diff,
array $handles) {
$fields = array();
$fields['Revision Status'] = $this->getRevisionStatusDisplay($revision);
$author = $revision->getOwnerID();
$fields['Author'] = <tools:handle handle={$handles[$author]}
link={true} />;
$sandcastle = $this->getSandcastleURI($diff);
if ($sandcastle) {
$fields['Sandcastle'] = <a href={$sandcastle}>{$sandcastle}</a>;
}
$path = $diff->getSourcePath();
if ($path) {
$host = $diff->getSourceMachine();
$branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : '';
if ($host) {
$user = $handles[$this->getRequest()->getViewerContext()->getUserID()]
->getName();
$fields['Path'] =
<x:frag>
<a href={"ssh://{$user}@{$host}"}>{$host}</a>:{$path}{$branch}
</x:frag>;
} else {
$fields['Path'] = $path;
}
}
$reviewer_links = array();
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_links[] = <tools:handle handle={$handles[$reviewer]}
link={true} />;
}
if ($reviewer_links) {
$fields['Reviewers'] = array_implode(', ', $reviewer_links);
} else {
$fields['Reviewers'] = <em>None</em>;
}
$ccs = $revision->getCCFBIDs();
if ($ccs) {
$links = array();
foreach ($ccs as $cc) {
$links[] = <tools:handle handle={$handles[$cc]}
link={true} />;
}
$fields['CCs'] = array_implode(', ', $links);
}
$blame_rev = $revision->getSvnBlameRevision();
if ($blame_rev) {
if ($revision->getRepositoryRef() && is_numeric($blame_rev)) {
$ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev);
$fields['Blame Revision'] =
<a href={URI($ref->getDetailURL())}>
{$ref->getName()}
</a>;
} else {
$fields['Blame Revision'] = $blame_rev;
}
}
$tasks = $revision->getTaskHandles();
if ($tasks) {
$links = array();
foreach ($tasks as $task) {
$links[] = <tools:handle handle={$task} link={true} />;
}
$fields['Tasks'] = array_implode(<br />, $links);
}
$bugzilla_id = $revision->getBugzillaID();
if ($bugzilla_id) {
$href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='.
$bugzilla_id;
$fields['Bugzilla'] = <a href={$href}>{'#'.$bugzilla_id}</a>;
}
$fields['Apply Patch'] = <tt>arc patch --revision {$revision->getID()}</tt>;
if ($diff->getParentRevisionID()) {
$parent = id(new DifferentialRevision())->load(
$diff->getParentRevisionID());
if ($parent) {
$fields['Depends On'] =
<a href={$parent->getURI()}>
D{$parent->getID()}: {$parent->getName()}
</a>;
}
}
$star = <span class="star">{"\xE2\x98\x85"}</span>;
Javelin::initBehavior('differential-star-more');
switch ($diff->getLinted()) {
case Diff::LINT_FAIL:
$more = $this->renderDiffPropertyMoreLink($diff, 'lint');
$fields['Lint'] =
<x:frag>
<span class="star-warn">{$star} Lint Failures</span>
{$more}
</x:frag>;
break;
case Diff::LINT_WARNINGS:
$more = $this->renderDiffPropertyMoreLink($diff, 'lint');
$fields['Lint'] =
<x:frag>
<span class="star-warn">{$star} Lint Warnings</span>
{$more}
</x:frag>;
break;
case Diff::LINT_OKAY:
$fields['Lint'] =
<span class="star-okay">{$star} Lint Free</span>;
break;
default:
case Diff::LINT_NO:
$fields['Lint'] =
<span class="star-none">{$star} Not Linted</span>;
break;
}
$unit_details = false;
switch ($diff->getUnitTested()) {
case Diff::UNIT_FAIL:
$fields['Unit Tests'] =
<span class="star-warn">{$star} Unit Test Failures</span>;
$unit_details = true;
break;
case Diff::UNIT_WARN:
$fields['Unit Tests'] =
<span class="star-warn">{$star} Unit Test Warnings</span>;
$unit_details = true;
break;
case Diff::UNIT_OKAY:
$fields['Unit Tests'] =
<span class="star-okay">{$star} Unit Tests Passed</span>;
$unit_details = true;
break;
case Diff::UNIT_NO_TESTS:
$fields['Unit Tests'] =
<span class="star-none">{$star} No Test Coverage</span>;
break;
case Diff::UNIT_NO:
default:
$fields['Unit Tests'] =
<span class="star-none">{$star} Not Unit Tested</span>;
break;
}
if ($unit_details) {
$fields['Unit Tests'] =
<x:frag>
{$fields['Unit Tests']}
{$this->renderDiffPropertyMoreLink($diff, 'unit')}
</x:frag>;
}
$platform_impact = $revision->getPlatformImpact();
if ($platform_impact) {
$fields['Platform Impact'] =
<text linebreaks="true">{$platform_impact}</text>;
}
return $fields;
}
protected function renderDiffPropertyMoreLink(Diff $diff, $name) {
$target = <div class="star-more"
style="display: none;">
<div class="star-loading">Loading...</div>
</div>;
$meta = array(
'target' => $target->requireUniqueID(),
'uri' => '/differential/diffprop/'.$diff->getID().'/'.$name.'/',
);
$more =
<span sigil="star-link-container">
&middot;
<a mustcapture="true"
sigil="star-more"
href="#"
meta={$meta}>Show Details</a>
</span>;
return <x:frag>{$more}{$target}</x:frag>;
}
protected function loadInlineComments(array $feedback, array &$changesets) {
$inline_comments = array();
$feedback_ids = array_filter(array_pull($feedback, 'getID'));
if (!$feedback_ids) {
return $inline_comments;
}
$inline_comments = id(new DifferentialInlineComment())
->loadAllWhere('feedbackID in (%Ld)', $feedback_ids);
$load_changesets = array();
$load_hunks = array();
foreach ($inline_comments as $inline) {
$changeset_id = $inline->getChangesetID();
if (isset($changesets[$changeset_id])) {
continue;
}
$load_changesets[$changeset_id] = true;
}
$more_changesets = array();
if ($load_changesets) {
$changeset_ids = array_keys($load_changesets);
$more_changesets += id(new DifferentialChangeset())
->loadAllWithIDs($changeset_ids);
}
if ($more_changesets) {
$changesets += $more_changesets;
$changesets = array_psort($changesets, 'getSortKey');
}
return $inline_comments;
}
protected function getRevisionStatusDisplay(DifferentialRevision $revision) {
$viewer_id = $this->getRequest()->getViewerContext()->getUserID();
$viewer_is_owner = ($viewer_id == $revision->getOwnerID());
$status = $revision->getStatus();
$more = null;
switch ($status) {
case DifferentialConstants::NEEDS_REVIEW:
$message = 'Pending Review';
break;
case DifferentialConstants::NEEDS_REVISION:
$message = 'Awaiting Revision';
if ($viewer_is_owner) {
$more = 'Make the requested changes and update the revision.';
}
break;
case DifferentialConstants::ACCEPTED:
$message = 'Ready for Commit';
if ($viewer_is_owner) {
$more =
<x:frag>
Run <tt>arc commit</tt> (svn) or <tt>arc amend</tt> (git) to
proceed.
</x:frag>;
}
break;
case DifferentialConstants::COMMITTED:
$message = 'Committed';
$ref = $revision->getRevisionRef();
$more = $ref
? (<a href={URI($ref->getDetailURL())}>
{$ref->getName()}
</a>)
: null;
$engineering_repository_id = RepositoryRef::getByCallsign('E')->getID();
if ($revision->getSVNRevision() &&
$revision->getRepositoryID() == $engineering_repository_id) {
Javelin::initBehavior(
'differential-revtracker-status',
array(
'uri' => '/differential/revtracker/'.$revision->getID().'/',
'statusId' => 'revtracker_status',
'mergeLinkId' => 'ask_for_merge_link',
));
}
break;
case DifferentialConstants::ABANDONED:
$message = 'Abandoned';
break;
default:
throw new Exception("Unknown revision status.");
}
if ($more) {
$message =
<x:frag>
<strong id="revtracker_status">{$message}</strong>
&middot; {$more}
</x:frag>;
} else {
$message = <strong id="revtracker_status">{$message}</strong>;
}
return $message;
}
protected function renderFeedbackList(array $xhp, array $obj, $viewer_id) {
// Use magical heuristics to try to hide older comments.
$obj = array_reverse($obj);
$obj = array_values($obj);
$xhp = array_reverse($xhp);
$xhp = array_values($xhp);
$last_comment = null;
foreach ($obj as $position => $feedback) {
if ($feedback->getUserID() == $viewer_id) {
if ($last_comment === null) {
$last_comment = $position;
} else if ($last_comment == $position - 1) {
// If you made consecuitive comments, show them all. This is a spaz
// rule for epriestley comments.
$last_comment = $position;
}
}
}
$header = array();
$hide = array();
if ($last_comment !== null) {
foreach ($obj as $position => $feedback) {
$action = $feedback->getAction();
if ($action == 'testplan' || $action == 'summarize') {
// Always show summary and test plan.
$header[] = $xhp[$position];
unset($xhp[$position]);
continue;
}
if ($position <= $last_comment) {
// Always show comments after your last comment.
continue;
}
if ($position < 3) {
// Always show the most recent 3 comments.
continue;
}
// Hide everything else.
$hide[] = $position;
}
}
if (count($hide) <= 3) {
// Don't hide if there's not much to hide.
$hide = array();
}
$header = array_reverse($header);
$hidden = array_select_keys($xhp, $hide);
$visible = array_diff_key($xhp, $hidden);
$visible = array_reverse($visible);
$hidden = array_reverse($hidden);
if ($hidden) {
Javelin::initBehavior(
'differential-show-all-feedback',
array(
'markup' => id(<x:frag>{$hidden}</x:frag>)->toString(),
));
$hidden =
<div sigil="all-feedback-container">
<div class="older-replies-are-hidden">
{number_format(count($hidden))} older replies are hidden.
<a href="#" sigil="show-all-feedback"
mustcapture="true">Show all feedback.</a>
</div>
</div>;
} else {
$hidden = null;
}
return
<x:frag>
{$header}
{$hidden}
{$visible}
</x:frag>;
}
}
protected function getDetailFields(
DifferentialRevision $revision,
Diff $diff,
array $handles) {
$fields = array();
$fields['Revision Status'] = $this->getRevisionStatusDisplay($revision);
$author = $revision->getOwnerID();
$fields['Author'] = <tools:handle handle={$handles[$author]}
link={true} />;
$sandcastle = $this->getSandcastleURI($diff);
if ($sandcastle) {
$fields['Sandcastle'] = <a href={$sandcastle}>{$sandcastle}</a>;
}
$path = $diff->getSourcePath();
if ($path) {
$host = $diff->getSourceMachine();
$branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : '';
if ($host) {
$user = $handles[$this->getRequest()->getViewerContext()->getUserID()]
->getName();
$fields['Path'] =
<x:frag>
<a href={"ssh://{$user}@{$host}"}>{$host}</a>:{$path}{$branch}
</x:frag>;
} else {
$fields['Path'] = $path;
}
}
$reviewer_links = array();
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_links[] = <tools:handle handle={$handles[$reviewer]}
link={true} />;
}
if ($reviewer_links) {
$fields['Reviewers'] = array_implode(', ', $reviewer_links);
} else {
$fields['Reviewers'] = <em>None</em>;
}
$ccs = $revision->getCCFBIDs();
if ($ccs) {
$links = array();
foreach ($ccs as $cc) {
$links[] = <tools:handle handle={$handles[$cc]}
link={true} />;
}
$fields['CCs'] = array_implode(', ', $links);
}
$blame_rev = $revision->getSvnBlameRevision();
if ($blame_rev) {
if ($revision->getRepositoryRef() && is_numeric($blame_rev)) {
$ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev);
$fields['Blame Revision'] =
<a href={URI($ref->getDetailURL())}>
{$ref->getName()}
</a>;
} else {
$fields['Blame Revision'] = $blame_rev;
}
}
$tasks = $revision->getTaskHandles();
if ($tasks) {
$links = array();
foreach ($tasks as $task) {
$links[] = <tools:handle handle={$task} link={true} />;
}
$fields['Tasks'] = array_implode(<br />, $links);
}
$bugzilla_id = $revision->getBugzillaID();
if ($bugzilla_id) {
$href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='.
$bugzilla_id;
$fields['Bugzilla'] = <a href={$href}>{'#'.$bugzilla_id}</a>;
}
$fields['Apply Patch'] = <tt>arc patch --revision {$revision->getID()}</tt>;
if ($diff->getParentRevisionID()) {
$parent = id(new DifferentialRevision())->load(
$diff->getParentRevisionID());
if ($parent) {
$fields['Depends On'] =
<a href={$parent->getURI()}>
D{$parent->getID()}: {$parent->getName()}
</a>;
}
}
$star = <span class="star">{"\xE2\x98\x85"}</span>;
Javelin::initBehavior('differential-star-more');
switch ($diff->getLinted()) {
case Diff::LINT_FAIL:
$more = $this->renderDiffPropertyMoreLink($diff, 'lint');
$fields['Lint'] =
<x:frag>
<span class="star-warn">{$star} Lint Failures</span>
{$more}
</x:frag>;
break;
case Diff::LINT_WARNINGS:
$more = $this->renderDiffPropertyMoreLink($diff, 'lint');
$fields['Lint'] =
<x:frag>
<span class="star-warn">{$star} Lint Warnings</span>
{$more}
</x:frag>;
break;
case Diff::LINT_OKAY:
$fields['Lint'] =
<span class="star-okay">{$star} Lint Free</span>;
break;
default:
case Diff::LINT_NO:
$fields['Lint'] =
<span class="star-none">{$star} Not Linted</span>;
break;
}
$unit_details = false;
switch ($diff->getUnitTested()) {
case Diff::UNIT_FAIL:
$fields['Unit Tests'] =
<span class="star-warn">{$star} Unit Test Failures</span>;
$unit_details = true;
break;
case Diff::UNIT_WARN:
$fields['Unit Tests'] =
<span class="star-warn">{$star} Unit Test Warnings</span>;
$unit_details = true;
break;
case Diff::UNIT_OKAY:
$fields['Unit Tests'] =
<span class="star-okay">{$star} Unit Tests Passed</span>;
$unit_details = true;
break;
case Diff::UNIT_NO_TESTS:
$fields['Unit Tests'] =
<span class="star-none">{$star} No Test Coverage</span>;
break;
case Diff::UNIT_NO:
default:
$fields['Unit Tests'] =
<span class="star-none">{$star} Not Unit Tested</span>;
break;
}
if ($unit_details) {
$fields['Unit Tests'] =
<x:frag>
{$fields['Unit Tests']}
{$this->renderDiffPropertyMoreLink($diff, 'unit')}
</x:frag>;
}
$platform_impact = $revision->getPlatformImpact();
if ($platform_impact) {
$fields['Platform Impact'] =
<text linebreaks="true">{$platform_impact}</text>;
}
return $fields;
}
*/
diff --git a/src/applications/differential/storage/inlinecomment/DifferentialInlineComment.php b/src/applications/differential/storage/inlinecomment/DifferentialInlineComment.php
new file mode 100644
index 0000000000..8b2446c759
--- /dev/null
+++ b/src/applications/differential/storage/inlinecomment/DifferentialInlineComment.php
@@ -0,0 +1,33 @@
+<?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 DifferentialInlineComment extends DifferentialDAO {
+
+ protected $revisionID;
+ protected $commentID;
+ protected $authorPHID;
+
+ protected $changesetID;
+ protected $isNewFile;
+
+ protected $lineNumber;
+ protected $lineLength;
+
+ protected $content;
+
+}
diff --git a/src/applications/differential/storage/inlinecomment/__init__.php b/src/applications/differential/storage/inlinecomment/__init__.php
new file mode 100644
index 0000000000..f3b257acbe
--- /dev/null
+++ b/src/applications/differential/storage/inlinecomment/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/differential/storage/base');
+
+
+phutil_require_source('DifferentialInlineComment.php');
diff --git a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
index b2c2387068..3a9b21bf17 100644
--- a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
@@ -1,128 +1,134 @@
<?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 DifferentialChangesetListView extends AphrontView {
private $changesets = array();
+ private $editable;
+ private $revision;
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
+ public function setEditable($editable) {
+ $this->editable = $editable;
+ return $this;
+ }
+
+ public function setRevision(DifferentialRevision $revision) {
+ $this->revision = $revision;
+ return $this;
+ }
+
public function render() {
require_celerity_resource('differential-changeset-view-css');
$against = array(); // TODO
$edit = false;
$changesets = $this->changesets;
foreach ($changesets as $key => $changeset) {
if (empty($against[$changeset->getID()])) {
$type = $changeset->getChangeType();
if ($type == DifferentialChangeType::TYPE_MOVE_AWAY ||
$type == DifferentialChangeType::TYPE_MULTICOPY) {
unset($changesets[$key]);
}
}
}
$output = array();
$mapping = array();
foreach ($changesets as $key => $changeset) {
$file = $changeset->getFilename();
$class = 'differential-changeset';
if (!$edit) {
$class .= ' differential-changeset-noneditable';
}
$id = $changeset->getID();
if ($id) {
$against_id = idx($against, $id);
} else {
$against_id = null;
}
/*
TODO
$detail_uri = URI($render_uri)
->addQueryData(array(
'changeset' => $id,
'against' => $against_id,
'whitespace' => $whitespace,
));
*/
$detail_uri = '/differential/changeset/?id='.$changeset->getID();
$detail_button = phutil_render_tag(
'a',
array(
'style' => 'float: right',
'class' => 'button small grey',
'href' => $detail_uri,
'target' => '_blank',
),
'Standalone View');
$uniq_id = celerity_generate_unique_node_id();
$detail = new DifferentialChangesetDetailView();
$detail->setChangeset($changeset);
$detail->addButton($detail_button);
$detail->appendChild(
phutil_render_tag(
'div',
array(
'id' => $uniq_id,
),
'<div class="differential-loading">Loading...</div>'));
$output[] = $detail->render();
$mapping[$uniq_id] = array($changeset->getID());
}
$whitespace = null;
Javelin::initBehavior('differential-populate', array(
'registry' => $mapping,
'whitespace' => $whitespace,
'uri' => '/differential/changeset/',
));
Javelin::initBehavior('differential-show-more', array(
'uri' => '/differential/changeset/',
));
-/*
-
-
- Javelin::initBehavior('differential-context', array(
- 'uri' => $render_uri,
- ));
- if ($edit) {
- require_static('remarkup-css');
- Javelin::initBehavior('differential-inline', array(
- 'uri' => '/differential/feedback/'.$revision->getID().'/',
+ if ($this->editable) {
+ $revision = $this->revision;
+ Javelin::initBehavior('differential-edit-inline-comments', array(
+ 'uri' => '/differential/inline/edit/'.$revision->getID().'/',
));
}
-*/
+
return
'<div class="differential-review-stage">'.
implode("\n", $output).
'</div>';
}
}
diff --git a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
index 96e6730ced..6c76198c6c 100644
--- a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
@@ -1,258 +1,258 @@
<?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.
*/
/**
* See #394445 for an explanation of why this thing even exists.
*/
class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
const STATUS_QUEUE = 'queued';
const STATUS_SENT = 'sent';
const STATUS_FAIL = 'fail';
const MAX_RETRIES = 250;
const RETRY_DELAY = 5;
protected $parameters;
protected $status;
protected $message;
protected $retryCount;
protected $nextRetry;
protected $relatedPHID;
public function __construct() {
$this->status = self::STATUS_QUEUE;
$this->retryCount = 0;
$this->nextRetry = time();
$this->parameters = array();
parent::__construct();
}
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
protected function setParam($param, $value) {
$this->parameters[$param] = $value;
return $this;
}
protected function getParam($param) {
return idx($this->parameters, $param);
}
public function getSubject() {
return $this->getParam('subject');
}
public function addTos(array $phids) {
$this->setParam('to', $phids);
return $this;
}
public function addCCs(array $phids) {
$this->setParam('cc', $phids);
return $this;
}
public function addHeader($name, $value) {
$this->parameters['headers'][$name] = $value;
return $this;
}
public function setFrom($from) {
$this->setParam('from', $from);
return $this;
}
public function setReplyTo($reply_to) {
$this->setParam('reply-to', $reply_to);
return $this;
}
public function setSubject($subject) {
$this->setParam('subject', $subject);
return $this;
}
public function setBody($body) {
$this->setParam('body', $body);
return $this;
}
public function setIsHTML($html) {
$this->setParam('is-html', $html);
return $this;
}
public function getSimulatedFailureCount() {
return nonempty($this->getParam('simulated-failures'), 0);
}
public function setSimulatedFailureCount($count) {
$this->setParam('simulated-failures', $count);
return $this;
}
-
+
public function save() {
$try_send = (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) &&
(!$this->getID());
-
+
$ret = parent::save();
-
+
if ($try_send) {
$mailer = new PhabricatorMailImplementationPHPMailerLiteAdapter();
$this->sendNow($force_send = false, $mailer);
}
-
+
return $ret;
}
-
+
public function sendNow(
$force_send = false,
PhabricatorMailImplementationAdapter $mailer) {
if (!$force_send) {
if ($this->getStatus() != self::STATUS_QUEUE) {
throw new Exception("Trying to send an already-sent mail!");
}
if (time() < $this->getNextRetry()) {
throw new Exception("Trying to send an email before next retry!");
}
}
-
+
try {
$parameters = $this->parameters;
$phids = array();
foreach ($parameters as $key => $value) {
switch ($key) {
case 'from':
case 'to':
case 'cc':
if (!is_array($value)) {
$value = array($value);
}
foreach (array_filter($value) as $phid) {
$phids[] = $phid;
}
break;
}
}
-
+
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
-
+
foreach ($this->parameters as $key => $value) {
switch ($key) {
case 'from':
$mailer->setFrom($handles[$value]->getEmail());
break;
case 'reply-to':
$mailer->addReplyTo($value);
break;
case 'to':
$emails = array();
foreach ($value as $phid) {
$emails[] = $handles[$phid]->getEmail();
}
$mailer->addTos($emails);
break;
case 'cc':
$emails = array();
foreach ($value as $phid) {
$emails[] = $handles[$phid]->getEmail();
}
$mailer->addCCs($emails);
break;
case 'headers':
foreach ($value as $header_key => $header_value) {
$mailer->addHeader($header_key, $header_value);
}
break;
case 'body':
$mailer->setBody($value);
break;
case 'subject':
$mailer->setSubject($value);
break;
case 'is-html':
if ($value) {
$mailer->setIsHTML(true);
}
break;
default:
// Just discard.
}
}
$mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
} catch (Exception $ex) {
$this->setStatus(self::STATUS_FAIL);
$this->setMessage($ex->getMessage());
$this->save();
return;
}
if ($this->getRetryCount() < $this->getSimulatedFailureCount()) {
$ok = false;
$error = 'Simulated failure.';
} else {
try {
$ok = $mailer->send();
} catch (Exception $ex) {
$ok = false;
$error = $ex->getMessage();
}
}
if (!$ok) {
$this->setMessage($error);
if ($this->getRetryCount() > self::MAX_RETRIES) {
$this->setStatus(self::STATUS_FAIL);
} else {
$this->setRetryCount($this->getRetryCount() + 1);
$next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY);
$this->setNextRetry($next_retry);
}
} else {
$this->setStatus(self::STATUS_SENT);
}
$this->save();
}
public static function getReadableStatus($status_code) {
static $readable = array(
self::STATUS_QUEUE => "Queued for Delivery",
self::STATUS_FAIL => "Delivery Failed",
self::STATUS_SENT => "Sent",
);
$status_code = coalesce($status_code, '?');
return idx($readable, $status_code, $status_code);
}
}
diff --git a/src/applications/metamta/storage/mail/__init__.php b/src/applications/metamta/storage/mail/__init__.php
index 43e0257082..715dad61dd 100644
--- a/src/applications/metamta/storage/mail/__init__.php
+++ b/src/applications/metamta/storage/mail/__init__.php
@@ -1,14 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
+phutil_require_module('phabricator', 'applications/metamta/adapter/phpmailerlite');
phutil_require_module('phabricator', 'applications/metamta/storage/base');
+phutil_require_module('phabricator', 'applications/phid/handle/data');
+phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorMetaMTAMail.php');
diff --git a/src/infrastructure/javelin/markup/markup.php b/src/infrastructure/javelin/markup/markup.php
index db10cf84c0..eb3f87f595 100644
--- a/src/infrastructure/javelin/markup/markup.php
+++ b/src/infrastructure/javelin/markup/markup.php
@@ -1,55 +1,48 @@
<?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.
*/
function javelin_render_tag(
$tag,
array $attributes = array(),
$content = null) {
if (isset($attributes['sigil']) ||
isset($attributes['meta']) ||
isset($attributes['mustcapture'])) {
- $classes = array();
foreach ($attributes as $k => $v) {
switch ($k) {
case 'sigil':
- $classes[] = 'FN_'.$v;
+ $attributes['data-sigil'] = $v;
unset($attributes[$k]);
break;
case 'meta':
$response = CelerityAPI::getStaticResourceResponse();
$id = $response->addMetadata($v);
- $classes[] = 'FD_'.$id;
+ $attributes['data-meta'] = $id;
unset($attributes[$k]);
break;
case 'mustcapture':
- $classes[] = 'FI_CAPTURE';
+ $attributes['data-mustcapture'] = '1';
unset($attributes[$k]);
break;
}
}
-
- if (isset($attributes['class'])) {
- $classes[] = $attributes['class'];
- }
-
- $attributes['class'] = implode(' ', $classes);
}
return phutil_render_tag($tag, $attributes, $content);
}
diff --git a/src/view/dialog/AphrontDialogView.php b/src/view/dialog/AphrontDialogView.php
index 600adbe158..e6225f460c 100755
--- a/src/view/dialog/AphrontDialogView.php
+++ b/src/view/dialog/AphrontDialogView.php
@@ -1,91 +1,92 @@
<?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 AphrontDialogView extends AphrontView {
private $title;
private $submitButton;
private $cancelURI;
private $submitURI;
public function setSubmitURI($uri) {
$this->submitURI = $uri;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
return $this->title;
}
public function addSubmitButton($text = 'Okay') {
$this->submitButton = $text;
return $this;
}
public function addCancelButton($uri) {
$this->cancelURI = $uri;
return $this;
}
final public function render() {
require_celerity_resource('aphront-dialog-view-css');
$buttons = array();
if ($this->submitButton) {
$buttons[] =
'<button name="__submit__">'.
phutil_escape_html($this->submitButton).
'</button>';
}
if ($this->cancelURI) {
$buttons[] = phutil_render_tag(
'a',
array(
'href' => $this->cancelURI,
'class' => 'button grey',
),
'Cancel');
}
- return phutil_render_tag(
+ return javelin_render_tag(
'form',
array(
'class' => 'aphront-dialog-view',
'action' => $this->submitURI,
'method' => 'post',
+ 'sigil' => 'jx-dialog',
),
'<input type="hidden" name="__form__" value="1" />'.
'<div class="aphront-dialog-head">'.
phutil_escape_html($this->title).
'</div>'.
'<div class="aphront-dialog-body">'.
$this->renderChildren().
'</div>'.
'<div class="aphront-dialog-tail">'.
implode('', $buttons).
'<div style="clear: both;"></div>'.
'</div>');
}
}
diff --git a/src/view/dialog/__init__.php b/src/view/dialog/__init__.php
index 8524dfbb6f..8ddedcb0bb 100644
--- a/src/view/dialog/__init__.php
+++ b/src/view/dialog/__init__.php
@@ -1,15 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'infrastructure/celerity/api');
+phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/base');
phutil_require_module('phutil', 'markup');
phutil_require_source('AphrontDialogView.php');
diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php
index 26a15aeeba..8c7a214603 100755
--- a/src/view/page/standard/PhabricatorStandardPageView.php
+++ b/src/view/page/standard/PhabricatorStandardPageView.php
@@ -1,171 +1,172 @@
<?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 PhabricatorStandardPageView extends AphrontPageView {
private $baseURI;
private $applicationName;
private $tabs = array();
private $selectedTab;
private $glyph;
private $bodyContent;
private $request;
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setTabs(array $tabs, $selected_tab) {
$this->tabs = $tabs;
$this->selectedTab = $selected_tab;
return $this;
}
public function getTitle() {
return $this->getGlyph().' '.parent::getTitle();
}
protected function willRenderPage() {
require_celerity_resource('phabricator-core-css');
require_celerity_resource('phabricator-core-buttons-css');
require_celerity_resource('phabricator-standard-page-view');
require_celerity_resource('javelin-lib-dev');
+ require_celerity_resource('javelin-workflow-dev');
$this->bodyContent = $this->renderChildren();
}
protected function getHead() {
$response = CelerityAPI::getStaticResourceResponse();
return
$response->renderResourcesOfType('css').
'<script type="text/javascript">window.__DEV__=1;</script>'.
'<script type="text/javascript" src="/rsrc/js/javelin/init.dev.js">'.
'</script>';
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
protected function getBody() {
$tabs = array();
foreach ($this->tabs as $name => $tab) {
$tabs[] = phutil_render_tag(
'a',
array(
'href' => idx($tab, 'href'),
'class' => ($name == $this->selectedTab)
? 'phabricator-selected-tab'
: null,
),
phutil_escape_html(idx($tab, 'name')));
}
$tabs = implode('', $tabs);
if ($tabs) {
$tabs = '<span class="phabricator-head-tabs">'.$tabs.'</span>';
}
$login_stuff = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user->getPHID()) {
$login_stuff =
'Logged in as '.phutil_escape_html($user->getUsername()).
' &middot; '.
'<form action="/logout/" method="post" style="display: inline;">'.
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => '__csrf__',
'value' => $user->getCSRFToken(),
)).
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => '__form__',
'value' => true,
)).
'<button class="small grey">Logout</button>'.
'</form>';
}
}
return
'<div class="phabricator-standard-page">'.
'<div class="phabricator-standard-header">'.
'<div class="phabricator-login-details">'.
$login_stuff.
'</div>'.
'<a href="/">Phabricator</a> '.
phutil_render_tag(
'a',
array(
'href' => $this->getBaseURI(),
'class' => 'phabricator-head-appname',
),
phutil_escape_html($this->getApplicationName())).
$tabs.
'</div>'.
$this->bodyContent.
'<div style="clear: both;"></div>'.
'</div>';
}
protected function getTail() {
$response = CelerityAPI::getStaticResourceResponse();
return
$response->renderResourcesOfType('js').
$response->renderHTMLFooter();
}
}
diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css
index bc80a7961f..6c47fc9c69 100644
--- a/webroot/rsrc/css/aphront/dialog-view.css
+++ b/webroot/rsrc/css/aphront/dialog-view.css
@@ -1,42 +1,60 @@
/**
* @provides aphront-dialog-view-css
*/
-
-
.aphront-dialog-view {
width: 480px;
padding: 8px;
background: #666;
margin: auto;
}
.aphront-dialog-head {
background: #003366;
border: none;
font-size: 15px;
padding: 5px 12px 6px;
color: #ffffff;
}
.aphront-dialog-body {
background: #ffffff;
padding: 16px 12px;
border: none;
overflow: hidden;
}
.aphront-dialog-tail {
border: none;
background: #ededed;
padding: 0.5em;
text-align: right;
}
.aphront-dialog-tail button,
.aphront-dialog-tail a.button {
float: right;
margin-left: .5em;
}
+.jx-client-dialog {
+ position: absolute;
+ z-index: 6;
+}
+
+.jx-mask {
+ opacity: .5;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)";
+ filter: alpha(opacity=75);
+ background: #999;
+ position: absolute;
+ z-index: 5;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+}
diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css
index 3ad2468c3f..7395e5a669 100644
--- a/webroot/rsrc/css/application/differential/changeset-view.css
+++ b/webroot/rsrc/css/application/differential/changeset-view.css
@@ -1,121 +1,132 @@
/**
* @provides differential-changeset-view-css
*/
.differential-diff {
background: #ffffff;
font-family: "Menlo", "Consolas", "Monaco", monospace;
font-size: 10px;
width: 100%;
}
.differential-diff td {
/* using monospace fonts makes ex/em most useful:
*
* Unfortunately, firefox 3.6 renders diffs columns for added and removed
* files "way-too-wide" when given em as the dimension measurement, so we
* use an eyeballed ex equivalent and reset it to the ch character width
* measurement for browsers that support that css3 measurement.
*/
width: 88ex;
width: 81ch;
/*
Disable ligatures in Firefox. Firefox 3 has fancypants ligature support, but
it gets applied to monospaced fonts, which sucks because it means that the
"fi" ligature only takes up one character, e.g. It's probably the font's
fault that it even specifies ligatures (seriously, what the hell?) but
that's hard to fix and this is "easy" to "fix": custom letter spacing
disables ligatures, as long as it's at least 0.008333-repeating pixels of
custom letter spacing. I have no idea where this number comes from, but note
that .83333.. = 5/6. -epriestley
*/
letter-spacing: 0.0083334px;
vertical-align: top;
white-space: pre;
padding: 0 8px 1px;
line-height: 16px;
overflow: hidden;
}
.differential-diff th {
text-align: right;
padding: 2px 6px;
width: 44px;
vertical-align: top;
background: #eeeeee;
color: #888888;
cursor: pointer;
border-style: solid;
border-width: 0px 1px;
border-color: #eeeeee #999999 #eeeeee #dddddd;
font-weight: bold;
font-family: "Verdana";
font-size: 11px;
overflow: hidden;
}
.differential-diff td.old {
background: #ffd0d0;
color: #161111;
}
.differential-diff td.new {
background: #d0ffd0;
color: #111611;
}
.differential-diff td.old-full,
.differential-diff td.old span.bright {
background: #ffaaaa;
color: #221111;
}
.differential-diff td.new-full,
.differential-diff td.new span.bright {
background: #aaffaa;
color: #112211;
}
.differential-diff td.show-more,
.differential-diff td.differential-shield {
background: #ffffee;
padding: 1em;
text-align: center;
font-family: "Verdana";
font-size: 11px;
border: 1px solid #ccccaa;
white-space: normal;
}
.differential-diff td.show-more {
color: #999966;
}
.differential-diff td.differential-shield {
text-align: center;
max-width: 1160px;
}
.differential-diff td.differential-shield a {
font-weight: bold;
}
.differential-meta-notice {
border: 1px solid #ffdd99;
background: #ffeeaa;
font-family: "Verdana";
font-size: 11px;
padding: 1em;
margin: 0 0 6px 0;
}
.differential-changeset h1 {
font-size: 14px;
font-weight: bold;
padding: 2px 0 8px;
}
.differential-changeset {
margin: 0.5em 0;
padding: 10px 0px 20px;
}
+
+.differential-reticle {
+ background: #ffeeaa;
+ border: 1px solid #ffcc00;
+ position: absolute;
+ z-index: 2;
+ opacity: 0.5;
+ top: 0px;
+ left: 0px;
+}
+
diff --git a/webroot/rsrc/css/core/dialog.css b/webroot/rsrc/css/core/dialog.css
new file mode 100644
index 0000000000..ece9beaf74
--- /dev/null
+++ b/webroot/rsrc/css/core/dialog.css
@@ -0,0 +1,55 @@
+/**
+ * @provides phabricator-core-dialog-css
+ */
+
+.jx-dialog {
+ display: block;
+ width: 480px;
+ padding: 8px;
+ background: #666;
+ margin: auto;
+}
+
+.jx-client-dialog {
+ position: absolute;
+ z-index: 6;
+}
+
+.jx-dialog .dialog-title {
+ background: #6d84b4;
+ border: none;
+ font-size: 15px;
+ font-weight: bold;
+ padding: 5px 12px 6px;
+ color: #ffffff;
+}
+.jx-dialog .dialog-body {
+ background: #ffffff;
+ padding: 16px 12px;
+ border: none;
+ overflow: hidden;
+}
+
+.jx-dialog .dialog-foot {
+ border: none;
+ background: #ededed;
+ padding: .5em;
+ text-align: right;
+}
+
+.jx-dialog button {
+ margin-left: 6px;
+}
+
+.jx-dialog input {
+ padding: 4px;
+}
+
+.jx-dialog .fields {
+ margin-top: 10px;
+}
+
+.jx-dialog input.block {
+ display: block;
+ margin: 3px 0 0 0;
+}
diff --git a/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
new file mode 100644
index 0000000000..a63526bb71
--- /dev/null
+++ b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
@@ -0,0 +1,229 @@
+/**
+ * @provides javelin-behavior-differential-edit-inline-comments
+ * @requires javelin-lib-dev
+ */
+
+JX.behavior('differential-edit-inline-comments', function(config) {
+
+ var selecting = false;
+ var reticle = JX.$N('div', {className: 'differential-reticle'});
+ JX.DOM.hide(reticle);
+ document.body.appendChild(reticle);
+
+ var origin = null;
+ var target = null;
+ var root = null;
+ var changeset = null;
+ var workflow = false;
+ var is_new = false;
+
+ function updateReticle() {
+ var top = origin;
+ var bot = target;
+ if (JX.$V(top).y > JX.$V(bot).y) {
+ var tmp = top;
+ top = bot;
+ bot = tmp;
+ }
+
+ var code = target.nextSibling;
+
+ var pos = JX.$V(top).add(1 + JX.$V.getDim(target).x, 0);
+ var dim = JX.$V.getDim(code).add(-4, 0);
+ dim.y = (JX.$V(bot).y - pos.y) + JX.$V.getDim(bot).y;
+
+ pos.setPos(reticle);
+ dim.setDim(reticle);
+
+ JX.DOM.show(reticle);
+ }
+
+ function hideReticle() {
+ JX.DOM.hide(reticle);
+ }
+
+ function finishSelect() {
+ selecting = false;
+ workflow = false;
+ hideReticle();
+ }
+
+ function drawInlineComment(table, anchor, r) {
+ copyRows(table, JX.$N('div', JX.HTML(r.markup)), anchor);
+ finishSelect();
+ }
+
+ function isNewFile(node) {
+ return node.parentNode.firstChild != node;
+ }
+
+ function getRowNumber(th_node) {
+ try {
+ return parseInt(th_node.id.match(/^C\d+[ON]L(\d+)$/)[1], 10);
+ } catch (x) {
+ return undefined;
+ }
+ }
+
+ JX.Stratcom.listen(
+ 'mousedown',
+ ['differential-changeset', 'tag:th'],
+ function(e) {
+ if (workflow ||
+ selecting ||
+ getRowNumber(e.getTarget()) === undefined) {
+ return;
+ }
+
+ selecting = true;
+ root = e.getNode('differential-changeset');
+
+ origin = target = e.getTarget();
+
+ var data = e.getNodeData('differential-changeset');
+ if (isNewFile(target)) {
+ changeset = data.oid;
+ } else {
+ changeset = data.nid;
+ }
+
+ updateReticle();
+
+ e.kill();
+ });
+
+ JX.Stratcom.listen(
+ 'mouseover',
+ ['differential-changeset', 'tag:th'],
+ function(e) {
+ if (!selecting ||
+ workflow ||
+ (getRowNumber(e.getTarget()) === undefined) ||
+ (isNewFile(e.getTarget()) != isNewFile(origin)) ||
+ (e.getNode('differential-changeset') !== root)) {
+ return;
+ }
+
+ target = e.getTarget();
+
+ updateReticle();
+ });
+
+ JX.Stratcom.listen(
+ 'mouseup',
+ null,
+ function(e) {
+ if (workflow || !selecting) {
+ return;
+ }
+
+ var o = getRowNumber(origin);
+ var t = getRowNumber(target);
+
+ var insert;
+ var len;
+ if (t < o) {
+ len = (o - t);
+ o = t;
+ insert = origin.parentNode;
+ } else {
+ len = (t - o);
+ insert = target.parentNode;
+ }
+
+ var data = {
+ op: 'new',
+ changeset: changeset,
+ number: o,
+ length: len,
+ is_new: isNewFile(target) ? 1 : 0
+ };
+
+ workflow = true;
+
+ var w = new JX.Workflow(config.uri, data)
+ .setHandler(function(r) {
+ // Skip over any rows which contain inline feedback. Don't mimic this!
+ // We're shipping around raw HTML here for performance reasons, but
+ // normally you should use sigils to encode this kind of data on
+ // the document.
+ var target = insert.nextSibling;
+ while (target &&
+ (!JX.DOM.isType(target, 'tr')
+ || target.className.indexOf('inline') !== -1)) {
+ target = target.nextSibling;
+ }
+ drawInlineComment(insert.parentNode, target, r);
+ finishSelect();
+ JX.Stratcom.invoke('inline-comment-update',
+ null,
+ {id : r.inlineCommentID});
+ })
+ .setCloseHandler(finishSelect);
+
+
+ w.listen('error', function(e) {
+ // TODO: uh, tell the user I guess
+ finishSelect();
+ JX.Stratcom.context().stop();
+ });
+
+ w.start();
+
+ e.kill();
+ });
+
+ JX.Stratcom.listen(
+ ['mouseover', 'mouseout'],
+ 'inline-comment',
+ function(e) {
+ if (selecting || workflow) {
+ return;
+ }
+
+ if (e.getType() == 'mouseout') {
+ hideReticle();
+ } else {
+ var data = e.getNodeData('inline-comment');
+ var change = e.getNodeData('differential-changeset');
+
+ root = e.getNode('differential-changeset');
+
+ var prefix = 'C' + change;
+
+ if (data.is_new) {
+ prefix += 'NL';
+ } else {
+ prefix += 'OL';
+ }
+
+ origin = JX.$(prefix + data.number);
+ target = JX.$(prefix + (data.number + data.length));
+
+ updateReticle();
+ }
+ });
+
+ JX.Stratcom.listen(
+ 'click',
+ [['inline-comment', 'delete'],
+ ['inline-comment', 'edit']],
+ function(e) {
+ var data = {
+ op: e.getNode('edit') ? 'edit' : 'delete',
+ id: e.getNodeData('inline-comment').id
+ };
+ new JX.Workflow(config.uri, data)
+ .setHandler(function(r) {
+ var base_row = e.getNode('inline-comment').parentNode.parentNode;
+ if (data.op == 'edit' && r.markup) {
+ drawInlineComment(base_row.parentNode, base_row, r);
+ }
+ JX.DOM.remove(base_row);
+ JX.Stratcom.invoke('differential-inline-comment-update');
+ })
+ .start();
+ e.kill();
+ });
+
+});
diff --git a/webroot/rsrc/js/javelin/init.dev.js b/webroot/rsrc/js/javelin/init.dev.js
index eadfd68a8e..402f8e3b5f 100644
--- a/webroot/rsrc/js/javelin/init.dev.js
+++ b/webroot/rsrc/js/javelin/init.dev.js
@@ -1,180 +1,179 @@
-/** @provides javelin-init-dev */
/**
* Javelin core; installs Javelin and Stratcom event delegation.
*
* @provides javelin-magical-init
* @nopackage
*
* @javelin-installs JX.__rawEventQueue
* @javelin-installs JX.__simulate
* @javelin-installs JX.enableDispatch
* @javelin-installs JX.onload
*
* @javelin
*/
(function() {
if (window.JX) {
return;
}
window.JX = {};
window['__DEV__'] = window['__DEV__'] || 0;
var loaded = false;
var onload = [];
var master_event_queue = [];
var root = document.documentElement;
var has_add_event_listener = !!root.addEventListener;
JX.__rawEventQueue = function(what) {
master_event_queue.push(what);
-
// Evade static analysis - JX.Stratcom
var Stratcom = JX['Stratcom'];
if (Stratcom && Stratcom.ready) {
// Empty the queue now so that exceptions don't cause us to repeatedly
// try to handle events.
var local_queue = master_event_queue;
master_event_queue = [];
for (var ii = 0; ii < local_queue.length; ++ii) {
var evt = local_queue[ii];
// Sometimes IE gives us events which throw when ".type" is accessed;
// just ignore them since we can't meaningfully dispatch them. TODO:
// figure out where these are coming from.
try { var test = evt.type; } catch (x) { continue; }
if (!loaded && evt.type == 'domready') {
document.body && (document.body.id = null);
loaded = true;
for (var ii = 0; ii < onload.length; ii++) {
onload[ii]();
}
}
Stratcom.dispatch(evt);
}
} else {
var target = what.srcElement || what.target;
if (target &&
(what.type in {click: 1, submit: 1}) &&
- (/ FI_CAPTURE /).test(' ' + target.className + ' ')) {
+ target.getAttribute &&
+ target.getAttribute('data-mustcapture') === '1') {
what.returnValue = false;
what.preventDefault && what.preventDefault();
document.body.id = 'event_capture';
// For versions of IE that use attachEvent, the event object is somehow
// stored globally by reference, and all the references we push to the
// master_event_queue will always refer to the most recent event. We
// work around this by popping the useless global event off the queue,
// and pushing a clone of the event that was just fired using the IE's
// proprietary createEventObject function.
// see: http://msdn.microsoft.com/en-us/library/ms536390(v=vs.85).aspx
if (!add_event_listener && document.createEventObject) {
master_event_queue.pop();
master_event_queue.push(document.createEventObject(what));
}
return false;
}
}
}
JX.enableDispatch = function(target, type) {
if (target.addEventListener) {
target.addEventListener(type, JX.__rawEventQueue, true);
} else if (target.attachEvent) {
target.attachEvent('on' + type, JX.__rawEventQueue);
}
};
var document_events = [
'click',
'change',
'keypress',
'mousedown',
'mouseover',
'mouseout',
'mouseup',
'keydown',
'drop',
'dragenter',
'dragleave',
'dragover'
];
// Simulate focus and blur in old versions of IE using focusin and focusout
// TODO: Document the gigantic IE mess here with focus/blur.
// TODO: beforeactivate/beforedeactivate?
// http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
if (!has_add_event_listener) {
document_events.push('focusin', 'focusout');
}
// Opera is multilol: it propagates focus / blur odd, and submit differently
if (window.opera) {
document_events.push('focus', 'blur');
} else {
document_events.push('submit');
}
for (var ii = 0; ii < document_events.length; ++ii) {
JX.enableDispatch(root, document_events[ii]);
}
// In particular, we're interested in capturing window focus/blur here so
// long polls can abort when the window is not focused.
var window_events = [
('onpagehide' in window) ? 'pagehide' : 'unload',
'resize',
'focus',
'blur'
];
for (var ii = 0; ii < window_events.length; ++ii) {
JX.enableDispatch(window, window_events[ii]);
}
JX.__simulate = function(node, event) {
if (!has_add_event_listener) {
var e = {target: node, type: event};
JX.__rawEventQueue(e);
if (e.returnValue === false) {
return false;
}
}
};
if (has_add_event_listener) {
document.addEventListener('DOMContentLoaded', function() {
JX.__rawEventQueue({type: 'domready'});
}, true);
} else {
var ready =
"if (this.readyState == 'complete') {" +
"JX.__rawEventQueue({type: 'domready'});" +
"}";
document.write(
'<script' +
' defer="defer"' +
' src="javascript:void(0)"' +
' onreadystatechange="' + ready + '"' +
'><\/sc' + 'ript\>');
}
JX.onload = function(func) {
if (loaded) {
func();
} else {
onload.push(func);
}
}
})();
diff --git a/webroot/rsrc/js/javelin/init.min.js b/webroot/rsrc/js/javelin/init.min.js
index 264f75659c..0ba48fc10f 100644
--- a/webroot/rsrc/js/javelin/init.min.js
+++ b/webroot/rsrc/js/javelin/init.min.js
@@ -1,2 +1,2 @@
/** @provides javelin-init-prod */
-(function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<m.length;++l){var k=m[l];try{var test=k.type;}catch(p){continue;}if(!d&&k.type=='domready'){document.body&&(document.body.id=null);d=true;for(var l=0;l<f.length;l++)f[l]();}j.dispatch(k);}}else{var n=o.srcElement||o.target;if(n&&(o.type in {click:1,submit:1})&&(/ FI_CAPTURE /).test(' '+n.className+' ')){o.returnValue=false;o.preventDefault&&o.preventDefault();document.body.id='event_capture';if(!add_event_listener&&document.createEventObject){e.pop();e.push(document.createEventObject(o));}return false;}}};JX.enableDispatch=function(j,k){if(j.addEventListener){j.addEventListener(k,JX.__rawEventQueue,true);}else if(j.attachEvent)j.attachEvent('on'+k,JX.__rawEventQueue);};var a=['click','change','keypress','mousedown','mouseover','mouseout','mouseup','keydown','drop','dragenter','dragleave','dragover'];if(!b)a.push('focusin','focusout');if(window.opera){a.push('focus','blur');}else a.push('submit');for(var c=0;c<a.length;++c)JX.enableDispatch(h,a[c]);var i=[('onpagehide' in window)?'pagehide':'unload','resize','focus','blur'];for(var c=0;c<i.length;++c)JX.enableDispatch(window,i[c]);JX.__simulate=function(k,event){if(!b){var j={target:k,type:event};JX.__rawEventQueue(j);if(j.returnValue===false)return false;}};if(b){document.addEventListener('DOMContentLoaded',function(){JX.__rawEventQueue({type:'domready'});},true);}else{var g="if (this.readyState == 'complete') {"+"JX.__rawEventQueue({type: 'domready'});"+"}";document.write('<script'+' defer="defer"'+' src="javascript:void(0)"'+' onreadystatechange="'+g+'"'+'><\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})();
\ No newline at end of file
+(function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<m.length;++l){var k=m[l];try{var test=k.type;}catch(p){continue;}if(!d&&k.type=='domready'){document.body&&(document.body.id=null);d=true;for(var l=0;l<f.length;l++)f[l]();}j.dispatch(k);}}else{var n=o.srcElement||o.target;if(n&&(o.type in {click:1,submit:1})&&n.getAttribute&&n.getAttribute('data-mustcapture')==='1'){o.returnValue=false;o.preventDefault&&o.preventDefault();document.body.id='event_capture';if(!add_event_listener&&document.createEventObject){e.pop();e.push(document.createEventObject(o));}return false;}}};JX.enableDispatch=function(j,k){if(j.addEventListener){j.addEventListener(k,JX.__rawEventQueue,true);}else if(j.attachEvent)j.attachEvent('on'+k,JX.__rawEventQueue);};var a=['click','change','keypress','mousedown','mouseover','mouseout','mouseup','keydown','drop','dragenter','dragleave','dragover'];if(!b)a.push('focusin','focusout');if(window.opera){a.push('focus','blur');}else a.push('submit');for(var c=0;c<a.length;++c)JX.enableDispatch(h,a[c]);var i=[('onpagehide' in window)?'pagehide':'unload','resize','focus','blur'];for(var c=0;c<i.length;++c)JX.enableDispatch(window,i[c]);JX.__simulate=function(k,event){if(!b){var j={target:k,type:event};JX.__rawEventQueue(j);if(j.returnValue===false)return false;}};if(b){document.addEventListener('DOMContentLoaded',function(){JX.__rawEventQueue({type:'domready'});},true);}else{var g="if (this.readyState == 'complete') {"+"JX.__rawEventQueue({type: 'domready'});"+"}";document.write('<script'+' defer="defer"'+' src="javascript:void(0)"'+' onreadystatechange="'+g+'"'+'><\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})();
diff --git a/webroot/rsrc/js/javelin/javelin.dev.js b/webroot/rsrc/js/javelin/javelin.dev.js
index 7f877a124f..7fc3cfbe28 100644
--- a/webroot/rsrc/js/javelin/javelin.dev.js
+++ b/webroot/rsrc/js/javelin/javelin.dev.js
@@ -1,2936 +1,2949 @@
/** @provides javelin-lib-dev */
/**
* Javelin utility functions.
*
* @provides javelin-util
*
* @javelin-installs JX.$A
* @javelin-installs JX.$AX
* @javelin-installs JX.copy
* @javelin-installs JX.bind
* @javelin-installs JX.bag
* @javelin-installs JX.keys
* @javelin-installs JX.defer
* @javelin-installs JX.go
* @javelin-installs JX.log
*
* @javelin
*/
/**
* Convert an array-like object (usually ##arguments##) into a real Array. An
* "array-like object" is something with a ##length## property and numerical
* keys. The most common use for this is to let you call Array functions on the
* magical ##arguments## object.
*
* JX.$A(arguments).slice(1);
*
* @param obj Array, or array-like object.
* @return Array Actual array.
*/
JX.$A = function(mysterious_arraylike_object) {
// NOTE: This avoids the Array.slice() trick because some bizarre COM object
// I dug up somewhere was freaking out when I tried to do it and it made me
// very upset, so do not replace this with Array.slice() cleverness.
var r = [];
for (var ii = 0; ii < mysterious_arraylike_object.length; ii++) {
r.push(mysterious_arraylike_object[ii]);
}
return r;
};
/**
* Cast a value into an array, by wrapping scalars into singletons. If the
* argument is an array, it is returned unmodified. If it is a scalar, an array
* with a single element is returned. For example:
*
* JX.$AX([3]); // Returns [3].
* JX.$AX(3); // Returns [3].
*
* Note that this function uses an "instanceof Array" check so you may need to
* convert array-like objects (such as ##arguments## and Array instances from
* iframes) into real arrays with @{JX.$A()}.
*
* @param wild Scalar or Array.
* @return Array If the argument was a scalar, an Array with the argument as
* its only element. Otherwise, the original Array.
*
*/
JX.$AX = function(maybe_scalar) {
return (maybe_scalar instanceof Array) ? maybe_scalar : [maybe_scalar];
};
/**
* Copy properties from one object to another. Note: does not copy the
* ##toString## property or anything else which isn't enumerable or is somehow
* magic or just doesn't work. But it's usually what you want. If properties
* already exist, they are overwritten.
*
* var cat = {
* ears: 'clean',
* paws: 'clean',
* nose: 'DIRTY OH NOES'
* };
* var more = {
* nose: 'clean',
* tail: 'clean'
* };
*
* JX.copy(cat, more);
*
* // cat is now:
* // {
* // ears: 'clean',
* // paws: 'clean',
* // nose: 'clean',
* // tail: 'clean'
* // }
*
* @param obj Destination object, which properties should be copied to.
* @param obj Source object, which properties should be copied from.
* @return obj Destination object.
*/
JX.copy = function(copy_dst, copy_src) {
for (var k in copy_src) {
copy_dst[k] = copy_src[k];
}
return copy_dst;
};
/**
* Create a function which invokes another function with a bound context and
* arguments (i.e., partial function application) when called; king of all
* functions.
*
* Bind performs context binding (letting you select what the value of ##this##
* will be when a function is invoked) and partial function application (letting
* you create some function which calls another one with bound arguments).
*
* = Context Binding =
*
* Normally, when you call ##obj.method()##, the magic ##this## object will be
* the ##obj## you invoked the method from. This can be undesirable when you
* need to pass a callback to another function. For instance:
*
* COUNTEREXAMPLE
* var dog = new JX.Dog();
* dog.barkNow(); // Makes the dog bark.
*
* JX.Stratcom.listen('click', 'bark', dog.barkNow); // Does not work!
*
* This doesn't work because ##this## is ##window## when the function is
* later invoked; @{JX.Stratcom.listen()} does not know about the context
* object ##dog##. The solution is to pass a function with a bound context
* object:
*
* var dog = new JX.Dog();
* var bound_function = JX.bind(dog, dog.barkNow);
*
* JX.Stratcom.listen('click', 'bark', bound_function);
*
* ##bound_function## is a function with ##dog## bound as ##this##; ##this##
* will always be ##dog## when the function is called, no matter what
* property chain it is invoked from.
*
* You can also pass ##null## as the context argument to implicitly bind
* ##window##.
*
* = Partial Function Application =
*
* @{JX.bind()} also performs partial function application, which allows you
* to bind one or more arguments to a function. For instance, if we have a
* simple function which adds two numbers:
*
* function add(a, b) { return a + b; }
* add(3, 4); // 7
*
* Suppose we want a new function, like this:
*
* function add3(b) { return 3 + b; }
* add3(4); // 7
*
* Instead of doing this, we can define ##add3()## in terms of ##add()## by
* binding the value ##3## to the ##a## argument:
*
* var add3_bound = JX.bind(null, add, 3);
* add3_bound(4); // 7
*
* Zero or more arguments may be bound in this way. This is particularly useful
* when using closures in a loop:
*
* COUNTEREXAMPLE
* for (var ii = 0; ii < button_list.length; ii++) {
* button_list[ii].onclick = function() {
* JX.log('You clicked button number '+ii+'!'); // Fails!
* };
* }
*
* This doesn't work; all the buttons report the highest number when clicked.
* This is because the local ##ii## is captured by the closure. Instead, bind
* the current value of ##ii##:
*
* var func = function(button_num) {
* JX.log('You clicked button number '+button_num+'!');
* }
* for (var ii = 0; ii < button_list.length; ii++) {
* button_list[ii].onclick = JX.bind(null, func, ii);
* }
*
* @param obj|null Context object to bind as ##this##.
* @param function Function to bind context and arguments to.
* @param ... Zero or more arguments to bind.
* @return function New function which invokes the original function with
* bound context and arguments when called.
*/
JX.bind = function(context, func, more) {
if (__DEV__) {
if (typeof func != 'function') {
throw new Error(
'JX.bind(context, <yuck>, ...): '+
'Attempting to bind something that is not a function.');
}
}
var bound = JX.$A(arguments).slice(2);
return function() {
return func.apply(context || window, bound.concat(JX.$A(arguments)));
}
};
/**
* "Bag of holding"; function that does nothing. Primarily, it's used as a
* placeholder when you want something to be callable but don't want it to
* actually have an effect.
*
* @return void
*/
JX.bag = function() {
// \o\ \o/ /o/ woo dance party
};
/**
* Convert an object's keys into a list. For example:
*
* JX.keys({sun: 1, moon: 1, stars: 1}); // Returns: ['sun', 'moon', 'stars']
*
* @param obj Object to retrieve keys from.
* @return list List of keys.
*/
JX.keys = function(obj) {
var r = [];
for (var k in obj) {
r.push(k);
}
return r;
};
/**
* Defer a function for later execution, similar to ##setTimeout()##. Returns
* an object with a ##stop()## method, which cancels the deferred call.
*
* var ref = JX.defer(yell, 3000); // Yell in 3 seconds.
* // ...
* ref.stop(); // Cancel the yell.
*
* @param function Function to invoke after the timeout.
* @param int? Timeout, in milliseconds. If this value is omitted, the
* function will be invoked once control returns to the browser
* event loop, as with ##setTimeout(func, 0)##.
* @return obj An object with a ##stop()## method, which cancels function
* execution.
*/
JX.defer = function(func, timeout) {
var t = setTimeout(func, timeout || 0);
return {stop : function() { clearTimeout(t); }}
};
/**
* Redirect the browser to another page by changing the window location.
*
* @param string Optional URI to redirect the browser to. If no URI is
* provided, the current page will be reloaded.
* @return void
*/
JX.go = function(uri) {
// Foil static analysis, etc. Strictly speaking, JX.go() doesn't really need
// to be in javelin-utils so we could do this properly at some point.
JX['Stratcom'] && JX['Stratcom'].invoke('go', null, {uri: uri});
(uri && (window.location = uri)) || window.location.reload(true);
};
if (__DEV__) {
if (!window.console || !window.console.log) {
if (window.opera && window.opera.postError) {
window.console = {log: function(m) { window.opera.postError(m); }};
} else {
window.console = {log: function(m) { }};
}
}
/**
* Print a message to the browser debugging console (like Firebug). This
* method exists only in ##__DEV__##.
*
* @param string Message to print to the browser debugging console.
* @return void
*/
JX.log = function(message) {
window.console.log(message);
}
window.alert = (function(native_alert) {
var recent_alerts = [];
var in_alert = false;
return function(msg) {
if (in_alert) {
JX.log(
'alert(...): '+
'discarded reentrant alert.');
return;
}
in_alert = true;
recent_alerts.push(new Date().getTime());
if (recent_alerts.length > 3) {
recent_alerts.splice(0, recent_alerts.length - 3);
}
if (recent_alerts.length >= 3 &&
(recent_alerts[recent_alerts.length - 1] - recent_alerts[0]) < 5000) {
if (confirm(msg + "\n\nLots of alert()s recently. Kill them?")) {
window.alert = JX.bag;
}
} else {
// Note that we can't .apply() the IE6 version of this "function".
native_alert(msg);
}
in_alert = false;
}
})(window.alert);
}
/**
* @requires javelin-util
* @provides javelin-install
* @javelin-installs JX.install
* @javelin
*/
/**
* Install a class into the Javelin ("JX") namespace. The first argument is the
* name of the class you want to install, and the second is a map of these
* attributes (all of which are optional):
*
* - ##construct## //(function)// Class constructor. If you don't provide one,
* one will be created for you (but it will be very boring).
* - ##extend## //(string)// The name of another JX-namespaced class to extend
* via prototypal inheritance.
* - ##members## //(map)// A map of instance methods and properties.
* - ##statics## //(map)// A map of static methods and properties.
* - ##initialize## //(function)// A function which will be run once, after
* this class has been installed.
* - ##properties## //(map)// A map of properties that should have instance
* getters and setters automatically generated for them. The key is the
* property name and the value is its default value. For instance, if you
* provide the property "size", the installed class will have the methods
* "getSize()" and "setSize()". It will **NOT** have a property ".size"
* and no guarantees are made about where install is actually chosing to
* store the data. The motivation here is to let you cheaply define a
* stable interface and refine it later as necessary.
* - ##events## //(list)// List of event types this class is capable of
* emitting.
*
* For example:
*
* JX.install('Dog', {
* construct : function(name) {
* this.setName(name);
* },
* members : {
* bark : function() {
* // ...
* }
* },
* properites : {
* name : null,
* }
* });
*
* This creates a new ##Dog## class in the ##JX## namespace:
*
* var d = new JX.Dog();
* d.bark();
*
* Javelin classes are normal Javascript functions and generally behave in
* the expected way. Some properties and methods are automatically added to
* all classes:
*
* - ##instance.__id__## Globally unique identifier attached to each instance.
* - ##instance.__super__## Reference to the parent class constructor, if one
* exists. Allows use of ##this.__super__.apply(this, ...)## to call the
* superclass's constructor.
* - ##instance.__parent__## Reference to the parent class prototype, if one
* exists. Allows use of ##this.__parent__.someMethod.apply(this, ...)##
* to call the superclass's methods.
* - ##prototype.__class__## Reference to the class constructor.
* - ##constructor.__path__## List of path tokens used emit events. It is
* probably never useful to access this directly.
* - ##constructor.__readable__## //DEV ONLY!// Readable class name. You could
* plausibly use this when constructing error messages.
* - ##constructor.__events__## //DEV ONLY!// List of events supported by
* this class.
* - ##constructor.listen()## Listen to all instances of this class. See
* @{JX.Base}.
* - ##instance.listen()## Listen to one instance of this class. See
* @{JX.Base}.
* - ##instance.invoke()## Invoke an event from an instance. See @{JX.Base}.
*
*
* @param string Name of the class to install. It will appear in the JX
* "namespace" (e.g., JX.Pancake).
* @param map Map of properties, see method documentation.
* @return void
*
* @author epriestley
*/
JX.install = function(new_name, new_junk) {
if (typeof JX.install._nextObjectID == 'undefined') {
JX.install._nextObjectID = 0;
}
// If we've already installed this, something is up.
if (new_name in JX) {
if (__DEV__) {
throw new Error(
'JX.install("' + new_name + '", ...): ' +
'trying to reinstall something that has already been installed.');
}
return;
}
// Since we may end up loading things out of order (e.g., Dog extends Animal
// but we load Dog first) we need to keep a list of things that we've been
// asked to install but haven't yet been able to install around.
if (!JX.install._queue) {
JX.install._queue = [];
}
JX.install._queue.push([new_name, new_junk]);
do {
var junk;
var name = null;
for (var ii = 0; ii < JX.install._queue.length; ++ii) {
junk = JX.install._queue[ii][1];
if (junk.extend && !JX[junk.extend]) {
// We need to extend something that we haven't been able to install
// yet, so just keep this in queue.
continue;
}
// Install time! First, get this out of the queue.
name = JX.install._queue[ii][0];
JX.install._queue.splice(ii, 1);
--ii;
if (__DEV__) {
var valid = {
construct : 1,
statics : 1,
members : 1,
extend : 1,
initialize: 1,
properties : 1,
events : 1,
canCallAsFunction : 1
};
for (var k in junk) {
if (!(k in valid)) {
throw new Error(
'JX.install("' + name + '", {"' + k + '": ...}): ' +
'trying to install unknown property `' + k + '`.');
}
}
if (junk.constructor !== {}.constructor) {
throw new Error(
'JX.install("' + name + '", {"constructor": ...}): ' +
'property `constructor` should be called `construct`.');
}
}
// First, build the constructor. If construct is just a function, this
// won't change its behavior (unless you have provided a really awesome
// function, in which case it will correctly punish you for your attempt
// at creativity).
JX[name] = (function(name, junk) {
var result = function() {
this.__id__ = '__obj__' + (++JX.install._nextObjectID);
this.__super__ = JX[junk.extend] || JX.bag;
this.__parent__ = JX[name].prototype;
if (JX[name].__prototyping__) {
return;
}
return (junk.construct || JX.bag).apply(this, arguments);
// TODO: Allow mixins to initialize here?
// TODO: Also, build mixins?
};
if (__DEV__) {
if (!junk.canCallAsFunction) {
var inner = result;
result = function() {
if (this === window || this === JX) {
throw new Error("<" + JX[name].__readable__ + ">: " +
"Tried to construct an instance " +
"without the 'new' operator. Either use " +
"'new' or set 'canCallAsFunction' where you " +
"install the class.");
}
return inner.apply(this, arguments);
};
}
}
return result;
})(name, junk);
// Copy in all the static methods and properties.
JX.copy(JX[name], junk.statics);
if (__DEV__) {
JX[name].__readable__ = 'JX.' + name;
}
JX[name].__prototyping__ = 0;
var proto;
if (junk.extend) {
JX[junk.extend].__prototyping__++;
proto = JX[name].prototype = new JX[junk.extend]();
JX[junk.extend].__prototyping__--;
} else {
proto = JX[name].prototype = {};
}
proto.__class__ = JX[name];
// Build getters and setters from the `prop' map.
for (var k in (junk.properties || {})) {
var base = k.charAt(0).toUpperCase()+k.substr(1);
var prop = '__auto__' + k;
proto[prop] = junk.properties[k];
proto['set' + base] = (function(prop) {
return function(v) {
this[prop] = v;
return this;
}
})(prop);
proto['get' + base] = (function(prop) {
return function() {
return this[prop];
}
})(prop);
}
if (__DEV__) {
// Check for aliasing in default values of members. If we don't do this,
// you can run into a problem like this:
//
// JX.install('List', { members : { stuff : [] }});
//
// var i_love = new JX.List();
// var i_hate = new JX.List();
//
// i_love.stuff.push('Psyduck'); // I love psyduck!
// JX.log(i_hate.stuff); // Show stuff I hate.
//
// This logs ["Psyduck"] because the push operation modifies
// JX.List.prototype.stuff, which is what both i_love.stuff and
// i_hate.stuff resolve to. To avoid this, set the default value to
// null (or any other scalar) and do "this.stuff = [];" in the
// constructor.
for (var member_name in junk.members) {
if (junk.extend && member_name[0] == '_') {
throw new Error(
'JX.install("' + name + '", ...): ' +
'installed member "' + member_name + '" must not be named with ' +
'a leading underscore because it is in a subclass. Variables ' +
'are analyzed and crushed one file at a time, and crushed ' +
'member variables in subclasses alias crushed member variables ' +
'in superclasses. Remove the underscore, refactor the class so ' +
'it does not extend anything, or fix the minifier to be ' +
'capable of safely crushing subclasses.');
}
var member_value = junk.members[member_name];
if (typeof member_value == 'object' && member_value !== null) {
throw new Error(
'JX.install("' + name + '", ...): ' +
'installed member "' + member_name + '" is not a scalar or ' +
'function. Prototypal inheritance in Javascript aliases object ' +
'references across instances so all instances are initialized ' +
'to point at the exact same object. This is almost certainly ' +
'not what you intended. Make this member static to share it ' +
'across instances, or initialize it in the constructor to ' +
'prevent reference aliasing and give each instance its own ' +
'copy of the value.');
}
}
}
// This execution order intentionally allows you to override methods
// generated from the "properties" initializer.
JX.copy(proto, junk.members);
// Build this ridiculous event model thing. Basically, this defines
// two instance methods, invoke() and listen(), and one static method,
// listen(). If you listen to an instance you get events for that
// instance; if you listen to a class you get events for all instances
// of that class (including instances of classes which extend it).
//
// This is rigged up through Stratcom. Each class has a path component
// like "class:Dog", and each object has a path component like
// "obj:23". When you invoke on an object, it emits an event with
// a path that includes its class, all parent classes, and its object
// ID.
//
// Calling listen() on an instance listens for just the object ID.
// Calling listen() on a class listens for that class's name. This
// has the effect of working properly, but installing them is pretty
// messy.
if (junk.events && junk.events.length) {
var parent = JX[junk.extend] || {};
// If we're in dev, we build up a list of valid events (for this
// class or some parent class) and then check them whenever we try
// to listen or invoke.
if (__DEV__) {
var valid_events = parent.__events__ || {};
for (var ii = 0; ii < junk.events.length; ++ii) {
valid_events[junk.events[ii]] = true;
}
JX[name].__events__ = valid_events;
}
// Build the class name chain.
JX[name].__name__ = 'class:' + name;
var ancestry = parent.__path__ || [];
JX[name].__path__ = ancestry.concat([JX[name].__name__]);
proto.invoke = function(type) {
if (__DEV__) {
if (!(type in this.__class__.__events__)) {
throw new Error(
this.__class__.__readable__ + '.invoke("' + type + '", ...): ' +
'invalid event type. Valid event types are: ' +
JX.keys(this.__class__.__events__).join(', ') + '.');
}
}
// Here and below, this nonstandard access notation is used to mask
// these callsites from the static analyzer. JX.Stratcom is always
// available by the time we hit these execution points.
return JX['Stratcom'].invoke(
'obj:' + type,
this.__class__.__path__.concat([this.__id__]),
{args : JX.$A(arguments).slice(1)});
};
proto.listen = function(type, callback) {
if (__DEV__) {
if (!(type in this.__class__.__events__)) {
throw new Error(
this.__class__.__readable__ + '.listen("' + type + '", ...): ' +
'invalid event type. Valid event types are: ' +
JX.keys(this.__class__.__events__).join(', ') + '.');
}
}
return JX['Stratcom'].listen(
'obj:' + type,
this.__id__,
JX.bind(this, function(e) {
return callback.apply(this, e.getData().args);
}));
};
JX[name].listen = function(type, callback) {
if (__DEV__) {
if (!(type in this.__events__)) {
throw new Error(
this.__readable__ + '.listen("' + type + '", ...): ' +
'invalid event type. Valid event types are: ' +
JX.keys(this.__events__).join(', ') + '.');
}
}
return JX['Stratcom'].listen(
'obj:' + type,
this.__name__,
JX.bind(this, function(e) {
return callback.apply(this, e.getData().args);
}));
};
} else if (__DEV__) {
var error_message =
'class does not define any events. Pass an "events" property to ' +
'JX.install() to define events.';
JX[name].listen = JX[name].listen || function() {
throw new Error(
this.__readable__ + '.listen(...): ' +
error_message);
};
JX[name].invoke = JX[name].invoke || function() {
throw new Error(
this.__readable__ + '.invoke(...): ' +
error_message);
};
proto.listen = proto.listen || function() {
throw new Error(
this.__class__.__readable__ + '.listen(...): ' +
error_message);
};
proto.invoke = proto.invoke || function() {
throw new Error(
this.__class__.__readable__ + '.invoke(...): ' +
error_message);
};
}
// Finally, run the init function if it was provided.
(junk.initialize || JX.bag)();
}
// In effect, this exits the loop as soon as we didn't make any progress
// installing things, which means we've installed everything we have the
// dependencies for.
} while (name);
}
/**
* @requires javelin-install
* @provides javelin-event
* @javelin
*/
/**
* A generic event, routed by @{JX.Stratcom}. All events within Javelin are
* represented by a {@JX.Event}, regardless of whether they originate from
* a native DOM event (like a mouse click) or are custom application events.
*
* Events have a propagation model similar to native Javascript events, in that
* they can be stopped with stop() (which stops them from continuing to
* propagate to other handlers) or prevented with prevent() (which prevents them
* from taking their default action, like following a link). You can do both at
* once with kill().
*
* @author epriestley
* @task stop Stopping Event Behaviors
* @task info Getting Event Information
*/
JX.install('Event', {
members : {
/**
* Stop an event from continuing to propagate. No other handler will
* receive this event, but its default behavior will still occur. See
* ""Using Events"" for more information on the distinction between
* 'stopping' and 'preventing' an event. See also prevent() (which prevents
* an event but does not stop it) and kill() (which stops and prevents an
* event).
*
* @return this
* @task stop
*/
stop : function() {
var r = this.getRawEvent();
if (r) {
r.cancelBubble = true;
r.stopPropagation && r.stopPropagation();
}
this.setStopped(true);
return this;
},
/**
* Prevent an event's default action. This depends on the event type, but
* the common default actions are following links, submitting forms,
* and typing text. Event prevention is generally used when you have a link
* or form which work properly without Javascript but have a specialized
* Javascript behavior. When you intercept the event and make the behavior
* occur, you prevent it to keep the browser from following the link.
*
* Preventing an event does not stop it from propagating, so other handlers
* will still receive it. See ""Using Events"" for more information on the
* distinction between 'stopping' and 'preventing' an event. See also
* stop() (which stops an event but does not prevent it) and kill()
* (which stops and prevents an event).
*
* @return this
* @task stop
*/
prevent : function() {
var r = this.getRawEvent();
if (r) {
r.returnValue = false;
r.preventDefault && r.preventDefault();
}
this.setPrevented(true);
return this;
},
/**
* Stop and prevent an event, which stops it from propagating and prevents
* its defualt behavior. This is a convenience function, see stop() and
* prevent() for information on what it means to stop or prevent an event.
*
* @return this
* @task stop
*/
kill : function() {
this.prevent();
this.stop();
return this;
},
/**
* Get the special key (like tab or return), if any, associated with this
* event. Browsers report special keys differently; this method allows you
* to identify a keypress in a browser-agnostic way. Note that this detects
* only some special keys: delete, tab, return escape, left, up, right,
* down.
*
* For example, if you want to react to the escape key being pressed, you
* could install a listener like this:
*
* JX.Stratcom.listen('keydown', 'example', function(e) {
* if (e.getSpecialKey() == 'esc') {
* JX.log("You pressed 'Escape'! Well done! Bravo!");
* }
* });
*
- *
* @return string|null ##null## if there is no associated special key,
* or one of the strings 'delete', 'tab', 'return',
* 'esc', 'left', 'up', 'right', or 'down'.
* @task info
*/
getSpecialKey : function() {
var r = this.getRawEvent();
if (!r || r.shiftKey) {
return null;
}
- var c = r.keyCode;
- do {
- c = JX.Event._keymap[c] || null;
- } while (c && JX.Event._keymap[c])
-
- return c;
+ return JX.Event._keymap[r.keyCode] || null;
},
+
/**
* Get the node corresponding to the specified key in this event's node map.
* This is a simple helper method that makes the API for accessing nodes
* less ugly.
*
* JX.Stratcom.listen('click', 'tag:a', function(e) {
- * var a = e.getNode('nearest:a');
+ * var a = e.getNode('tag:a');
* // do something with the link that was clicked
* });
*
* @param string sigil or stratcom node key
* @return node|null Node mapped to the specified key, or null if it the
* key does not exist. The available keys include:
* - 'tag:'+tag - first node of each type
* - 'id:'+id - all nodes with an id
* - sigil - first node of each sigil
* @task info
*/
- getNode: function(key) {
+ getNode : function(key) {
return this.getNodes()[key] || null;
- }
+ },
+
+ /**
+ * Get the metadata associated with the node that corresponds to the key
+ * in this event's node map. This is a simple helper method that makes
+ * the API for accessing metadata associated with specific nodes less ugly.
+ *
+ * JX.Stratcom.listen('click', 'tag:a', function(event) {
+ * var anchorData = event.getNodeData('tag:a');
+ * // do something with the metadata of the link that was clicked
+ * });
+ *
+ * @param string sigil or stratcom node key
+ * @return dict dictionary of the node's metadata
+ * @task info
+ */
+ getNodeData : function(key) {
+ // Evade static analysis - JX.Stratcom
+ return JX['Stratcom'].getData(this.getNode(key));
+ }
},
statics : {
_keymap : {
8 : 'delete',
9 : 'tab',
13 : 'return',
27 : 'esc',
37 : 'left',
38 : 'up',
39 : 'right',
40 : 'down',
- 63232 : 38,
- 63233 : 40,
- 62234 : 37,
- 62235 : 39
+ 63232 : 'up',
+ 63233 : 'down',
+ 62234 : 'left',
+ 62235 : 'right'
}
},
properties : {
/**
* Native Javascript event which generated this @{JX.Event}. Not every
* event is generated by a native event, so there may be ##null## in
* this field.
*
* @type Event|null
* @task info
*/
rawEvent : null,
/**
* String describing the event type, like 'click' or 'mousedown'. This
* may also be an application or object event.
*
* @type string
* @task info
*/
type : null,
/**
* If available, the DOM node where this event occurred. For example, if
* this event is a click on a button, the target will be the button which
* was clicked. Application events will not have a target, so this property
* will return the value ##null##.
*
* @type DOMNode|null
* @task info
*/
target : null,
/**
* Metadata attached to nodes associated with this event.
*
* For native events, the DOM is walked from the event target to the root
* element. Each sigil which is encountered while walking up the tree is
* added to the map as a key. If the node has associated metainformation,
* it is set as the value; otherwise, the value is null.
*
* @type dict<string, *>
* @task info
*/
data : null,
/**
* Sigil path this event was activated from. TODO: explain this
*
* @type list<string>
* @task info
*/
path : [],
/**
* True if propagation of the event has been stopped. See stop().
*
* @type bool
* @task stop
*/
stopped : false,
/**
* True if default behavior of the event has been prevented. See prevent().
*
* @type bool
* @task stop
*/
prevented : false,
/**
* @task info
*/
nodes : {}
},
/**
* @{JX.Event} installs a toString() method in ##__DEV__## which allows you to
* log or print events and get a reasonable representation of them:
*
* Event<'click', ['path', 'stuff'], [object HTMLDivElement]>
*/
initialize : function() {
if (__DEV__) {
JX.Event.prototype.toString = function() {
var path = '['+this.getPath().join(', ')+']';
return 'Event<'+this.getType()+', '+path+', '+this.getTarget()+'>';
}
}
}
});
/**
* @requires javelin-install javelin-event javelin-util javelin-magical-init
* @provides javelin-stratcom
* @javelin
*/
/**
* Javelin strategic command, the master event delegation core. This class is
* a sort of hybrid between Arbiter and traditional event delegation, and
* serves to route event information to handlers in a general way.
*
* Each Javelin :JX.Event has a 'type', which may be a normal Javascript type
* (for instance, a click or a keypress) or an application-defined type. It
* also has a "path", based on the path in the DOM from the root node to the
* event target. Note that, while the type is required, the path may be empty
* (it often will be for application-defined events which do not originate
* from the DOM).
*
* The path is determined by walking down the tree to the event target and
* looking for nodes that have been tagged with metadata. These names are used
* to build the event path, and unnamed nodes are ignored. Each named node may
* also have data attached to it.
*
* Listeners specify one or more event types they are interested in handling,
* and, optionally, one or more paths. A listener will only receive events
* which occurred on paths it is listening to. See listen() for more details.
*
* @author epriestley
*
* @task invoke Invoking Events
* @task listen Listening to Events
* @task handle Responding to Events
* @task sigil Managing Sigils
+ * @task meta Managing Metadata
* @task internal Internals
*/
JX.install('Stratcom', {
statics : {
ready : false,
_targets : {},
_handlers : [],
_need : {},
- _matchName : /\bFN_([^ ]+)/,
- _matchData : /\bFD_([^ ]+)_([^ ]+)/,
_auto : '*',
_data : {},
_execContext : [],
_typeMap : {focusin: 'focus', focusout: 'blur'},
/**
* Node metadata is stored in a series of blocks to prevent collisions
* between indexes that are generated on the server side (and potentially
* concurrently). Block 0 is for metadata on the initial page load, block 1
* is for metadata added at runtime with JX.Stratcom.siglize(), and blocks
* 2 and up are for metadata generated from other sources (e.g. JX.Request).
* Use allocateMetadataBlock() to reserve a block, and mergeData() to fill
* a block with data.
*
* When a JX.Request is sent, a block is allocated for it and any metadata
* it returns is filled into that block.
*/
_dataBlock : 2,
/**
* Within each datablock, data is identified by a unique index. The data
- * pointer on a node looks like this:
+ * pointer (data-meta attribute) on a node looks like this:
*
- * FD_1_2
+ * 1_2
*
* ...where 1 is the block, and 2 is the index within that block. Normally,
* blocks are filled on the server side, so index allocation takes place
- * there. However, when data is provided with JX.Stratcom.sigilize(), we
+ * there. However, when data is provided with JX.Stratcom.addData(), we
* need to allocate indexes on the client.
*/
_dataIndex : 0,
/**
* Dispatch a simple event that does not have a corresponding native event
* object. It is unusual to call this directly. Generally, you will instead
* dispatch events from an object using the invoke() method present on all
* objects. See @{JX.Base.invoke()} for documentation.
*
* @param string Event type.
* @param list? Optionally, a path to attach to the event. This is
* rarely meaingful for simple events.
* @param object? Optionally, arbitrary data to send with the event.
* @return @{JX.Event} The event object which was dispatched to listeners.
* The main use of this is to test whether any
* listeners prevented the event.
* @task invoke
*/
invoke : function(type, path, data) {
var proxy = new JX.Event()
.setType(type)
.setData(data || {})
.setPath(path || []);
return this._dispatchProxy(proxy);
},
/**
* Listen for events on given paths. Specify one or more event types, and
* zero or more paths to filter on. If you don't specify a path, you will
* receive all events of the given type:
*
* // Listen to all clicks.
* JX.Stratcom.listen('click', null, handler);
*
* This will notify you of all clicks anywhere in the document (unless
* they are intercepted and killed by a higher priority handler before they
* get to you).
*
* Often, you may be interested in only clicks on certain elements. You
* can specify the paths you're interested in to filter out events which
* you do not want to be notified of.
*
* // Listen to all clicks inside elements annotated "news-feed".
* JX.Stratcom.listen('click', 'news-feed', handler);
*
* By adding more elements to the path, you can create a finer-tuned
* filter:
*
* // Listen to only "like" clicks inside "news-feed".
* JX.Stratcom.listen('click', ['news-feed', 'like'], handler);
*
*
* TODO: Further explain these shenanigans.
*
* @param string|list<string> Event type (or list of event names) to
* listen for. For example, ##'click'## or
* ##['keydown', 'keyup']##.
*
* @param wild Sigil paths to listen for this event on. See discussion
* in method documentation.
*
* @param function Callback to invoke when this event is triggered. It
* should have the signature ##f(:JX.Event e)##.
*
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
* @author epriestley
* @task listen
*/
listen : function(types, paths, func) {
if (__DEV__) {
if (arguments.length == 4) {
throw new Error(
'JX.Stratcom.listen(...): '+
'requires exactly 3 arguments. Did you mean JX.DOM.listen?');
}
if (arguments.length != 3) {
throw new Error(
'JX.Stratcom.listen(...): '+
'requires exactly 3 arguments.');
}
if (typeof func != 'function') {
throw new Error(
'JX.Stratcom.listen(...): '+
'callback is not a function.');
}
}
var ids = [];
types = JX.$AX(types);
if (!paths) {
paths = this._auto;
}
if (!(paths instanceof Array)) {
paths = [[paths]];
} else if (!(paths[0] instanceof Array)) {
paths = [paths];
}
// To listen to multiple event types on multiple paths, we just install
// the same listener a whole bunch of times: if we install for two
// event types on three paths, we'll end up with six references to the
// listener.
//
// TODO: we'll call your listener twice if you install on two paths where
// one path is a subset of another. The solution is "don't do that", but
// it would be nice to verify that the caller isn't doing so, in __DEV__.
for (var ii = 0; ii < types.length; ++ii) {
var type = types[ii];
if (('onpagehide' in window) && type == 'unload') {
// If we use "unload", we break the bfcache ("Back-Forward Cache") in
// Safari and Firefox. The BFCache makes using the back/forward
// buttons really fast since the pages can come out of magical
// fairyland instead of over the network, so use "pagehide" as a proxy
// for "unload" in these browsers.
type = 'pagehide';
}
if (!(type in this._targets)) {
this._targets[type] = {};
}
var type_target = this._targets[type];
for (var jj = 0; jj < paths.length; ++jj) {
var path = paths[jj];
var id = this._handlers.length;
this._handlers.push(func);
this._need[id] = path.length;
ids.push(id);
for (var kk = 0; kk < path.length; ++kk) {
if (__DEV__) {
if (path[kk] == 'tag:#document') {
throw new Error(
'JX.Stratcom.listen(..., "tag:#document", ...): ' +
- 'listen for document events as "tag:window", not ' +
- '"tag:#document", in order to get consistent behavior ' +
- 'across browsers.');
+ 'listen for all events using null, not "tag:#document"');
+ }
+ if (path[kk] == 'tag:window') {
+ throw new Error(
+ 'JX.Stratcom.listen(..., "tag:window", ...): ' +
+ 'listen for window events using null, not "tag:window"');
}
}
if (!type_target[path[kk]]) {
type_target[path[kk]] = [];
}
type_target[path[kk]].push(id);
}
}
}
return {
remove : function() {
for (var ii = 0; ii < ids.length; ii++) {
delete JX.Stratcom._handlers[ids[ii]];
}
}
};
},
/**
* Dispatch a native Javascript event through the Stratcom control flow.
* Generally, this is automatically called for you by the master dipatcher
* installed by ##init.js##. When you want to dispatch an application event,
* you should instead call invoke().
*
* @param Event Native event for dispatch.
* @return :JX.Event Dispatched :JX.Event.
* @task internal
*/
dispatch : function(event) {
- // TODO: simplify this :P
- var target;
- try {
- target = event.srcElement || event.target;
- if (target === window || (!target || target.nodeName == '#document')) {
- target = {nodeName: 'window'};
- }
- } catch (x) {
- target = null;
- }
-
var path = [];
var nodes = {};
var push = function(key, node) {
// we explicitly only store the first occurrence of each key
- if (!(key in nodes)) {
+ if (!nodes.hasOwnProperty(key)) {
nodes[key] = node;
path.push(key);
}
};
+ var target = event.srcElement || event.target;
+
+ // Since you can only listen by tag, id or sigil, which are all
+ // attributes of an Element (the DOM interface), we unset the target
+ // if it isn't an Element (window and Document are Nodes but not Elements)
+ if (!target || !target.getAttribute) {
+ target = null;
+ }
+
var cursor = target;
- while (cursor) {
+ while (cursor && cursor.getAttribute) {
push('tag:' + cursor.nodeName.toLowerCase(), cursor);
var id = cursor.id;
if (id) {
push('id:' + id, cursor);
}
- var source = cursor.className || '';
- // className is an SVGAnimatedString for SVG elements, use baseVal
- var token = ((source.baseVal || source).match(this._matchName) || [])[1];
- if (token) {
- push(token, cursor);
+ var sigils = cursor.getAttribute('data-sigil');
+ if (sigils) {
+ sigils = sigils.split(' ');
+ for (var ii = 0; ii < sigils.length; ii++) {
+ push(sigils[ii], cursor);
+ }
}
cursor = cursor.parentNode;
}
var etype = event.type;
if (etype in this._typeMap) {
etype = this._typeMap[etype];
}
- var data = {};
- for (var key in nodes) {
- data[key] = this.getData(nodes[key]);
- }
-
var proxy = new JX.Event()
.setRawEvent(event)
.setType(etype)
.setTarget(target)
- .setData(data)
.setNodes(nodes)
.setPath(path.reverse());
// JX.log('~> '+proxy.toString());
return this._dispatchProxy(proxy);
},
/**
* Dispatch a previously constructed proxy :JX.Event.
*
* @param :JX.Event Event to dispatch.
* @return :JX.Event Returns the event argument.
* @task internal
*/
_dispatchProxy : function(proxy) {
var scope = this._targets[proxy.getType()];
if (!scope) {
return proxy;
}
var path = proxy.getPath();
var len = path.length;
var hits = {};
var matches;
for (var root = -1; root < len; ++root) {
if (root == -1) {
matches = scope[this._auto];
} else {
matches = scope[path[root]];
}
if (!matches) {
continue;
}
for (var ii = 0; ii < matches.length; ++ii) {
hits[matches[ii]] = (hits[matches[ii]] || 0) + 1;
}
}
var exec = [];
for (var k in hits) {
if (hits[k] == this._need[k]) {
var handler = this._handlers[k];
if (handler) {
exec.push(handler);
}
}
}
this._execContext.push({
handlers: exec,
event: proxy,
cursor: 0
});
this.pass();
this._execContext.pop();
return proxy;
},
/**
* Pass on an event, allowing other handlers to process it. The use case
* here is generally something like:
*
* if (JX.Stratcom.pass()) {
* // something else handled the event
* return;
* }
* // handle the event
* event.prevent();
*
* This allows you to install event handlers that operate at a lower
* effective priority, and provide a default behavior which is overridable
* by listeners.
*
* @return bool True if the event was stopped or prevented by another
* handler.
* @task handle
*/
pass : function() {
var context = this._execContext[this._execContext.length - 1];
while (context.cursor < context.handlers.length) {
var cursor = context.cursor;
++context.cursor;
(context.handlers[cursor] || JX.bag)(context.event);
if (context.event.getStopped()) {
break;
}
}
return context.event.getStopped() || context.event.getPrevented();
},
/**
* Retrieve the event (if any) which is currently being dispatched.
*
* @return :JX.Event|null Event which is currently being dispatched, or
* null if there is no active dispatch.
* @task handle
*/
context : function() {
var len = this._execContext.length;
if (!len) {
return null;
}
return this._execContext[len - 1].event;
},
/**
* Merge metadata. You must call this (even if you have no metadata) to
* start the Stratcom queue.
*
* @param int The datablock to merge data into.
* @param dict Dictionary of metadata.
* @return void
* @task internal
*/
mergeData : function(block, data) {
this._data[block] = data;
if (block == 0) {
JX.Stratcom.ready = true;
JX.__rawEventQueue({type: 'start-queue'});
}
},
/**
- * Attach a sigil (and, optionally, metadata) to a node. Note that you can
- * not overwrite, remove or replace a sigil.
+ * Determine if a node has a specific sigil.
+ *
+ * @param Node Node to test.
+ * @param string Sigil to check for.
+ * @return bool True if the node has the sigil.
*
- * @param Node Node without any sigil.
- * @param string Sigil to name the node with.
- * @param object? Optional metadata object to attach to the node.
- * @return void
* @task sigil
*/
- sigilize : function(node, sigil, data) {
+ hasSigil : function(node, sigil) {
if (__DEV__) {
- if (node.className.match(this._matchName)) {
- throw new Error(
- 'JX.Stratcom.sigilize(<node>, ' + sigil + ', ...): ' +
- 'node already has a sigil, sigils may not be overwritten.');
- }
- if (typeof data != 'undefined' &&
- (data === null || typeof data != 'object')) {
+ if (!node || !node.getAttribute) {
throw new Error(
- 'JX.Stratcom.sigilize(..., ..., <nonobject>): ' +
- 'data to attach to node is not an object. You must use ' +
- 'objects, not primitives, for metadata.');
+ 'JX.Stratcom.hasSigil(<non-element>, ...): ' +
+ 'node is not an element. Most likely, you\'re passing window or ' +
+ 'document, which are not elements and can\'t have sigils.');
}
}
- if (data) {
- JX.Stratcom._setData(node, data);
- }
-
- node.className = 'FN_' + sigil + ' ' + node.className;
+ var sigils = node.getAttribute('data-sigil');
+ return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1;
},
/**
- * Determine if a node has a specific sigil.
- *
- * @param Node Node to test.
- * @param string Sigil to check for.
- * @return bool True if the node has the sigil.
+ * Add a sigil to a node.
*
+ * @param Node Node to add the sigil to.
+ * @param string Sigil to name the node with.
+ * @return void
* @task sigil
*/
- hasSigil : function(node, sigil) {
- if (!node.className) {
- // Some nodes don't have a className, notably 'document'. We hit
- // 'document' when following .parentNode chains, e.g. in
- // JX.DOM.nearest(), so exit early if we don't have a className to avoid
- // fataling on 'node.className.match' being undefined.
- return false;
- }
- return (node.className.match(this._matchName) || [])[1] == sigil;
+ addSigil: function(node, sigil) {
+ if (__DEV__) {
+ if (!node || !node.getAttribute) {
+ throw new Error(
+ 'JX.Stratcom.addSigil(<non-element>, ...): ' +
+ 'node is not an element. Most likely, you\'re passing window or ' +
+ 'document, which are not elements and can\'t have sigils.');
+ }
+ }
+
+ var sigils = node.getAttribute('data-sigil');
+ if (sigils && !JX.Stratcom.hasSigil(node, sigil)) {
+ sigil = sigils + ' ' + sigil;
+ }
+
+ node.setAttribute('data-sigil', sigil);
},
/**
* Retrieve a node's metadata.
*
- * @param Node Node from which to retrieve data.
- * @return object Data attached to the node, or an empty dictionary if
- * the node has no data attached. In this case, the empty
- * dictionary is set as the node's metadata -- i.e.,
- * subsequent calls to getData() will retrieve the same
- * object.
- *
- * @task sigil
+ * @param Node Node from which to retrieve data.
+ * @return object Data attached to the node. If no data has been attached
+ * to the node yet, an empty object will be returned, but
+ * subsequent calls to this method will always retrieve the
+ * same object.
+ * @task meta
*/
getData : function(node) {
if (__DEV__) {
- if (!node) {
+ if (!node || !node.getAttribute) {
throw new Error(
- 'JX.Stratcom.getData(<empty>): ' +
- 'you must provide a node to get associated data from.');
+ 'JX.Stratcom.getData(<non-element>): ' +
+ 'node is not an element. Most likely, you\'re passing window or ' +
+ 'document, which are not elements and can\'t have data.');
}
}
- var matches = (node.className || '').match(this._matchData);
- if (matches) {
- var block = this._data[matches[1]];
- var index = matches[2];
+ var meta_id = (node.getAttribute('data-meta') || '').split('_');
+ if (meta_id[0] && meta_id[1]) {
+ var block = this._data[meta_id[0]];
+ var index = meta_id[1];
if (block && (index in block)) {
return block[index];
}
}
- return JX.Stratcom._setData(node, {});
+ var data = {};
+ if (!this._data[1]) { // data block 1 is reserved for JavaScript
+ this._data[1] = {};
+ }
+ this._data[1][this._dataIndex] = data;
+ node.setAttribute('data-meta', '1_' + (this._dataIndex++));
+ return data;
},
- /**
- * @task internal
+ /**
+ * Add data to a node's metadata.
+ *
+ * @param Node Node which data should be attached to.
+ * @param object Data to add to the node's metadata.
+ * @return object Data attached to the node that is returned by
+ * JX.Stratcom.getData().
+ * @task meta
*/
- allocateMetadataBlock : function() {
- return this._dataBlock++;
+ addData : function(node, data) {
+ if (__DEV__) {
+ if (!node || !node.getAttribute) {
+ throw new Error(
+ 'JX.Stratcom.addData(<non-element>, ...): ' +
+ 'node is not an element. Most likely, you\'re passing window or ' +
+ 'document, which are not elements and can\'t have sigils.');
+ }
+ if (!data || typeof data != 'object') {
+ throw new Error(
+ 'JX.Stratcom.addData(..., <nonobject>): ' +
+ 'data to attach to node is not an object. You must use ' +
+ 'objects, not primitives, for metadata.');
+ }
+ }
+
+ return JX.copy(JX.Stratcom.getData(node), data);
},
+
/**
- * Attach metadata to a node. This data can later be retrieved through
- * @{JX.Stratcom.getData()}, or @{JX.Event.getData()}.
- *
- * @param Node Node which data should be attached to.
- * @param object Data to attach.
- * @return object Attached data.
- *
* @task internal
*/
- _setData : function(node, data) {
- if (!this._data[1]) { // data block 1 is reserved for javascript
- this._data[1] = {};
- }
- this._data[1][this._dataIndex] = data;
- node.className = 'FD_1_' + (this._dataIndex++) + ' ' + node.className;
- return data;
+ allocateMetadataBlock : function() {
+ return this._dataBlock++;
}
}
});
/**
* @provides javelin-behavior
*
* @javelin-installs JX.behavior
* @javelin-installs JX.initBehaviors
*
* @javelin
*/
JX.behavior = function(name, control_function) {
if (__DEV__) {
- if (name in JX.behavior._behaviors) {
+ if (JX.behavior._behaviors.hasOwnProperty(name)) {
throw new Error(
'JX.behavior("'+name+'", ...): '+
'behavior is already registered.');
}
if (!control_function) {
throw new Error(
'JX.behavior("'+name+'", <nothing>): '+
'initialization function is required.');
}
if (typeof control_function != 'function') {
throw new Error(
'JX.behavior("'+name+'", <garbage>): '+
'initialization function is not a function.');
}
}
JX.behavior._behaviors[name] = control_function;
};
JX.initBehaviors = function(map) {
for (var name in map) {
if (__DEV__) {
if (!(name in JX.behavior._behaviors)) {
throw new Error(
'JX.initBehavior("'+name+'", ...): '+
'behavior is not registered.');
}
}
var configs = map[name];
if (!configs.length) {
- if (name in JX.behavior._initialized) {
+ if (JX.behavior._initialized.hasOwnProperty(name)) {
continue;
} else {
configs = [null];
}
}
for (var ii = 0; ii < configs.length; ii++) {
JX.behavior._behaviors[name](configs[ii]);
}
JX.behavior._initialized[name] = true;
}
};
!function(JX) {
JX.behavior._behaviors = {};
JX.behavior._initialized = {};
}(JX);
/**
* @requires javelin-install
* javelin-stratcom
* javelin-util
* javelin-behavior
* @provides javelin-request
* @javelin
*/
/**
* Make basic AJAX XMLHTTPRequests.
*/
JX.install('Request', {
construct : function(uri, handler) {
this.setURI(uri);
if (handler) {
this.listen('done', handler);
}
},
events : ['send', 'done', 'error', 'finally'],
members : {
_xhrkey : null,
_transport : null,
_finished : false,
_block : null,
send : function() {
var xport = null;
try {
try {
xport = new XMLHttpRequest();
} catch (x) {
xport = new ActiveXObject("Msxml2.XMLHTTP");
}
} catch (x) {
xport = new ActiveXObject("Microsoft.XMLHTTP");
}
this._transport = xport;
this._xhrkey = JX.Request._xhr.length;
JX.Request._xhr.push(this);
xport.onreadystatechange = JX.bind(this, this._onreadystatechange);
var data = this.getData() || {};
data.__ajax__ = true;
this._block = JX.Stratcom.allocateMetadataBlock();
data.__metablock__ = this._block;
var q = (this.getDataSerializer() ||
JX.Request.defaultDataSerializer)(data);
var uri = this.getURI();
var method = this.getMethod().toUpperCase();
if (method == 'GET') {
uri += ((uri.indexOf('?') === -1) ? '?' : '&') + q;
}
this.invoke('send', this);
if (this.getTimeout()) {
this._timer = JX.defer(
JX.bind(
this,
this._fail,
JX.Request.ERROR_TIMEOUT),
this.getTimeout());
}
xport.open(method, uri, true);
if (__DEV__) {
if (this.getFile()) {
if (method != 'POST') {
throw new Error(
'JX.Request.send(): ' +
'attempting to send a file over GET. You must use POST.');
}
if (this.getData()) {
throw new Error(
'JX.Request.send(): ' +
'attempting to send data and a file. You can not send both ' +
'at once.');
}
}
}
if (method == 'POST') {
if (this.getFile()) {
xport.send(this.getFile());
} else {
xport.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded');
xport.send(q);
}
} else {
xport.send(null);
}
},
abort : function() {
this._cleanup();
},
_onreadystatechange : function() {
var xport = this._transport;
try {
if (this._finished) {
return;
}
if (xport.readyState != 4) {
return;
}
if (xport.status < 200 || xport.status >= 300) {
this._fail();
return;
}
if (__DEV__) {
if (!xport.responseText.length) {
throw new Error(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an empty response.');
}
if (xport.responseText.indexOf('for (;;);') != 0) {
throw new Error(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an invalid response.');
}
}
var text = xport.responseText.substring('for (;;);'.length);
var response = eval('('+text+')');
} catch (exception) {
if (__DEV__) {
JX.log(
'JX.Request("'+this.getURI()+'", ...): '+
'caught exception processing response: '+exception);
}
this._fail();
return;
}
try {
if (response.error) {
this._fail(response.error);
} else {
JX.Stratcom.mergeData(
this._block,
response.javelin_metadata || {});
this._done(response);
JX.initBehaviors(response.javelin_behaviors || {});
}
} catch (exception) {
// In Firefox+Firebug, at least, something eats these. :/
JX.defer(function() {
throw exception;
});
}
},
_fail : function(error) {
this._cleanup();
this.invoke('error', error, this);
this.invoke('finally');
},
_done : function(response) {
this._cleanup();
if (response.onload) {
for (var ii = 0; ii < response.onload.length; ii++) {
(new Function(response.onload[ii]))();
}
}
this.invoke('done', this.getRaw() ? response : response.payload, this);
this.invoke('finally');
},
_cleanup : function() {
this._finished = true;
delete JX.Request._xhr[this._xhrkey];
this._timer && this._timer.stop();
this._transport.abort();
}
},
statics : {
_xhr : [],
shutdown : function() {
for (var ii = 0; ii < JX.Request._xhr.length; ii++) {
try {
JX.Request._xhr[ii] && JX.Request._xhr[ii].abort();
} catch (x) {
// Ignore.
}
}
JX.Request._xhr = [];
},
ERROR_TIMEOUT : -9000,
defaultDataSerializer : function(data) {
var uri = [];
for (var k in data) {
uri.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
}
return uri.join('&');
}
},
properties : {
URI : null,
data : null,
dataSerializer : null,
/**
* Configure which HTTP method to use for the request. Permissible values
* are "POST" (default) or "GET".
*
* @param string HTTP method, one of "POST" or "GET".
*/
method : 'POST',
file : null,
raw : false,
/**
* Configure a timeout, in milliseconds. If the request has not resolved
* (either with success or with an error) within the provided timeframe,
* it will automatically fail with error JX.Request.ERROR_TIMEOUT.
*
* @param int Timeout, in milliseconds (e.g. 3000 = 3 seconds).
*/
timeout : null
},
initialize : function() {
- JX.Stratcom.listen('unload', 'tag:window', JX.Request.shutdown);
+ JX.Stratcom.listen('unload', null, JX.Request.shutdown);
}
});
/**
* @requires javelin-install javelin-event
* @provides javelin-vector
* @javelin
*/
/**
* Query and update positions and dimensions of nodes (and other things)
* within a document. 'V' stands for 'Vector'. Each vector has two elements,
* 'x' and 'y', which usually represent width/height (a "dimension vector") or
* left/top (a "position vector").
*
* Vectors are used to manage the sizes and positions of elements, events,
* the document, and the viewport (the visible section of the document, i.e.
* how much of the page the user can actually see in their browser window).
* Unlike most Javelin classes, @{JX.$V} exposes two bare properties, 'x' and
* 'y'. You can read and manipulate these directly:
*
* // Give the user information about elements when they click on them.
* JX.Stratcom.listen(
* 'click',
* null,
* function(e) {
* var p = JX.$V(e);
* var d = JX.$V.getDim(e.getTarget());
*
* alert('You clicked at <'+p.x+','+p.y'>; the element you clicked '+
* 'is '+d.x+' pixels wide and '+d.y+' pixels high.');
* });
*
* You can also update positions and dimensions using vectors:
*
* // When the user clicks on something, make it 10px wider and 10px taller.
* JX.Stratcom.listen(
* 'click',
* null,
* function(e) {
* var t = e.getTarget();
* JX.$V(t).add(10, 10).setDim(t);
* });
*
* Additionally, vectors can be used to query document and viewport information:
*
* var v = JX.$V.getViewport(); // Viewport (window) width and height.
* var d = JX.$V.getDocument(); // Document width and height.
* var visible_area = parseInt(100 * (v.x * v.y) / (d.x * d.y), 10);
* alert('You can currently see '+visible_area'+ percent of the document.');
*
* @author epriestley
*
* @task query Querying Positions and Dimensions
* @task update Changing Positions and Dimensions
* @task manip Manipulating Vectors
*
*/
JX.install('$V', {
/**
* Construct a vector, either from explicit coordinates or from a node
* or event. You can pass two Numbers to construct an explicit vector:
*
* var v = JX.$V(35, 42);
*
* Otherwise, you can pass a @{JX.Event} or a Node to implicitly construct a
* vector:
*
* var u = JX.$V(some_event);
* var v = JX.$V(some_node);
*
* These are just like calling getPos() on the @{JX.Event} or Node.
*
* For convenience, @{JX.$V()} constructs a new vector even without the 'new'
* keyword. That is, these are equivalent:
*
* var q = new JX.$V(x, y);
* var r = JX.$V(x, y);
*
* Methods like getScroll(), getViewport() and getDocument() also create
* new vectors.
*
* Once you have a vector, you can manipulate it with add():
*
* var u = JX.$V(35, 42);
* var v = u.add(5, -12); // v = <40, 30>
*
* @param wild 'x' component of the vector, or a @{JX.Event}, or a Node.
* @param Number? If providing an 'x' component, the 'y' component of the
* vector.
* @return @{JX.$V} Specified vector.
* @task query
*/
construct : function(x, y) {
if (this == JX || this == window) {
return new JX.$V(x, y);
}
if (typeof y == 'undefined') {
return JX.$V.getPos(x);
}
this.x = parseFloat(x);
this.y = parseFloat(y);
},
canCallAsFunction : true,
members : {
x : null,
y : null,
/**
* Move a node around by setting the position of a Node to the vector's
* coordinates. For instance, if you want to move an element to the top left
* corner of the document, you could do this (assuming it has 'position:
* absolute'):
*
* JX.$V(0, 0).setPos(node);
*
* @param Node Node to move.
* @return this
* @task update
*/
setPos : function(node) {
node.style.left = (this.x === null) ? '' : (parseInt(this.x, 10) + 'px');
node.style.top = (this.y === null) ? '' : (parseInt(this.y, 10) + 'px');
return this;
},
/**
* Change the size of a node by setting its dimensions to the vector's
* coordinates. For instance, if you want to change an element to be 100px
* by 100px:
*
* JX.$V(100, 100).setDim(node);
*
* Or if you want to expand a node's dimensions by 50px:
*
* JX.$V(node).add(50, 50).setDim(node);
*
* @param Node Node to resize.
* @return this
* @task update
*/
setDim : function(node) {
node.style.width =
(this.x === null)
? ''
: (parseInt(this.x, 10) + 'px');
node.style.height =
(this.y === null)
? ''
: (parseInt(this.y, 10) + 'px');
return this;
},
/**
* Change a vector's x and y coordinates by adding numbers to them, or
* adding the coordinates of another vector. For example:
*
* var u = JX.$V(3, 4).add(100, 200); // u = <103, 204>
*
* You can also add another vector:
*
* var q = JX.$V(777, 999);
* var r = JX.$V(1000, 2000);
* var s = q.add(r); // s = <1777, 2999>
*
* Note that this method returns a new vector. It does not modify the
* 'this' vector.
*
* @param wild Value to add to the vector's x component, or another
* vector.
* @param Number? Value to add to the vector's y component.
* @return @{JX.$V} New vector, with summed components.
* @task manip
*/
add : function(x, y) {
if (x instanceof JX.$V) {
return this.add(x.x, x.y);
}
return JX.$V(this.x + parseFloat(x), this.y + parseFloat(y));
}
},
statics : {
_viewport: null,
/**
* Determine where in a document an element is (or where an event, like
* a click, occurred) by building a new vector containing the position of a
* Node or @{JX.Event}. The 'x' component of the vector will correspond to
* the pixel offset of the argument relative to the left edge of the
* document, and the 'y' component will correspond to the pixel offset of
* the argument relative to the top edge of the document. Note that all
* vectors are generated in document coordinates, so the scroll position
* does not affect them.
*
* See also getDim(), used to determine an element's dimensions.
*
* @param Node|@{JX.Event} Node or event to determine the position of.
* @return @{JX.$V} New vector with the argument's position.
* @task query
*/
getPos : function(node) {
JX.Event && (node instanceof JX.Event) && (node = node.getRawEvent());
if (('pageX' in node) || ('clientX' in node)) {
var c = JX.$V._viewport;
return JX.$V(
node.pageX || (node.clientX + c.scrollLeft),
node.pageY || (node.clientY + c.scrollTop));
}
var x = node.offsetLeft;
var y = node.offsetTop;
while (node.offsetParent && (node.offsetParent != document.body)) {
node = node.offsetParent;
x += node.offsetLeft;
y += node.offsetTop;
}
return JX.$V(x, y);
},
/**
* Determine the width and height of a node by building a new vector with
* dimension information. The 'x' component of the vector will correspond
* to the element's width in pixels, and the 'y' component will correspond
* to its height in pixels.
*
* See also getPos(), used to determine an element's position.
*
* @param Node Node to determine the display size of.
* @return @{JX.$V} New vector with the node's dimensions.
* @task query
*/
getDim : function(node) {
return JX.$V(node.offsetWidth, node.offsetHeight);
},
/**
* Determine the current scroll position by building a new vector where
* the 'x' component corresponds to how many pixels the user has scrolled
* from the left edge of the document, and the 'y' component corresponds to
* how many pixels the user has scrolled from the top edge of the document.
*
* See also getViewport(), used to determine the size of the viewport.
*
* @return @{JX.$V} New vector with the document scroll position.
* @task query
*/
getScroll : function() {
// We can't use $V._viewport here because there's diversity between
// browsers with respect to where position/dimension and scroll position
// information is stored.
var b = document.body;
var e = document.documentElement;
return JX.$V(b.scrollLeft || e.scrollLeft, b.scrollTop || e.scrollTop);
},
/**
* Determine the size of the viewport (basically, the browser window) by
* building a new vector where the 'x' component corresponds to the width
* of the viewport in pixels and the 'y' component corresponds to the height
* of the viewport in pixels.
*
* See also getScroll(), used to determine the position of the viewport, and
* getDocument(), used to determine the size of the entire document.
*
* @return @{JX.$V} New vector with the viewport dimensions.
* @task query
*/
getViewport : function() {
var c = JX.$V._viewport;
var w = window;
return JX.$V(
w.innerWidth || c.clientWidth || 0,
w.innerHeight || c.clientHeight || 0
);
},
/**
* Determine the size of the document, including any area outside the
* current viewport which the user would need to scroll in order to see, by
* building a new vector where the 'x' component corresponds to the document
* width in pixels and the 'y' component corresponds to the document height
* in pixels.
*
* @return @{JX.$V} New vector with the document dimensions.
* @task query
*/
getDocument : function() {
var c = JX.$V._viewport;
return JX.$V(c.scrollWidth || 0, c.scrollHeight || 0);
}
},
/**
* On initialization, the browser-dependent viewport root is determined and
* stored.
*
* In ##__DEV__##, @{JX.$V} installs a toString() method so vectors print in a
* debuggable way:
*
* <23, 92>
*
* @return void
*/
initialize : function() {
var c = ((c = document) && (c = c.documentElement)) ||
((c = document) && (c = c.body))
JX.$V._viewport = c;
if (__DEV__) {
JX.$V.prototype.toString = function() {
return '<'+this.x+', '+this.y+'>';
}
}
}
});
/**
* @requires javelin-install javelin-util javelin-vector javelin-stratcom
* @provides javelin-dom
*
* @javelin-installs JX.$
* @javelin-installs JX.$N
*
* @javelin
*/
/**
* Select an element by its "id" attribute, like ##document.getElementById()##.
* For example:
*
* var node = JX.$('some_id');
*
* This will select the node with the specified "id" attribute:
*
* LANG=HTML
* <div id="some_id">...</div>
*
* If the specified node does not exist, @{JX.$()} will throw ##JX.$.NotFound##.
* For other ways to select nodes from the document, see @{JX.DOM.scry()} and
* @{JX.DOM.find()}.
*
* @param string "id" attribute to select from the document.
* @return Node Node with the specified "id" attribute.
*/
JX.$ = function(id) {
if (__DEV__) {
if (!id) {
throw new Error('Empty ID passed to JX.$()!');
}
}
var node = document.getElementById(id);
if (!node || (node.id != id)) {
if (__DEV__) {
if (node && (node.id != id)) {
throw new Error(
'JX.$("'+id+'"): '+
'document.getElementById() returned an element without the '+
'correct ID. This usually means that the element you are trying '+
'to select is being masked by a form with the same value in its '+
'"name" attribute.');
}
}
throw JX.$.NotFound;
}
return node;
};
JX.$.NotFound = {};
if (__DEV__) {
// If we're in dev, upgrade this object into an Error so that it will
// print something useful if it escapes the stack after being thrown.
JX.$.NotFound = new Error(
'JX.$() or JX.DOM.find() call matched no nodes.');
}
/**
* Upcast a string into an HTML object so it is treated as markup instead of
* plain text. See @{JX.$N} for discussion of Javelin's security model. Every
* time you call this function you potentially open up a security hole. Avoid
* its use wherever possible.
*
* This class intentionally supports only a subset of HTML because many browsers
* named "Internet Explorer" have awkward restrictions around what they'll
* accept for conversion to document fragments. Alter your datasource to emit
* valid HTML within this subset if you run into an unsupported edge case. All
* the edge cases are crazy and you should always be reasonably able to emit
* a cohesive tag instead of an unappendable fragment.
*
* @task build String into HTML
* @task nodes HTML into Nodes
*/
JX.install('HTML', {
/**
* Build a new HTML object from a trustworthy string.
*
* @task build
* @param string A string which you want to be treated as HTML, because you
* know it is from a trusted source and any data in it has been
* properly escaped.
* @return JX.HTML HTML object, suitable for use with @{JX.$N}.
*/
construct : function(str) {
if (this == JX || this == window) {
return new JX.HTML(str);
}
if (__DEV__) {
var tags = ['legend', 'thead', 'tbody', 'tfoot', 'column', 'colgroup',
'caption', 'tr', 'th', 'td', 'option'];
var evil_stuff = new RegExp('^\\s*<('+tags.join('|')+')\\b', 'i');
var match = null;
if (match = str.match(evil_stuff)) {
throw new Error(
'JX.HTML("<'+match[1]+'>..."): '+
'call initializes an HTML object with an invalid partial fragment '+
'and can not be converted into DOM nodes. The enclosing tag of an '+
'HTML content string must be appendable to a document fragment. '+
'For example, <table> is allowed but <tr> or <tfoot> are not.');
}
var really_evil = /<script\b/;
if (str.match(really_evil)) {
throw new Error(
'JX.HTML("...<script>..."): '+
'call initializes an HTML object with an embedded script tag! '+
'Are you crazy?! Do NOT do this!!!');
}
var wont_work = /<object\b/;
if (str.match(wont_work)) {
throw new Error(
'JX.HTML("...<object>..."): '+
'call initializes an HTML object with an embedded <object> tag. IE '+
'will not do the right thing with this.');
}
// TODO(epriestley): May need to deny <option> more broadly, see
// http://support.microsoft.com/kb/829907 and the whole mess in the
// heavy stack. But I seem to have gotten away without cloning into the
// documentFragment below, so this may be a nonissue.
}
this._content = str;
},
canCallAsFunction : true,
members : {
_content : null,
/**
* Convert the raw HTML string into a DOM node tree.
*
* @task nodes
* @return DocumentFragment A document fragment which contains the nodes
* corresponding to the HTML string you provided.
*/
getFragment : function() {
var wrapper = JX.$N('div');
wrapper.innerHTML = this._content;
var fragment = document.createDocumentFragment();
while (wrapper.firstChild) {
// TODO(epriestley): Do we need to do a bunch of cloning junk here?
// See heavy stack. I'm disconnecting the nodes instead; this seems
// to work but maybe my test case just isn't extensive enough.
fragment.appendChild(wrapper.removeChild(wrapper.firstChild));
}
return fragment;
}
}
});
/**
* Create a new DOM node with attributes and content.
*
* var link = JX.$N('a');
*
* This creates a new, empty anchor tag without any attributes. The equivalent
* markup would be:
*
* LANG=HTML
* <a />
*
* You can also specify attributes by passing a dictionary:
*
* JX.$N('a', {name: 'anchor'});
*
* This is equivalent to:
*
* LANG=HTML
* <a name="anchor" />
*
* Additionally, you can specify content:
*
* JX.$N(
* 'a',
* {href: 'http://www.javelinjs.com'},
* 'Visit the Javelin Homepage');
*
* This is equivalent to:
*
* LANG=HTML
* <a href="http://www.javelinjs.com">Visit the Javelin Homepage</a>
*
* If you only want to specify content, you can omit the attribute parameter.
* That is, these calls are equivalent:
*
* JX.$N('div', {}, 'Lorem ipsum...'); // No attributes.
* JX.$N('div', 'Lorem ipsum...') // Same as above.
*
* Both are equivalent to:
*
* LANG=HTML
* <div>Lorem ipsum...</div>
*
* Note that the content is treated as plain text, not HTML. This means it is
* safe to use untrusted strings:
*
* JX.$N('div', '<script src="evil.com" />');
*
* This is equivalent to:
*
* LANG=HTML
* <div>&lt;script src="evil.com" /&gt;</div>
*
* That is, the content will be properly escaped and will not create a
* vulnerability. If you want to set HTML content, you can use @{JX.HTML}:
*
* JX.$N('div', JX.HTML(some_html));
*
* **This is potentially unsafe**, so make sure you understand what you're
* doing. You should usually avoid passing HTML around in string form. See
* @{JX.HTML} for discussion.
*
* You can create new nodes with a Javelin sigil (and, optionally, metadata) by
* providing "sigil" and "metadata" keys in the attribute dictionary.
*
* @param string Tag name, like 'a' or 'div'.
* @param dict|string|@{JX.HTML}? Property dictionary, or content if you don't
* want to specify any properties.
* @param string|@{JX.HTML}? Content string (interpreted as plain text)
* or @{JX.HTML} object (interpreted as HTML,
* which may be dangerous).
* @return Node New node with whatever attributes and
* content were specified.
*/
JX.$N = function(tag, attr, content) {
if (typeof content == 'undefined' &&
(typeof attr != 'object' || attr instanceof JX.HTML)) {
content = attr;
attr = {};
}
if (__DEV__) {
if (tag.toLowerCase() != tag) {
throw new Error(
'$N("'+tag+'", ...): '+
'tag name must be in lower case; '+
'use "'+tag.toLowerCase()+'", not "'+tag+'".');
}
}
var node = document.createElement(tag);
if (attr.style) {
JX.copy(node.style, attr.style);
delete attr.style;
}
if (attr.sigil) {
- JX.Stratcom.sigilize(node, attr.sigil, attr.meta);
+ JX.Stratcom.addSigil(node, attr.sigil);
delete attr.sigil;
+ }
+
+ if (attr.meta) {
+ JX.Stratcom.addData(node, attr.meta);
delete attr.meta;
}
if (__DEV__) {
if (('metadata' in attr) || ('data' in attr)) {
throw new Error(
'$N(' + tag + ', ...): ' +
'use the key "meta" to specify metadata, not "data" or "metadata".');
}
- if (attr.meta) {
- throw new Error(
- '$N(' + tag + ', ...): ' +
- 'if you specify "meta" metadata, you must also specify a "sigil".');
- }
- }
-
- // prevent sigil from being wiped by blind copying the className
- if (attr.className) {
- JX.DOM.alterClass(node, attr.className, true);
- delete attr.className;
}
JX.copy(node, attr);
if (content) {
JX.DOM.setContent(node, content);
}
return node;
};
/**
* Query and update the DOM. Everything here is static, this is essentially
* a collection of common utility functions.
*
* @task stratcom Attaching Event Listeners
* @task content Changing DOM Content
* @task nodes Updating Nodes
* @task test Testing DOM Properties
* @task convenience Convenience Methods
* @task query Finding Nodes in the DOM
* @task view Changing View State
*/
JX.install('DOM', {
statics : {
_autoid : 0,
_metrics : {},
/**
* @task content
*/
setContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
throw new Error(
'JX.DOM.setContent(<yuck>, ...): '+
'first argument must be a DOM node.');
}
}
while (node.firstChild) {
JX.DOM.remove(node.firstChild);
}
JX.DOM.appendContent(node, content);
},
/**
* @task content
*/
prependContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
throw new Error(
'JX.DOM.prependContent(<junk>, ...): '+
'first argument must be a DOM node.');
}
}
this._insertContent(node, content, this._mechanismPrepend);
},
/**
* @task content
*/
appendContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
throw new Error(
'JX.DOM.appendContent(<bleh>, ...): '+
'first argument must be a DOM node.');
}
}
this._insertContent(node, content, this._mechanismAppend);
},
/**
* @task content
*/
_mechanismPrepend : function(node, content) {
node.insertBefore(content, node.firstChild);
},
/**
* @task content
*/
_mechanismAppend : function(node, content) {
node.appendChild(content);
},
/**
* @task content
*/
_insertContent : function(parent, content, mechanism) {
if (content === null || typeof content == 'undefined') {
return;
}
if (content instanceof JX.HTML) {
content = content.getFragment();
}
if (content instanceof Array) {
for (var ii = 0; ii < content.length; ii++) {
var child = (typeof content[ii] == 'string')
? document.createTextNode(content[ii])
: content[ii];
mechanism(parent, child);
}
} else if (content.nodeType) {
mechanism(parent, content);
} else {
mechanism(parent, document.createTextNode(content));
}
},
/**
* @task nodes
*/
remove : function(node) {
node.parentNode && JX.DOM.replace(node, null);
return node;
},
/**
* @task nodes
*/
replace : function(node, replacement) {
if (__DEV__) {
if (!node.parentNode) {
throw new Error(
'JX.DOM.replace(<node>, ...): '+
'node has no parent node, so it can not be replaced.');
}
}
var mechanism;
if (node.nextSibling) {
mechanism = JX.bind(node.nextSibling, function(parent, content) {
parent.insertBefore(content, this);
});
} else {
mechanism = this._mechanismAppend;
}
var parent = node.parentNode;
node.parentNode.removeChild(node);
this._insertContent(parent, replacement, mechanism);
return node;
},
/**
* Retrieve the nearest parent node matching the desired sigil.
* @param Node The child element to search from
* @return The matching parent or null if no parent could be found
* @author jgabbard
*/
nearest : function(node, sigil) {
- while (node && !JX.Stratcom.hasSigil(node, sigil)) {
+ while (node && node.getAttribute && !JX.Stratcom.hasSigil(node, sigil)) {
node = node.parentNode;
}
return node;
},
serialize : function(form) {
var elements = form.getElementsByTagName('*');
var data = {};
for (var ii = 0; ii < elements.length; ++ii) {
if (!elements[ii].name) {
continue;
}
var type = elements[ii].type;
var tag = elements[ii].tagName;
if ((type in {radio: 1, checkbox: 1} && elements[ii].checked) ||
type in {text: 1, hidden: 1, password: 1} ||
tag in {TEXTAREA: 1, SELECT: 1}) {
data[elements[ii].name] = elements[ii].value;
}
}
return data;
},
/**
* Test if an object is a valid Node.
*
* @task test
* @param wild Something which might be a Node.
* @return bool True if the parameter is a DOM node.
*/
isNode : function(node) {
return !!(node && node.nodeName && (node !== window));
},
/**
* Test if an object is a node of some specific (or one of several) types.
* For example, this tests if the node is an ##<input />##, ##<select />##,
* or ##<textarea />##.
*
* JX.DOM.isType(node, ['input', 'select', 'textarea']);
*
* @task test
* @param wild Something which might be a Node.
* @param string|list One or more tags which you want to test for.
* @return bool True if the object is a node, and it's a node of one
* of the provided types.
*/
isType : function(node, of_type) {
node = ('' + (node.nodeName || '')).toUpperCase();
of_type = JX.$AX(of_type);
for (var ii = 0; ii < of_type.length; ++ii) {
if (of_type[ii].toUpperCase() == node) {
return true;
}
}
return false;
},
/**
* Listen for events occuring beneath a specific node in the DOM. This is
* similar to @{JX.Stratcom.listen()}, but allows you to specify some node
* which serves as a scope instead of the default scope (the whole document)
* which you get if you install using @{JX.Stratcom.listen()} directly. For
* example, to listen for clicks on nodes with the sigil 'menu-item' below
* the root menu node:
*
* var the_menu = getReferenceToTheMenuNodeSomehow();
* JX.DOM.listen(the_menu, 'click', 'menu-item', function(e) { ... });
*
* @task stratcom
* @param Node The node to listen for events underneath.
* @param string|list One or more event types to listen for.
* @param list? A path to listen on.
* @param function Callback to invoke when a matching event occurs.
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
*/
listen : function(node, type, path, callback) {
return JX.Stratcom.listen(
type,
['id:'+JX.DOM.uniqID(node)].concat(JX.$AX(path || [])),
callback);
},
uniqID : function(node) {
if (!node.id) {
node.id = 'autoid_'+(++JX.DOM._autoid);
}
return node.id;
},
alterClass : function(node, className, add) {
var has = ((' '+node.className+' ').indexOf(' '+className+' ') > -1);
if (add && !has) {
node.className += ' '+className;
} else if (has && !add) {
node.className = node.className.replace(
new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), ' ');
}
},
htmlize : function(str) {
return (''+str)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
},
/**
* Show one or more elements, by removing their "display" style. This
* assumes you have hidden them with hide(), or explicitly set the style
* to "display: none;".
*
* @task convenience
* @param ... One or more nodes to remove "display" styles from.
* @return void
*/
show : function() {
if (__DEV__) {
for (var ii = 0; ii < arguments.length; ++ii) {
if (!arguments[ii]) {
throw new Error(
'JX.DOM.show(...): ' +
'one or more arguments were null or empty.');
}
}
}
for (var ii = 0; ii < arguments.length; ++ii) {
arguments[ii].style.display = '';
}
},
/**
* Hide one or more elements, by setting "display: none;" on them. This is
* a convenience method. See also show().
*
* @task convenience
* @param ... One or more nodes to set "display: none" on.
* @return void
*/
hide : function() {
if (__DEV__) {
for (var ii = 0; ii < arguments.length; ++ii) {
if (!arguments[ii]) {
throw new Error(
'JX.DOM.hide(...): ' +
'one or more arguments were null or empty.');
}
}
}
for (var ii = 0; ii < arguments.length; ++ii) {
arguments[ii].style.display = 'none';
}
},
textMetrics : function(node, pseudoclass, x) {
if (!this._metrics[pseudoclass]) {
var n = JX.$N(
'var',
{className: pseudoclass});
this._metrics[pseudoclass] = n;
}
var proxy = this._metrics[pseudoclass];
document.body.appendChild(proxy);
proxy.style.width = x ? (x+'px') : '';
JX.DOM.setContent(
proxy,
JX.HTML(JX.DOM.htmlize(node.value).replace(/\n/g, '<br />')));
var metrics = JX.$V.getDim(proxy);
document.body.removeChild(proxy);
return metrics;
},
/**
* Search the document for DOM nodes by providing a root node to look
* beneath, a tag name, and (optionally) a sigil. Nodes which match all
* specified conditions are returned.
*
* @task query
*
* @param Node Root node to search beneath.
* @param string Tag name, like 'a' or 'textarea'.
* @param string Optionally, a sigil which nodes are required to have.
*
* @return list List of matching nodes, which may be empty.
*/
scry : function(root, tagname, sigil) {
if (__DEV__) {
if (!JX.DOM.isNode(root)) {
throw new Error(
'JX.DOM.scry(<yuck>, ...): '+
'first argument must be a DOM node.');
}
}
var nodes = root.getElementsByTagName(tagname);
if (!sigil) {
return JX.$A(nodes);
}
var result = [];
for (var ii = 0; ii < nodes.length; ii++) {
if (JX.Stratcom.hasSigil(nodes[ii], sigil)) {
result.push(nodes[ii]);
}
}
return result;
},
/**
* Select a node uniquely identified by a root, tagname and sigil. This
* is similar to JX.DOM.scry() but expects exactly one result. It will
* throw JX.$.NotFound if it matches no results.
*
* @task query
*
* @param Node Root node to search beneath.
* @param string Tag name, like 'a' or 'textarea'.
* @param string Optionally, sigil which selected node must have.
*
* @return Node Node uniquely identified by the criteria.
*/
find : function(root, tagname, sigil) {
if (__DEV__) {
if (!JX.DOM.isNode(root)) {
throw new Error(
'JX.DOM.find(<glop>, "'+tagname+'", "'+sigil+'"): '+
'first argument must be a DOM node.');
}
}
var result = JX.DOM.scry(root, tagname, sigil);
if (__DEV__) {
if (result.length > 1) {
throw new Error(
'JX.DOM.find(<node>, "'+tagname+'", "'+sigil+'"): '+
'matched more than one node.');
}
}
if (!result.length) {
throw JX.$.NotFound;
}
return result[0];
},
/**
* Focus a node safely. This is just a convenience wrapper that allows you
* to avoid IE's habit of throwing when nearly any focus operation is
* invoked.
*
* @task convenience
* @param Node Node to move cursor focus to, if possible.
* @return void
*/
focus : function(node) {
try { node.focus(); } catch (lol_ie) {}
},
/**
* Scroll to the position of an element in the document.
* @task view
* @param Node Node to move document scroll position to, if possible.
* @return void
*/
scrollTo : function(node) {
window.scrollTo(0, JX.$V(node).y);
}
}
});
/**
* Simple JSON serializer.
*
* @requires javelin-install javelin-util
* @provides javelin-json
* @javelin
*/
JX.install('JSON', {
statics : {
serialize : function(obj) {
if (__DEV__) {
try {
return JX.JSON._val(obj);
} catch (x) {
JX.log(
'JX.JSON.serialize(...): '+
'caught exception while serializing object. ('+x+')');
}
} else {
return JX.JSON._val(obj);
}
},
_val : function(val) {
var out = [];
if (val === null) {
return 'null';
} else if (val.push && val.pop) {
for (var ii = 0; ii < val.length; ii++) {
if (typeof val[ii] != 'undefined') {
out.push(JX.JSON._val(val[ii]));
}
}
return '['+out.join(',')+']';
} else if (val === true) {
return 'true';
} else if (val === false) {
return 'false';
} else if (typeof val == 'string') {
return JX.JSON._esc(val);
} else if (typeof val == 'number') {
return val;
} else {
for (var k in val) {
out.push(JX.JSON._esc(k)+':'+JX.JSON._val(val[k]));
}
return '{'+out.join(',')+'}';
}
},
_esc : function(str) {
return '"'+str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')+'"';
}
}
});
diff --git a/webroot/rsrc/js/javelin/javelin.min.js b/webroot/rsrc/js/javelin/javelin.min.js
index e199548c52..596bfadc85 100644
--- a/webroot/rsrc/js/javelin/javelin.min.js
+++ b/webroot/rsrc/js/javelin/javelin.min.js
@@ -1,2 +1,3 @@
/** @provides javelin-lib-prod */
-JX.$A=function(b){var c=[];for(var a=0;a<b.length;a++)c.push(b[a]);return c;};JX.$AX=function(a){return (a instanceof Array)?a:[a];};JX.copy=function(a,b){for(var c in b)a[c]=b[c];return a;};JX.bind=function(b,c,d){var a=JX.$A(arguments).slice(2);return function(){return c.apply(b||window,a.concat(JX.$A(arguments)));};};JX.bag=function(){};JX.keys=function(b){var c=[];for(var a in b)c.push(a);return c;};JX.defer=function(a,c){var b=setTimeout(a,c||0);return {stop:function(){clearTimeout(b);}};};JX.go=function(a){JX.Stratcom&&JX.Stratcom.invoke('go',null,{uri:a});(a&&(window.location=a))||window.location.reload(true);};JX.install=function(h,g){if(typeof JX.install._a=='undefined')JX.install._a=0;if(h in JX)return;if(!JX.install._b)JX.install._b=[];JX.install._b.push([h,g]);do{var d;var f=null;for(var c=0;c<JX.install._b.length;++c){d=JX.install._b[c][1];if(d.extend&&!JX[d.extend])continue;f=JX.install._b[c][0];JX.install._b.splice(c,1);--c;JX[f]=(function(m,l){var n=function(){this.__id__='__obj__'+(++JX.install._a);this.__super__=JX[l.extend]||JX.bag;this.__parent__=JX[m].prototype;if(JX[m].__prototyping__)return;return (l.construct||JX.bag).apply(this,arguments);};return n;})(f,d);JX.copy(JX[f],d.statics);JX[f].__prototyping__=0;var k;if(d.extend){JX[d.extend].__prototyping__++;k=JX[f].prototype=new JX[d.extend]();JX[d.extend].__prototyping__--;}else k=JX[f].prototype={};k.__class__=JX[f];for(var e in (d.properties||{})){var b=e.charAt(0).toUpperCase()+e.substr(1);var j='__auto__'+e;k[j]=d.properties[e];k['set'+b]=(function(l){return function(m){this[l]=m;return this;};})(j);k['get'+b]=(function(l){return function(){return this[l];};})(j);}JX.copy(k,d.members);if(d.events&&d.events.length){var i=JX[d.extend]||{};JX[f].__name__='class:'+f;var a=i.__path__||[];JX[f].__path__=a.concat([JX[f].__name__]);k.invoke=function(l){return JX.Stratcom.invoke('obj:'+l,this.__class__.__path__.concat([this.__id__]),{args:JX.$A(arguments).slice(1)});};k.listen=function(m,l){return JX.Stratcom.listen('obj:'+m,this.__id__,JX.bind(this,function(n){return l.apply(this,n.getData().args);}));};JX[f].listen=function(m,l){return JX.Stratcom.listen('obj:'+m,this.__name__,JX.bind(this,function(n){return l.apply(this,n.getData().args);}));};}(d.initialize||JX.bag)();}}while(f);};JX.install('Event',{members:{stop:function(){var a=this.getRawEvent();if(a){a.cancelBubble=true;a.stopPropagation&&a.stopPropagation();}this.setStopped(true);return this;},prevent:function(){var a=this.getRawEvent();if(a){a.returnValue=false;a.preventDefault&&a.preventDefault();}this.setPrevented(true);return this;},kill:function(){this.prevent();this.stop();return this;},getSpecialKey:function(){var b=this.getRawEvent();if(!b||b.shiftKey)return null;var a=b.keyCode;do{a=JX.Event._c[a]||null;}while(a&&JX.Event._c[a]);return a;},getNode:function(a){return this.getNodes()[a]||null;}},statics:{_c:{8:'delete',9:'tab',13:'return',27:'esc',37:'left',38:'up',39:'right',40:'down',63232:38,63233:40,62234:37,62235:39}},properties:{rawEvent:null,type:null,target:null,data:null,path:[],stopped:false,prevented:false,nodes:{}},initialize:function(){}});JX.install('Stratcom',{statics:{ready:false,_d:{},_e:[],_f:{},_g:/\bFN_([^ ]+)/,_h:/\bFD_([^ ]+)_([^ ]+)/,_i:'*',_j:{},_k:[],_l:{focusin:'focus',focusout:'blur'},_m:2,_n:0,invoke:function(d,b,a){var c=new JX.Event().setType(d).setData(a||{}).setPath(b||[]);return this._o(c);},listen:function(k,h,a){var c=[];k=JX.$AX(k);if(!h)h=this._i;if(!(h instanceof Array)){h=[[h]];}else if(!(h[0] instanceof Array))h=[h];for(var d=0;d<k.length;++d){var i=k[d];if(('onpagehide' in window)&&i=='unload')i='pagehide';if(!(i in this._d))this._d[i]={};var j=this._d[i];for(var e=0;e<h.length;++e){var g=h[e];var b=this._e.length;this._e.push(a);this._f[b]=g.length;c.push(b);for(var f=0;f<g.length;++f){if(!j[g[f]])j[g[f]]=[];j[g[f]].push(b);}}}return {remove:function(){for(var l=0;l<c.length;l++)delete JX.Stratcom._e[c[l]];}};},dispatch:function(event){var k;try{k=event.srcElement||event.target;if(k===window||(!k||k.nodeName=='#document'))k={nodeName:'window'};}catch(m){k=null;}var g=[];var f={};var i=function(n,o){if(!(n in f)){f[n]=o;g.push(n);}};var a=k;while(a){i('tag:'+a.nodeName.toLowerCase(),a);var d=a.id;if(d)i('id:'+d,a);var j=a.className||'';var l=((j.baseVal||j).match(this._g)||[])[1];if(l)i(l,a);a=a.parentNode;}var c=event.type;if(c in this._l)c=this._l[c];var b={};for(var e in f)b[e]=this.getData(f[e]);var h=new JX.Event().setRawEvent(event).setType(c).setTarget(k).setData(b).setNodes(f).setPath(g.reverse());return this._o(h);},_o:function(i){var k=this._d[i.getType()];if(!k)return i;var h=i.getPath();var f=h.length;var c={};var g;for(var j=-1;j<f;++j){if(j==-1){g=k[this._i];}else g=k[h[j]];if(!g)continue;for(var d=0;d<g.length;++d)c[g[d]]=(c[g[d]]||0)+1;}var a=[];for(var e in c)if(c[e]==this._f[e]){var b=this._e[e];if(b)a.push(b);}this._k.push({handlers:a,event:i,cursor:0});this.pass();this._k.pop();return i;},pass:function(){var a=this._k[this._k.length-1];while(a.cursor<a.handlers.length){var b=a.cursor;++a.cursor;(a.handlers[b]||JX.bag)(a.event);if(a.event.getStopped())break;}return a.event.getStopped()||a.event.getPrevented();},context:function(){var a=this._k.length;if(!a)return null;return this._k[a-1].event;},mergeData:function(a,b){this._j[a]=b;if(a==0){JX.Stratcom.ready=true;JX.__rawEventQueue({type:'start-queue'});}},sigilize:function(b,c,a){if(a)JX.Stratcom._p(b,a);b.className='FN_'+c+' '+b.className;},hasSigil:function(a,b){if(!a.className)return false;return (a.className.match(this._g)||[])[1]==b;},getData:function(d){var c=(d.className||'').match(this._h);if(c){var a=this._j[c[1]];var b=c[2];if(a&&(b in a))return a[b];}return JX.Stratcom._p(d,{});},allocateMetadataBlock:function(){return this._m++;},_p:function(b,a){if(!this._j[1])this._j[1]={};this._j[1][this._n]=a;b.className='FD_1_'+(this._n++)+' '+b.className;return a;}}});JX.behavior=function(b,a){JX.behavior._q[b]=a;};JX.initBehaviors=function(c){for(var d in c){var a=c[d];if(!a.length)if(d in JX.behavior._r){continue;}else a=[null];for(var b=0;b<a.length;b++)JX.behavior._q[d](a[b]);JX.behavior._r[d]=true;}};!function(a){a.behavior._q={};a.behavior._r={};}(JX);JX.install('Request',{construct:function(b,a){this.setURI(b);if(a)this.listen('done',a);},events:['send','done','error','finally'],members:{_s:null,_t:null,_u:false,_v:null,send:function(){var f=null;try{try{f=new XMLHttpRequest();}catch(e){f=new ActiveXObject("Msxml2.XMLHTTP");}}catch(e){f=new ActiveXObject("Microsoft.XMLHTTP");}this._t=f;this._s=JX.Request._w.length;JX.Request._w.push(this);f.onreadystatechange=JX.bind(this,this._x);var a=this.getData()||{};a.__ajax__=true;this._v=JX.Stratcom.allocateMetadataBlock();a.__metablock__=this._v;var c=(this.getDataSerializer()||JX.Request.defaultDataSerializer)(a);var d=this.getURI();var b=this.getMethod().toUpperCase();if(b=='GET')d+=((d.indexOf('?')===-1)?'?':'&')+c;this.invoke('send',this);if(this.getTimeout())this._y=JX.defer(JX.bind(this,this._z,JX.Request.ERROR_TIMEOUT),this.getTimeout());f.open(b,d,true);if(b=='POST'){if(this.getFile()){f.send(this.getFile());}else{f.setRequestHeader('Content-Type','application/x-www-form-urlencoded');f.send(c);}}else f.send(null);},abort:function(){this._za();},_x:function(){var xport=this._t;try{if(this._u)return;if(xport.readyState!=4)return;if(xport.status<200||xport.status>=300){this._z();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._z();return;}try{if(response.error){this._z(response.error);}else{JX.Stratcom.mergeData(this._v,response.javelin_metadata||{});this._zb(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_z:function(a){this._za();this.invoke('error',a,this);this.invoke('finally');},_zb:function(b){this._za();if(b.onload)for(var a=0;a<b.onload.length;a++)(new Function(b.onload[a]))();this.invoke('done',this.getRaw()?b:b.payload,this);this.invoke('finally');},_za:function(){this._u=true;delete JX.Request._w[this._s];this._y&&this._y.stop();this._t.abort();}},statics:{_w:[],shutdown:function(){for(var a=0;a<JX.Request._w.length;a++)try{JX.Request._w[a]&&JX.Request._w[a].abort();}catch(b){}JX.Request._w=[];},ERROR_TIMEOUT:-9000,defaultDataSerializer:function(a){var c=[];for(var b in a)c.push(encodeURIComponent(b)+'='+encodeURIComponent(a[b]));return c.join('&');}},properties:{URI:null,data:null,dataSerializer:null,method:'POST',file:null,raw:false,timeout:null},initialize:function(){JX.Stratcom.listen('unload','tag:window',JX.Request.shutdown);}});JX.install('$V',{construct:function(a,b){if(this==JX||this==window)return new JX.$V(a,b);if(typeof b=='undefined')return JX.$V.getPos(a);this.x=parseFloat(a);this.y=parseFloat(b);},canCallAsFunction:true,members:{x:null,y:null,setPos:function(a){a.style.left=(this.x===null)?'':(parseInt(this.x,10)+'px');a.style.top=(this.y===null)?'':(parseInt(this.y,10)+'px');return this;},setDim:function(a){a.style.width=(this.x===null)?'':(parseInt(this.x,10)+'px');a.style.height=(this.y===null)?'':(parseInt(this.y,10)+'px');return this;},add:function(a,b){if(a instanceof JX.$V)return this.add(a.x,a.y);return JX.$V(this.x+parseFloat(a),this.y+parseFloat(b));}},statics:{_zc:null,getPos:function(b){JX.Event&&(b instanceof JX.Event)&&(b=b.getRawEvent());if(('pageX' in b)||('clientX' in b)){var a=JX.$V._zc;return JX.$V(b.pageX||(b.clientX+a.scrollLeft),b.pageY||(b.clientY+a.scrollTop));}var c=b.offsetLeft;var d=b.offsetTop;while(b.offsetParent&&(b.offsetParent!=document.body)){b=b.offsetParent;c+=b.offsetLeft;d+=b.offsetTop;}return JX.$V(c,d);},getDim:function(a){return JX.$V(a.offsetWidth,a.offsetHeight);},getScroll:function(){var a=document.body;var b=document.documentElement;return JX.$V(a.scrollLeft||b.scrollLeft,a.scrollTop||b.scrollTop);},getViewport:function(){var a=JX.$V._zc;var b=window;return JX.$V(b.innerWidth||a.clientWidth||0,b.innerHeight||a.clientHeight||0);},getDocument:function(){var a=JX.$V._zc;return JX.$V(a.scrollWidth||0,a.scrollHeight||0);}},initialize:function(){var a=((a=document)&&(a=a.documentElement))||((a=document)&&(a=a.body));JX.$V._zc=a;}});JX.$=function(a){var b=document.getElementById(a);if(!b||(b.id!=a))throw JX.$.NotFound;return b;};JX.$.NotFound={};JX.install('HTML',{construct:function(a){if(this==JX||this==window)return new JX.HTML(a);this._zd=a;},canCallAsFunction:true,members:{_zd:null,getFragment:function(){var b=JX.$N('div');b.innerHTML=this._zd;var a=document.createDocumentFragment();while(b.firstChild)a.appendChild(b.removeChild(b.firstChild));return a;}}});JX.$N=function(d,a,b){if(typeof b=='undefined'&&(typeof a!='object'||a instanceof JX.HTML)){b=a;a={};}var c=document.createElement(d);if(a.style){JX.copy(c.style,a.style);delete a.style;}if(a.sigil){JX.Stratcom.sigilize(c,a.sigil,a.meta);delete a.sigil;delete a.meta;}if(a.className){JX.DOM.alterClass(c,a.className,true);delete a.className;}JX.copy(c,a);if(b)JX.DOM.setContent(c,b);return c;};JX.install('DOM',{statics:{_ze:0,_zf:{},setContent:function(b,a){while(b.firstChild)JX.DOM.remove(b.firstChild);JX.DOM.appendContent(b,a);},prependContent:function(b,a){this._zg(b,a,this._zh);},appendContent:function(b,a){this._zg(b,a,this._zi);},_zh:function(b,a){b.insertBefore(a,b.firstChild);},_zi:function(b,a){b.appendChild(a);},_zg:function(e,b,d){if(b===null||typeof b=='undefined')return;if(b instanceof JX.HTML)b=b.getFragment();if(b instanceof Array){for(var c=0;c<b.length;c++){var a=(typeof b[c]=='string')?document.createTextNode(b[c]):b[c];d(e,a);}}else if(b.nodeType){d(e,b);}else d(e,document.createTextNode(b));},remove:function(a){a.parentNode&&JX.DOM.replace(a,null);return a;},replace:function(b,d){var a;if(b.nextSibling){a=JX.bind(b.nextSibling,function(f,e){f.insertBefore(e,this);});}else a=this._zi;var c=b.parentNode;b.parentNode.removeChild(b);this._zg(c,d,a);return b;},nearest:function(a,b){while(a&&!JX.Stratcom.hasSigil(a,b))a=a.parentNode;return a;},serialize:function(c){var b=c.getElementsByTagName('*');var a={};for(var d=0;d<b.length;++d){if(!b[d].name)continue;var f=b[d].type;var e=b[d].tagName;if((f in {radio:1,checkbox:1}&&b[d].checked)||f in {text:1,hidden:1,password:1}||e in {TEXTAREA:1,SELECT:1})a[b[d].name]=b[d].value;}return a;},isNode:function(a){return !!(a&&a.nodeName&&(a!==window));},isType:function(b,c){b=(''+(b.nodeName||'')).toUpperCase();c=JX.$AX(c);for(var a=0;a<c.length;++a)if(c[a].toUpperCase()==b)return true;return false;},listen:function(b,d,c,a){return JX.Stratcom.listen(d,['id:'+JX.DOM.uniqID(b)].concat(JX.$AX(c||[])),a);},uniqID:function(a){if(!a.id)a.id='autoid_'+(++JX.DOM._ze);return a.id;},alterClass:function(d,b,a){var c=((' '+d.className+' ').indexOf(' '+b+' ')>-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');},show:function(){for(var a=0;a<arguments.length;++a)arguments[a].style.display='';},hide:function(){for(var a=0;a<arguments.length;++a)arguments[a].style.display='none';},textMetrics:function(c,e,f){if(!this._zf[e]){var b=JX.$N('var',{className:e});this._zf[e]=b;}var d=this._zf[e];document.body.appendChild(d);d.style.width=f?(f+'px'):'';JX.DOM.setContent(d,JX.HTML(JX.DOM.htmlize(c.value).replace(/\n/g,'<br />')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a<b.length;a++)if(JX.Stratcom.hasSigil(b[a],e))c.push(b[a]);return c;},find:function(b,d,c){var a=JX.DOM.scry(b,d,c);if(!a.length)throw JX.$.NotFound;return a[0];},focus:function(b){try{b.focus();}catch(a){}},scrollTo:function(a){window.scrollTo(0,JX.$V(a).y);}}});JX.install('JSON',{statics:{serialize:function(a){return JX.JSON._zj(a);},_zj:function(d){var c=[];if(d===null){return 'null';}else if(d.push&&d.pop){for(var a=0;a<d.length;a++)if(typeof d[a]!='undefined')c.push(JX.JSON._zj(d[a]));return '['+c.join(',')+']';}else if(d===true){return 'true';}else if(d===false){return 'false';}else if(typeof d=='string'){return JX.JSON._zk(d);}else if(typeof d=='number'){return d;}else{for(var b in d)c.push(JX.JSON._zk(b)+':'+JX.JSON._zj(d[b]));return '{'+c.join(',')+'}';}},_zk:function(a){return '"'+a.replace(/\\/g,'\\\\').replace(/"/g,'\\"')+'"';}}});
\ No newline at end of file
+
+JX.$A=function(b){var c=[];for(var a=0;a<b.length;a++)c.push(b[a]);return c;};JX.$AX=function(a){return (a instanceof Array)?a:[a];};JX.copy=function(a,b){for(var c in b)a[c]=b[c];return a;};JX.bind=function(b,c,d){var a=JX.$A(arguments).slice(2);return function(){return c.apply(b||window,a.concat(JX.$A(arguments)));};};JX.bag=function(){};JX.keys=function(b){var c=[];for(var a in b)c.push(a);return c;};JX.defer=function(a,c){var b=setTimeout(a,c||0);return {stop:function(){clearTimeout(b);}};};JX.go=function(a){JX.Stratcom&&JX.Stratcom.invoke('go',null,{uri:a});(a&&(window.location=a))||window.location.reload(true);};JX.install=function(h,g){if(typeof JX.install._a=='undefined')JX.install._a=0;if(h in JX)return;if(!JX.install._b)JX.install._b=[];JX.install._b.push([h,g]);do{var d;var f=null;for(var c=0;c<JX.install._b.length;++c){d=JX.install._b[c][1];if(d.extend&&!JX[d.extend])continue;f=JX.install._b[c][0];JX.install._b.splice(c,1);--c;JX[f]=(function(m,l){var n=function(){this.__id__='__obj__'+(++JX.install._a);this.__super__=JX[l.extend]||JX.bag;this.__parent__=JX[m].prototype;if(JX[m].__prototyping__)return;return (l.construct||JX.bag).apply(this,arguments);};return n;})(f,d);JX.copy(JX[f],d.statics);JX[f].__prototyping__=0;var k;if(d.extend){JX[d.extend].__prototyping__++;k=JX[f].prototype=new JX[d.extend]();JX[d.extend].__prototyping__--;}else k=JX[f].prototype={};k.__class__=JX[f];for(var e in (d.properties||{})){var b=e.charAt(0).toUpperCase()+e.substr(1);var j='__auto__'+e;k[j]=d.properties[e];k['set'+b]=(function(l){return function(m){this[l]=m;return this;};})(j);k['get'+b]=(function(l){return function(){return this[l];};})(j);}JX.copy(k,d.members);if(d.events&&d.events.length){var i=JX[d.extend]||{};JX[f].__name__='class:'+f;var a=i.__path__||[];JX[f].__path__=a.concat([JX[f].__name__]);k.invoke=function(l){return JX.Stratcom.invoke('obj:'+l,this.__class__.__path__.concat([this.__id__]),{args:JX.$A(arguments).slice(1)});};k.listen=function(m,l){return JX.Stratcom.listen('obj:'+m,this.__id__,JX.bind(this,function(n){return l.apply(this,n.getData().args);}));};JX[f].listen=function(m,l){return JX.Stratcom.listen('obj:'+m,this.__name__,JX.bind(this,function(n){return l.apply(this,n.getData().args);}));};}(d.initialize||JX.bag)();}}while(f);};JX.install('Event',{members:{stop:function(){var a=this.getRawEvent();if(a){a.cancelBubble=true;a.stopPropagation&&a.stopPropagation();}this.setStopped(true);return this;},prevent:function(){var a=this.getRawEvent();if(a){a.returnValue=false;a.preventDefault&&a.preventDefault();}this.setPrevented(true);return this;},kill:function(){this.prevent();this.stop();return this;},getSpecialKey:function(){var a=this.getRawEvent();if(!a||a.shiftKey)return null;return JX.Event._c[a.keyCode]||null;},getNode:function(a){return this.getNodes()[a]||null;},getNodeData:function(a){return JX.Stratcom.getData(this.getNode(a));}},statics:{_c:{8:'delete',9:'tab',13:'return',27:'esc',37:'left',38:'up',39:'right',40:'down',63232:'up',63233:'down',62234:'left',62235:'right'}},properties:{rawEvent:null,type:null,target:null,data:null,path:[],stopped:false,prevented:false,nodes:{}},initialize:function(){}});JX.install('Stratcom',{statics:{ready:false,_d:{},_e:[],_f:{},_g:'*',_h:{},_i:[],_j:{focusin:'focus',focusout:'blur'},_k:2,_l:0,invoke:function(d,b,a){var c=new JX.Event().setType(d).setData(a||{}).setPath(b||[]);return this._m(c);},listen:function(k,h,a){var c=[];k=JX.$AX(k);if(!h)h=this._g;if(!(h instanceof Array)){h=[[h]];}else if(!(h[0] instanceof Array))h=[h];for(var d=0;d<k.length;++d){var i=k[d];if(('onpagehide' in window)&&i=='unload')i='pagehide';if(!(i in this._d))this._d[i]={};var j=this._d[i];for(var e=0;e<h.length;++e){var g=h[e];var b=this._e.length;this._e.push(a);this._f[b]=g.length;c.push(b);for(var f=0;f<g.length;++f){if(!j[g[f]])j[g[f]]=[];j[g[f]].push(b);}}}return {remove:function(){for(var l=0;l<c.length;l++)delete JX.Stratcom._e[c[l]];}};},dispatch:function(event){var f=[];var e={};var h=function(k,l){if(!e.hasOwnProperty(k)){e[k]=l;f.push(k);}};var j=event.srcElement||event.target;if(!j||!j.getAttribute)j=null;var a=j;while(a&&a.getAttribute){h('tag:'+a.nodeName.toLowerCase(),a);var c=a.id;if(c)h('id:'+c,a);var i=a.getAttribute('data-sigil');if(i){i=i.split(' ');for(var d=0;d<i.length;d++)h(i[d],a);}a=a.parentNode;}var b=event.type;if(b in this._j)b=this._j[b];var g=new JX.Event().setRawEvent(event).setType(b).setTarget(j).setNodes(e).setPath(f.reverse());return this._m(g);},_m:function(i){var k=this._d[i.getType()];if(!k)return i;var h=i.getPath();var f=h.length;var c={};var g;for(var j=-1;j<f;++j){if(j==-1){g=k[this._g];}else g=k[h[j]];if(!g)continue;for(var d=0;d<g.length;++d)c[g[d]]=(c[g[d]]||0)+1;}var a=[];for(var e in c)if(c[e]==this._f[e]){var b=this._e[e];if(b)a.push(b);}this._i.push({handlers:a,event:i,cursor:0});this.pass();this._i.pop();return i;},pass:function(){var a=this._i[this._i.length-1];while(a.cursor<a.handlers.length){var b=a.cursor;++a.cursor;(a.handlers[b]||JX.bag)(a.event);if(a.event.getStopped())break;}return a.event.getStopped()||a.event.getPrevented();},context:function(){var a=this._i.length;if(!a)return null;return this._i[a-1].event;},mergeData:function(a,b){this._h[a]=b;if(a==0){JX.Stratcom.ready=true;JX.__rawEventQueue({type:'start-queue'});}},hasSigil:function(a,b){var c=a.getAttribute('data-sigil');return c&&(' '+c+' ').indexOf(' '+b+' ')>-1;},addSigil:function(a,b){var c=a.getAttribute('data-sigil');if(c&&!JX.Stratcom.hasSigil(a,b))b=c+' '+b;a.setAttribute('data-sigil',b);},getData:function(e){var d=(e.getAttribute('data-meta')||'').split('_');if(d[0]&&d[1]){var a=this._h[d[0]];var c=d[1];if(a&&(c in a))return a[c];}var b={};if(!this._h[1])this._h[1]={};this._h[1][this._l]=b;e.setAttribute('data-meta','1_'+(this._l++));return b;},addData:function(b,a){return JX.copy(JX.Stratcom.getData(b),a);},allocateMetadataBlock:function(){return this._k++;}}});JX.behavior=function(b,a){JX.behavior._n[b]=a;};JX.initBehaviors=function(c){for(var d in c){var a=c[d];if(!a.length)if(JX.behavior._o.hasOwnProperty(d)){continue;}else a=[null];for(var b=0;b<a.length;b++)JX.behavior._n[d](a[b]);JX.behavior._o[d]=true;}};!function(a){a.behavior._n={};a.behavior._o={};}(JX);JX.install('Request',{construct:function(b,a){this.setURI(b);if(a)this.listen('done',a);},events:['send','done','error','finally'],members:{_p:null,_q:null,_r:false,_s:null,send:function(){var f=null;try{try{f=new XMLHttpRequest();}catch(e){f=new ActiveXObject("Msxml2.XMLHTTP");}}catch(e){f=new ActiveXObject("Microsoft.XMLHTTP");}this._q=f;this._p=JX.Request._t.length;JX.Request._t.push(this);f.onreadystatechange=JX.bind(this,this._u);var a=this.getData()||{};a.__ajax__=true;this._s=JX.Stratcom.allocateMetadataBlock();a.__metablock__=this._s;var c=(this.getDataSerializer()||JX.Request.defaultDataSerializer)(a);var d=this.getURI();var b=this.getMethod().toUpperCase();if(b=='GET')d+=((d.indexOf('?')===-1)?'?':'&')+c;this.invoke('send',this);if(this.getTimeout())this._v=JX.defer(JX.bind(this,this._w,JX.Request.ERROR_TIMEOUT),this.getTimeout());f.open(b,d,true);if(b=='POST'){if(this.getFile()){f.send(this.getFile());}else{f.setRequestHeader('Content-Type','application/x-www-form-urlencoded');f.send(c);}}else f.send(null);},abort:function(){this._x();},_u:function(){var xport=this._q;try{if(this._r)return;if(xport.readyState!=4)return;if(xport.status<200||xport.status>=300){this._w();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._w();return;}try{if(response.error){this._w(response.error);}else{JX.Stratcom.mergeData(this._s,response.javelin_metadata||{});this._y(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_w:function(a){this._x();this.invoke('error',a,this);this.invoke('finally');},_y:function(b){this._x();if(b.onload)for(var a=0;a<b.onload.length;a++)(new Function(b.onload[a]))();this.invoke('done',this.getRaw()?b:b.payload,this);this.invoke('finally');},_x:function(){this._r=true;delete JX.Request._t[this._p];this._v&&this._v.stop();this._q.abort();}},statics:{_t:[],shutdown:function(){for(var a=0;a<JX.Request._t.length;a++)try{JX.Request._t[a]&&JX.Request._t[a].abort();}catch(b){}JX.Request._t=[];},ERROR_TIMEOUT:-9000,defaultDataSerializer:function(a){var c=[];for(var b in a)c.push(encodeURIComponent(b)+'='+encodeURIComponent(a[b]));return c.join('&');}},properties:{URI:null,data:null,dataSerializer:null,method:'POST',file:null,raw:false,timeout:null},initialize:function(){JX.Stratcom.listen('unload',null,JX.Request.shutdown);}});JX.install('$V',{construct:function(a,b){if(this==JX||this==window)return new JX.$V(a,b);if(typeof b=='undefined')return JX.$V.getPos(a);this.x=parseFloat(a);this.y=parseFloat(b);},canCallAsFunction:true,members:{x:null,y:null,setPos:function(a){a.style.left=(this.x===null)?'':(parseInt(this.x,10)+'px');a.style.top=(this.y===null)?'':(parseInt(this.y,10)+'px');return this;},setDim:function(a){a.style.width=(this.x===null)?'':(parseInt(this.x,10)+'px');a.style.height=(this.y===null)?'':(parseInt(this.y,10)+'px');return this;},add:function(a,b){if(a instanceof JX.$V)return this.add(a.x,a.y);return JX.$V(this.x+parseFloat(a),this.y+parseFloat(b));}},statics:{_z:null,getPos:function(b){JX.Event&&(b instanceof JX.Event)&&(b=b.getRawEvent());if(('pageX' in b)||('clientX' in b)){var a=JX.$V._z;return JX.$V(b.pageX||(b.clientX+a.scrollLeft),b.pageY||(b.clientY+a.scrollTop));}var c=b.offsetLeft;var d=b.offsetTop;while(b.offsetParent&&(b.offsetParent!=document.body)){b=b.offsetParent;c+=b.offsetLeft;d+=b.offsetTop;}return JX.$V(c,d);},getDim:function(a){return JX.$V(a.offsetWidth,a.offsetHeight);},getScroll:function(){var a=document.body;var b=document.documentElement;return JX.$V(a.scrollLeft||b.scrollLeft,a.scrollTop||b.scrollTop);},getViewport:function(){var a=JX.$V._z;var b=window;return JX.$V(b.innerWidth||a.clientWidth||0,b.innerHeight||a.clientHeight||0);},getDocument:function(){var a=JX.$V._z;return JX.$V(a.scrollWidth||0,a.scrollHeight||0);}},initialize:function(){var a=((a=document)&&(a=a.documentElement))||((a=document)&&(a=a.body));JX.$V._z=a;}});JX.$=function(a){var b=document.getElementById(a);if(!b||(b.id!=a))throw JX.$.NotFound;return b;};JX.$.NotFound={};JX.install('HTML',{construct:function(a){if(this==JX||this==window)return new JX.HTML(a);this._za=a;},canCallAsFunction:true,members:{_za:null,getFragment:function(){var b=JX.$N('div');b.innerHTML=this._za;var a=document.createDocumentFragment();while(b.firstChild)a.appendChild(b.removeChild(b.firstChild));return a;}}});JX.$N=function(d,a,b){if(typeof b=='undefined'&&(typeof a!='object'||a instanceof JX.HTML)){b=a;a={};}var c=document.createElement(d);if(a.style){JX.copy(c.style,a.style);delete a.style;}if(a.sigil){JX.Stratcom.addSigil(c,a.sigil);delete a.sigil;}if(a.meta){JX.Stratcom.addData(c,a.meta);delete a.meta;}JX.copy(c,a);if(b)JX.DOM.setContent(c,b);return c;};JX.install('DOM',{statics:{_zb:0,_zc:{},setContent:function(b,a){while(b.firstChild)JX.DOM.remove(b.firstChild);JX.DOM.appendContent(b,a);},prependContent:function(b,a){this._zd(b,a,this._ze);},appendContent:function(b,a){this._zd(b,a,this._zf);},_ze:function(b,a){b.insertBefore(a,b.firstChild);},_zf:function(b,a){b.appendChild(a);},_zd:function(e,b,d){if(b===null||typeof b=='undefined')return;if(b instanceof JX.HTML)b=b.getFragment();if(b instanceof Array){for(var c=0;c<b.length;c++){var a=(typeof b[c]=='string')?document.createTextNode(b[c]):b[c];d(e,a);}}else if(b.nodeType){d(e,b);}else d(e,document.createTextNode(b));},remove:function(a){a.parentNode&&JX.DOM.replace(a,null);return a;},replace:function(b,d){var a;if(b.nextSibling){a=JX.bind(b.nextSibling,function(f,e){f.insertBefore(e,this);});}else a=this._zf;var c=b.parentNode;b.parentNode.removeChild(b);this._zd(c,d,a);return b;},nearest:function(a,b){while(a&&a.getAttribute&&!JX.Stratcom.hasSigil(a,b))a=a.parentNode;return a;},serialize:function(c){var b=c.getElementsByTagName('*');var a={};for(var d=0;d<b.length;++d){if(!b[d].name)continue;var f=b[d].type;var e=b[d].tagName;if((f in {radio:1,checkbox:1}&&b[d].checked)||f in {text:1,hidden:1,password:1}||e in {TEXTAREA:1,SELECT:1})a[b[d].name]=b[d].value;}return a;},isNode:function(a){return !!(a&&a.nodeName&&(a!==window));},isType:function(b,c){b=(''+(b.nodeName||'')).toUpperCase();c=JX.$AX(c);for(var a=0;a<c.length;++a)if(c[a].toUpperCase()==b)return true;return false;},listen:function(b,d,c,a){return JX.Stratcom.listen(d,['id:'+JX.DOM.uniqID(b)].concat(JX.$AX(c||[])),a);},uniqID:function(a){if(!a.id)a.id='autoid_'+(++JX.DOM._zb);return a.id;},alterClass:function(d,b,a){var c=((' '+d.className+' ').indexOf(' '+b+' ')>-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');},show:function(){for(var a=0;a<arguments.length;++a)arguments[a].style.display='';},hide:function(){for(var a=0;a<arguments.length;++a)arguments[a].style.display='none';},textMetrics:function(c,e,f){if(!this._zc[e]){var b=JX.$N('var',{className:e});this._zc[e]=b;}var d=this._zc[e];document.body.appendChild(d);d.style.width=f?(f+'px'):'';JX.DOM.setContent(d,JX.HTML(JX.DOM.htmlize(c.value).replace(/\n/g,'<br />')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a<b.length;a++)if(JX.Stratcom.hasSigil(b[a],e))c.push(b[a]);return c;},find:function(b,d,c){var a=JX.DOM.scry(b,d,c);if(!a.length)throw JX.$.NotFound;return a[0];},focus:function(b){try{b.focus();}catch(a){}},scrollTo:function(a){window.scrollTo(0,JX.$V(a).y);}}});JX.install('JSON',{statics:{serialize:function(a){return JX.JSON._zg(a);},_zg:function(d){var c=[];if(d===null){return 'null';}else if(d.push&&d.pop){for(var a=0;a<d.length;a++)if(typeof d[a]!='undefined')c.push(JX.JSON._zg(d[a]));return '['+c.join(',')+']';}else if(d===true){return 'true';}else if(d===false){return 'false';}else if(typeof d=='string'){return JX.JSON._zh(d);}else if(typeof d=='number'){return d;}else{for(var b in d)c.push(JX.JSON._zh(b)+':'+JX.JSON._zg(d[b]));return '{'+c.join(',')+'}';}},_zh:function(a){return '"'+a.replace(/\\/g,'\\\\').replace(/"/g,'\\"')+'"';}}});
diff --git a/webroot/rsrc/js/javelin/typeahead.dev.js b/webroot/rsrc/js/javelin/typeahead.dev.js
index df62d80aee..57359297b7 100644
--- a/webroot/rsrc/js/javelin/typeahead.dev.js
+++ b/webroot/rsrc/js/javelin/typeahead.dev.js
@@ -1,1110 +1,1123 @@
/** @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());
+ this._choose(e.getNode('tag:a'));
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));
+ nodes.push(this.createNode(this._raw[hits[kk]]));
}
this._typeahead.showResults(nodes);
},
+
+ createNode : function(data) {
+ return JX.$N(
+ 'a',
+ {
+ href: data.uri,
+ name: data.name,
+ rel: data.id,
+ className: 'jx-result'
+ },
+ data.display
+ );
+ },
+
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
- });
+ var focus = this.buildInput(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);
+ if (e.getNode('remove')) {
+ this._remove(e.getNodeData('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;'));
+ var token = this.buildToken(key, value);
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;
},
+ buildInput: function(value) {
+ return JX.$N('input', {
+ className: 'jx-tokenizer-input',
+ type: 'text',
+ value: value
+ });
+ },
+
+ /**
+ * Generate a token based on a key and value. The "token" and "remove"
+ * sigils are observed by a listener in start().
+ */
+ buildToken: function(key, 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',
+ sigil: 'remove'
+ }, JX.HTML('&times;'));
+
+ return JX.$N('a', {
+ className: 'jx-tokenizer-token',
+ sigil: 'token',
+ meta: {key: key}
+ }, [value, input, remove]);
+ },
+
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
index 07ab5a6f22..8799f11069 100644
--- a/webroot/rsrc/js/javelin/typeahead.min.js
+++ b/webroot/rsrc/js/javelin/typeahead.min.js
@@ -1,3 +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);});}}});
+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.getNode('tag:a'));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(q){var h={};var i={};var k={};var o={};var p=this.tokenize(q);p.sort(function(r,s){return s.length-r.length;});for(var c=0;c<p.length;++c){if(p[c] in o){p.splice(c--,1);continue;}o[p[c]]=true;var a=p[c];for(var m in this._o)if(m.substr(0,a.length)===a){if(!(m in k)){k[m]=true;}else continue;var g=this._o[m];for(var d=0;d<g.length;++d){var j=g[d];if(!i[j])i[j]={};if(!(a in i[j])){i[j][a]=true;h[j]=(h[j]||0)+1;}}}}var b=[];for(var e in h)if(h[e]==p.length)b.push(e);var l=Math.min(this.getMaximumResultCount(),b.length);var n=[];for(var f=0;f<l;f++)n.push(this.createNode(this._n[b[f]]));this._p.showResults(n);},createNode:function(a){return JX.$N('a',{href:a.uri,name:a.name,rel:a.id,className:'jx-result'},a.display);},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=this.buildInput(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.getNode('remove')){this._za(d.getNodeData('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(b,e){if(b in this._w)return false;var a=this._i;var c=this._c;var d=this.buildToken(b,e);this._w[b]={value:e,key:b,node:d};this._v.push(b);c.insertBefore(d,a);return true;},buildInput:function(a){return JX.$N('input',{className:'jx-tokenizer-input',type:'text',value:a});},buildToken:function(b,d){var a=JX.$N('input',{type:'hidden',value:b,name:this._t.name+'['+(this._y++)+']'});var c=JX.$N('a',{className:'jx-tokenizer-x',sigil:'remove'},JX.HTML('&times;'));return JX.$N('a',{className:'jx-tokenizer-token',sigil:'token',meta:{key:b}},[d,a,c]);},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);});}}});
diff --git a/webroot/rsrc/js/javelin/workflow.dev.js b/webroot/rsrc/js/javelin/workflow.dev.js
new file mode 100644
index 0000000000..27fc83a262
--- /dev/null
+++ b/webroot/rsrc/js/javelin/workflow.dev.js
@@ -0,0 +1,239 @@
+/** @provides javelin-workflow-dev */
+
+/**
+ * @requires javelin-install javelin-vector javelin-dom
+ * @provides javelin-mask
+ * @javelin
+ */
+
+/**
+ * Show a transparent "mask" over the page; used by Workflow to draw visual
+ * attention to modal dialogs.
+ */
+JX.install('Mask', {
+ statics : {
+ _depth : 0,
+ _mask : null,
+ show : function() {
+ if (!JX.Mask._depth) {
+ JX.Mask._mask = JX.$N('div', {className: 'jx-mask'});
+ document.body.appendChild(JX.Mask._mask);
+ JX.$V.getDocument().setDim(JX.Mask._mask);
+ }
+ ++JX.Mask._depth;
+ },
+ hide : function() {
+ --JX.Mask._depth;
+ if (!JX.Mask._depth) {
+ JX.DOM.remove(JX.Mask._mask);
+ JX.Mask._mask = null;
+ }
+ }
+ }
+});
+/**
+ * @requires javelin-stratcom
+ * javelin-request
+ * javelin-dom
+ * javelin-vector
+ * javelin-install
+ * javelin-util
+ * javelin-mask
+ * @provides javelin-workflow
+ * @javelin
+ */
+
+JX.install('Workflow', {
+ construct : function(uri, data) {
+ if (__DEV__) {
+ if (!uri || uri == '#') {
+ throw new Error(
+ 'new JX.Workflow(<?>, ...): '+
+ 'bogus URI provided when creating workflow.');
+ }
+ }
+ this.setURI(uri);
+ this.setData(data || {});
+ },
+
+ events : ['error', 'finally', 'submit'],
+
+ statics : {
+ _stack : [],
+ newFromForm : function(form, data) {
+ var inputs = [].concat(
+ JX.DOM.scry(form, 'input'),
+ JX.DOM.scry(form, 'button'),
+ JX.DOM.scry(form, 'textarea'));
+
+ for (var ii = 0; ii < inputs.length; ii++) {
+ if (inputs[ii].disabled) {
+ delete inputs[ii];
+ } else {
+ inputs[ii].disabled = true;
+ }
+ }
+
+ var workflow = new JX.Workflow(
+ form.getAttribute('action'),
+ JX.copy(data || {}, JX.DOM.serialize(form)));
+ workflow.setMethod(form.getAttribute('method'));
+ workflow.listen('finally', function() {
+ for (var ii = 0; ii < inputs.length; ii++) {
+ inputs[ii] && (inputs[ii].disabled = false);
+ }
+ });
+ return workflow;
+ },
+ newFromLink : function(link) {
+ var workflow = new JX.Workflow(link.href);
+ return workflow;
+ },
+ _push : function(workflow) {
+ JX.Mask.show();
+ JX.Workflow._stack.push(workflow);
+ },
+ _pop : function() {
+ var dialog = JX.Workflow._stack.pop();
+ (dialog.getCloseHandler() || JX.bag)();
+ dialog._destroy();
+ JX.Mask.hide();
+ },
+ disable : function() {
+ JX.Workflow._disabled = true;
+ },
+ _onbutton : function(event) {
+
+ if (JX.Stratcom.pass()) {
+ return;
+ }
+
+ if (JX.Workflow._disabled) {
+ return;
+ }
+ var t = event.getTarget();
+ if (t.name == '__cancel__' || t.name == '__close__') {
+ JX.Workflow._pop();
+ } else {
+
+ var form = event.getNode('jx-dialog');
+ var data = JX.DOM.serialize(form);
+ data[t.name] = true;
+ data.__wflow__ = true;
+
+ var active = JX.Workflow._stack[JX.Workflow._stack.length - 1];
+ var e = active.invoke('submit', {form: form, data: data});
+ if (!e.getStopped()) {
+ active._destroy();
+ active
+ .setURI(form.getAttribute('action') || active.getURI())
+ .setData(data)
+ .start();
+ }
+ }
+ event.prevent();
+ }
+ },
+
+ members : {
+ _root : null,
+ _pushed : false,
+ _onload : function(r) {
+ // It is permissible to send back a falsey redirect to force a page
+ // reload, so we need to take this branch if the key is present.
+ if (r && (typeof r.redirect != 'undefined')) {
+ JX.go(r.redirect, true);
+ } else if (r && r.dialog) {
+ this._push();
+ this._root = JX.$N(
+ 'div',
+ {className: 'jx-client-dialog'},
+ JX.HTML(r.dialog));
+ JX.DOM.listen(
+ this._root,
+ 'click',
+ 'tag:button',
+ JX.Workflow._onbutton);
+ document.body.appendChild(this._root);
+ var d = JX.$V.getDim(this._root);
+ var v = JX.$V.getViewport();
+ var s = JX.$V.getScroll();
+ JX.$V((v.x - d.x) / 2, s.y + 100).setPos(this._root);
+ try {
+ JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__'));
+ var inputs = JX.DOM.scry(this._root, 'input')
+ .concat(JX.DOM.scry(this._root, 'textarea'));
+ var miny = Number.POSITIVE_INFINITY;
+ var target = null;
+ for (var ii = 0; ii < inputs.length; ++ii) {
+ if (inputs[ii].type != 'hidden') {
+ // Find the topleft-most displayed element.
+ var p = JX.$V(inputs[ii]);
+ if (p.y < miny) {
+ miny = p.y;
+ target = inputs[ii];
+ }
+ }
+ }
+ target && JX.DOM.focus(target);
+ } catch (_ignored) {}
+ } else if (this.getHandler()) {
+ this.getHandler()(r);
+ this._pop();
+ } else if (r) {
+ if (__DEV__) {
+ throw new Error('Response to workflow request went unhandled.');
+ }
+ }
+ },
+ _push : function() {
+ if (!this._pushed) {
+ this._pushed = true;
+ JX.Workflow._push(this);
+ }
+ },
+ _pop : function() {
+ if (this._pushed) {
+ this._pushed = false;
+ JX.Workflow._pop();
+ }
+ },
+ _destroy : function() {
+ if (this._root) {
+ JX.DOM.remove(this._root);
+ this._root = null;
+ }
+ },
+ start : function() {
+ var uri = this.getURI();
+ var method = this.getMethod();
+ var r = new JX.Request(uri, JX.bind(this, this._onload));
+ r.setData(this.getData());
+ r.setDataSerializer(this.getDataSerializer());
+ if (method) {
+ r.setMethod(method);
+ }
+ r.listen('finally', JX.bind(this, this.invoke, 'finally'));
+ r.listen('error', JX.bind(this, function(error) {
+ var e = this.invoke('error', error);
+ if (e.getStopped()) {
+ return;
+ }
+ // TODO: Default error behavior? On Facebook Lite, we just shipped the
+ // user to "/error/". We could emit a blanket 'workflow-failed' type
+ // event instead.
+ }));
+ r.send();
+ }
+ },
+
+ properties : {
+ handler : null,
+ closeHandler : null,
+ data : null,
+ dataSerializer : null,
+ method : null,
+ URI : null
+ }
+
+});
diff --git a/webroot/rsrc/js/javelin/workflow.min.js b/webroot/rsrc/js/javelin/workflow.min.js
new file mode 100644
index 0000000000..33b51ba0df
--- /dev/null
+++ b/webroot/rsrc/js/javelin/workflow.min.js
@@ -0,0 +1,3 @@
+/** @provides javelin-workflow-prod */
+
+JX.install('Mask',{statics:{_a:0,_b:null,show:function(){if(!JX.Mask._a){JX.Mask._b=JX.$N('div',{className:'jx-mask'});document.body.appendChild(JX.Mask._b);JX.$V.getDocument().setDim(JX.Mask._b);}++JX.Mask._a;},hide:function(){--JX.Mask._a;if(!JX.Mask._a){JX.DOM.remove(JX.Mask._b);JX.Mask._b=null;}}}});JX.install('Workflow',{construct:function(b,a){this.setURI(b);this.setData(a||{});},events:['error','finally','submit'],statics:{_c:[],newFromForm:function(b,a){var d=[].concat(JX.DOM.scry(b,'input'),JX.DOM.scry(b,'button'),JX.DOM.scry(b,'textarea'));for(var c=0;c<d.length;c++)if(d[c].disabled){delete d[c];}else d[c].disabled=true;var e=new JX.Workflow(b.getAttribute('action'),JX.copy(a||{},JX.DOM.serialize(b)));e.setMethod(b.getAttribute('method'));e.listen('finally',function(){for(var f=0;f<d.length;f++)d[f]&&(d[f].disabled=false);});return e;},newFromLink:function(a){var b=new JX.Workflow(a.href);return b;},_d:function(a){JX.Mask.show();JX.Workflow._c.push(a);},_e:function(){var a=JX.Workflow._c.pop();(a.getCloseHandler()||JX.bag)();a._f();JX.Mask.hide();},disable:function(){JX.Workflow._g=true;},_h:function(event){if(JX.Stratcom.pass())return;if(JX.Workflow._g)return;var e=event.getTarget();if(e.name=='__cancel__'||e.name=='__close__'){JX.Workflow._e();}else{var d=event.getNode('jx-dialog');var b=JX.DOM.serialize(d);b[e.name]=true;b.__wflow__=true;var a=JX.Workflow._c[JX.Workflow._c.length-1];var c=a.invoke('submit',{form:d,data:b});if(!c.getStopped()){a._f();a.setURI(d.getAttribute('action')||a.getURI()).setData(b).start();}}event.prevent();}},members:{_i:null,_j:false,_k:function(c){if(c&&(typeof c.redirect!='undefined')){JX.go(c.redirect,true);}else if(c&&c.dialog){this._d();this._i=JX.$N('div',{className:'jx-client-dialog'},JX.HTML(c.dialog));JX.DOM.listen(this._i,'click','tag:button',JX.Workflow._h);document.body.appendChild(this._i);var b=JX.$V.getDim(this._i);var e=JX.$V.getViewport();var d=JX.$V.getScroll();JX.$V((e.x-b.x)/2,d.y+100).setPos(this._i);try{JX.DOM.focus(JX.DOM.find(this._i,'button','__default__'));var inputs=JX.DOM.scry(this._i,'input').concat(JX.DOM.scry(this._i,'textarea'));var miny=Number.POSITIVE_INFINITY;var target=null;for(var ii=0;ii<inputs.length;++ii)if(inputs[ii].type!='hidden'){var p=JX.$V(inputs[ii]);if(p.y<miny){miny=p.y;target=inputs[ii];}}target&&JX.DOM.focus(target);}catch(a){}}else if(this.getHandler()){this.getHandler()(c);this._e();}},_d:function(){if(!this._j){this._j=true;JX.Workflow._d(this);}},_e:function(){if(this._j){this._j=false;JX.Workflow._e();}},_f:function(){if(this._i){JX.DOM.remove(this._i);this._i=null;}},start:function(){var c=this.getURI();var a=this.getMethod();var b=new JX.Request(c,JX.bind(this,this._k));b.setData(this.getData());b.setDataSerializer(this.getDataSerializer());if(a)b.setMethod(a);b.listen('finally',JX.bind(this,this.invoke,'finally'));b.listen('error',JX.bind(this,function(e){var d=this.invoke('error',e);if(d.getStopped())return;}));b.send();}},properties:{handler:null,closeHandler:null,data:null,dataSerializer:null,method:null,URI:null}});

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 3:14 PM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
187037
Default Alt Text
(323 KB)

Event Timeline