Page MenuHomePhorge

No OneTemporary

diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php
index d50c0b928e..927ceb8589 100644
--- a/src/__celerity_resource_map__.php
+++ b/src/__celerity_resource_map__.php
@@ -1,397 +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/d98e6292/rsrc/css/aphront/dialog-view.css',
+ 'uri' => '/res/a05107ae/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',
+ 'uri' => '/res/11e7232a/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',
+ 'uri' => '/res/f66cec41/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',
+ 'uri' => '/res/be5ed33e/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-magical-init' =>
array(
'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/ce6bff38/rsrc/js/javelin/init.min.js',
+ 'uri' => '/res/1267c868/rsrc/js/javelin/init.min.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/init.min.js',
),
'javelin-lib-dev' =>
array(
- 'uri' => '/res/29c9b6b4/rsrc/js/javelin/javelin.dev.js',
+ 'uri' => '/res/53784c9a/rsrc/js/javelin/javelin.dev.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/javelin.dev.js',
),
'javelin-lib-prod' =>
array(
- 'uri' => '/res/ef13c830/rsrc/js/javelin/javelin.min.js',
+ 'uri' => '/res/2f2b3b2e/rsrc/js/javelin/javelin.min.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/javelin/javelin.min.js',
),
'javelin-typeahead-dev' =>
array(
'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/593d9bb8/rsrc/js/javelin/typeahead.min.js',
+ 'uri' => '/res/69d5fad1/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',
+ 'uri' => '/res/7e690e16/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 (
- 'dc2390af' =>
+ '4bb7e37f' =>
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/dc2390af/core.pkg.css',
+ 'uri' => '/res/pkg/4bb7e37f/core.pkg.css',
'type' => 'css',
),
- '69b11588' =>
+ 'f399aad7' =>
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',
+ 'uri' => '/res/pkg/f399aad7/differential.pkg.css',
'type' => 'css',
),
),
'reverse' =>
array (
- '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',
+ 'phabricator-core-css' => '4bb7e37f',
+ 'phabricator-core-buttons-css' => '4bb7e37f',
+ 'phabricator-standard-page-view' => '4bb7e37f',
+ 'aphront-dialog-view-css' => '4bb7e37f',
+ 'aphront-form-view-css' => '4bb7e37f',
+ 'aphront-panel-view-css' => '4bb7e37f',
+ 'aphront-side-nav-view-css' => '4bb7e37f',
+ 'aphront-table-view-css' => '4bb7e37f',
+ 'aphront-tokenizer-control-css' => '4bb7e37f',
+ 'aphront-typeahead-control-css' => '4bb7e37f',
+ 'phabricator-directory-css' => '4bb7e37f',
+ 'differential-core-view-css' => 'f399aad7',
+ 'differential-changeset-view-css' => 'f399aad7',
+ 'differential-revision-detail-css' => 'f399aad7',
+ 'differential-revision-history-css' => 'f399aad7',
+ 'differential-table-of-contents-css' => 'f399aad7',
),
));
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index b40da8afca..ed66f1dc3e 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,346 +1,350 @@
<?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',
+ 'DifferentialInlineCommentEditController' => 'applications/differential/controller/inlinecommentedit',
+ 'DifferentialInlineCommentView' => 'applications/differential/view/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',
+ 'DifferentialInlineCommentEditController' => 'DifferentialController',
+ 'DifferentialInlineCommentView' => 'AphrontView',
'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/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
index 85367d98f0..748e384c30 100644
--- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
@@ -1,195 +1,200 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group aphront
*/
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
public function getApplicationName() {
return 'aphront-default';
}
public function getURIMap() {
return array(
'/repository/' => array(
'$' => 'RepositoryListController',
'new/$' => 'RepositoryEditController',
'edit/(?<id>\d+)/$' => 'RepositoryEditController',
'delete/(?<id>\d+)/$' => 'RepositoryDeleteController',
),
'/' => array(
'$' => 'PhabricatorDirectoryMainController',
),
'/directory/' => array(
'item/$'
=> 'PhabricatorDirectoryItemListController',
'item/edit/(?:(?<id>\d+)/)?$'
=> 'PhabricatorDirectoryItemEditController',
'item/delete/(?<id>\d+)/'
=> 'PhabricatorDirectoryItemDeleteController',
'category/$'
=> 'PhabricatorDirectoryCategoryListController',
'category/edit/(?:(?<id>\d+)/)?$'
=> 'PhabricatorDirectoryCategoryEditController',
'category/delete/(?<id>\d+)/'
=> 'PhabricatorDirectoryCategoryDeleteController',
),
'/file/' => array(
'$' => 'PhabricatorFileListController',
'upload/$' => 'PhabricatorFileUploadController',
'(?<view>info)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
'(?<view>view)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
'(?<view>download)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
),
'/phid/' => array(
'$' => 'PhabricatorPHIDLookupController',
'list/$' => 'PhabricatorPHIDListController',
'type/$' => 'PhabricatorPHIDTypeListController',
'type/edit/(?:(?<id>\d+)/)?$' => 'PhabricatorPHIDTypeEditController',
'new/$' => 'PhabricatorPHIDAllocateController',
),
'/people/' => array(
'$' => 'PhabricatorPeopleListController',
'edit/(?:(?<username>\w+)/)?$' => 'PhabricatorPeopleEditController',
),
'/p/(?<username>\w+)/$' => 'PhabricatorPeopleProfileController',
'/conduit/' => array(
'$' => 'PhabricatorConduitConsoleController',
'method/(?<method>[^/]+)$' => 'PhabricatorConduitConsoleController',
'log/$' => 'PhabricatorConduitLogController',
),
'/api/(?<method>[^/]+)$' => 'PhabricatorConduitAPIController',
'/D(?<id>\d+)' => 'DifferentialRevisionViewController',
'/differential/' => array(
'$' => 'DifferentialRevisionListController',
'filter/(?<filter>\w+)/$' => 'DifferentialRevisionListController',
'diff/(?<id>\d+)/$' => 'DifferentialDiffViewController',
'changeset/$' => 'DifferentialChangesetViewController',
'revision/edit/(?:(?<id>\d+)/)?$'
=> 'DifferentialRevisionEditController',
'comment/' => array(
'preview/(?<id>\d+)/$' => 'DifferentialCommentPreviewController',
'save/$' => 'DifferentialCommentSaveController',
'inline/' => array(
'preview/$' => 'DifferentialInlineCommentPreviewController',
- 'edit/$' => 'DifferentialInlineCommentEditController',
+ 'edit/(?<id>\d+)/$' => 'DifferentialInlineCommentEditController',
),
),
),
'/res/' => array(
'(?<package>pkg/)?(?<hash>[a-f0-9]{8})/(?<path>.+\.(?:css|js))$'
=> 'CelerityResourceController',
),
'/typeahead/' => array(
'common/(?<type>\w+)/$'
=> 'PhabricatorTypeaheadCommonDatasourceController',
),
'/mail/' => array(
'$' => 'PhabricatorMetaMTAListController',
'send/$' => 'PhabricatorMetaMTASendController',
'view/(?<id>\d+)/$' => 'PhabricatorMetaMTAViewController',
'lists/$' => 'PhabricatorMetaMTAMailingListsController',
'lists/edit/(?:(?<id>\d+)/)?$'
=> 'PhabricatorMetaMTAMailingListEditController',
),
'/login/' => array(
'$' => 'PhabricatorLoginController',
'email/$' => 'PhabricatorEmailLoginController',
'etoken/(?<token>\w+)/$' => 'PhabricatorEmailTokenController',
),
'/logout/$' => 'PhabricatorLogoutController',
'/facebook-auth/' => array(
'$' => 'PhabricatorFacebookAuthController',
'diagnose/$' => 'PhabricatorFacebookAuthDiagnosticsController',
),
);
}
public function buildRequest() {
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($_GET + $_POST);
return $request;
}
public function handleException(Exception $ex) {
$class = phutil_escape_html(get_class($ex));
$message = phutil_escape_html($ex->getMessage());
$content =
'<div class="aphront-unhandled-exception">'.
'<h1>Unhandled Exception "'.$class.'": '.$message.'</h1>'.
'<code>'.phutil_escape_html((string)$ex).'</code>'.
'</div>';
$view = new PhabricatorStandardPageView();
$view->appendChild($content);
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
}
public function willSendResponse(AphrontResponse $response) {
$request = $this->getRequest();
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax()) {
$view = new PhabricatorStandardPageView();
$view->appendChild(
'<div style="padding: 2em 0;">'.
$response->buildResponseString().
'</div>');
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
+ } else {
+ return id(new AphrontAjaxResponse())
+ ->setContent(array(
+ 'dialog' => $response->buildResponseString(),
+ ));
}
} else if ($response instanceof Aphront404Response) {
$failure = new AphrontRequestFailureView();
$failure->setHeader('404 Not Found');
$failure->appendChild(
'<p>The page you requested was not found.</p>');
$view = new PhabricatorStandardPageView();
$view->setTitle('404 Not Found');
$view->appendChild($failure);
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
$response->setHTTPResponseCode(404);
return $response;
}
return $response;
}
public function build404Controller() {
return array(new Phabricator404Controller($this->getRequest()), array());
}
}
diff --git a/src/aphront/default/configuration/__init__.php b/src/aphront/default/configuration/__init__.php
index ea6f86450a..0da64e03d9 100644
--- a/src/aphront/default/configuration/__init__.php
+++ b/src/aphront/default/configuration/__init__.php
@@ -1,19 +1,21 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/applicationconfiguration');
phutil_require_module('phabricator', 'aphront/request');
+phutil_require_module('phabricator', 'aphront/response/ajax');
phutil_require_module('phabricator', 'aphront/response/webpage');
phutil_require_module('phabricator', 'applications/base/controller/404');
phutil_require_module('phabricator', 'view/page/failure');
phutil_require_module('phabricator', 'view/page/standard');
phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
phutil_require_source('AphrontDefaultApplicationConfiguration.php');
diff --git a/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php b/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php
index f8d7cab6c2..d10e5789f4 100644
--- a/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php
@@ -1,88 +1,113 @@
<?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 DifferentialChangesetViewController extends DifferentialController {
public function processRequest() {
$request = $this->getRequest();
$id = $request->getStr('id');
+ $author_phid = $request->getUser()->getPHID();
$changeset = id(new DifferentialChangeset())->load($id);
if (!$changeset) {
return new Aphront404Response();
}
$changeset->attachHunks($changeset->loadHunks());
$range_s = null;
$range_e = null;
$mask = array();
$range = $request->getStr('range');
if ($range) {
$match = null;
if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $range, $match)) {
$range_s = (int)$match[1];
$range_e = (int)$match[2];
if (count($match) > 3) {
$start = (int)$match[3];
$len = (int)$match[4];
for ($ii = $start; $ii < $start + $len; $ii++) {
$mask[$ii] = true;
}
}
}
}
$parser = new DifferentialChangesetParser();
$parser->setChangeset($changeset);
+
+ $phids = array();
+ $inlines = $this->loadInlineComments($id, $author_phid);
+ foreach ($inlines as $inline) {
+ $parser->parseInlineComment($inline);
+ $phids[$inline->getAuthorPHID()] = true;
+ }
+ $phids = array_keys($phids);
+
+ $handles = id(new PhabricatorObjectHandleData($phids))
+ ->loadHandles();
+ $parser->setHandles($handles);
+
+ $factory = new DifferentialMarkupEngineFactory();
+ $engine = $factory->newDifferentialCommentMarkupEngine();
+ $parser->setMarkupEngine($engine);
$output = $parser->render(null, $range_s, $range_e, $mask);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent($output);
}
Javelin::initBehavior('differential-show-more', array(
'uri' => '/differential/changeset/',
));
$detail = new DifferentialChangesetDetailView();
$detail->setChangeset($changeset);
$detail->appendChild($output);
$output =
'<div class="differential-primary-pane">'.
'<div class="differential-review-stage">'.
$detail->render().
'</div>'.
'</div>';
return $this->buildStandardPageResponse(
array(
$output
),
array(
'title' => 'Changeset View',
));
}
+ private function loadInlineComments($changeset_id, $author_phid) {
+ return id(new DifferentialInlineComment())->loadAllWhere(
+ 'changesetID = %d AND (commentID IS NOT NULL OR authorPHID = %s)',
+ $changeset_id,
+ $author_phid);
+ }
+
+
}
diff --git a/src/applications/differential/controller/inlinecommentedit/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/inlinecommentedit/DifferentialInlineCommentEditController.php
new file mode 100644
index 0000000000..674db18520
--- /dev/null
+++ b/src/applications/differential/controller/inlinecommentedit/DifferentialInlineCommentEditController.php
@@ -0,0 +1,116 @@
+<?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 DifferentialInlineCommentEditController extends DifferentialController {
+
+ private $revisionID;
+
+ public function willProcessRequest(array $data) {
+ $this->revisionID = $data['id'];
+ }
+
+ public function processRequest() {
+ $request = $this->getRequest();
+
+ $changeset = $request->getInt('changeset');
+ $is_new = $request->getInt('is_new');
+ $on_right = $request->getInt('on_right');
+ $number = $request->getInt('number');
+ $length = $request->getInt('length');
+ $text = $request->getStr('text');
+ $op = $request->getStr('op');
+
+ $user = $request->getUser();
+
+ $submit_uri = '/differential/comment/inline/edit/'.$this->revisionID.'/';
+
+ switch ($op) {
+ case 'delete':
+ if ($request->isFormPost()) {
+ // do the delete;
+ return new AphrontAjaxResponse();
+ }
+
+ $dialog = new AphrontDialogView();
+ $dialog->setTitle('Really delete this comment?');
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ case 'edit':
+ $dialog = new AphrontDialogView();
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ case 'create':
+
+ if (!$request->isFormPost() || !strlen($text)) {
+ return new AphrontAjaxResponse();
+ }
+
+ $factory = new DifferentialMarkupEngineFactory();
+ $engine = $factory->newDifferentialCommentMarkupEngine();
+
+ $phids = array($user->getPHID());
+
+ $handles = id(new PhabricatorObjectHandleData($phids))
+ ->loadHandles();
+
+ $inline = id(new DifferentialInlineComment())
+ ->setRevisionID($this->revisionID)
+ ->setChangesetID($changeset)
+ ->setCommentID(null)
+ ->setAuthorPHID($user->getPHID())
+ ->setLineNumber($number)
+ ->setLineLength($length)
+ ->setIsNewFile($is_new)
+ ->setContent($text)
+ ->save();
+
+ $view = new DifferentialInlineCommentView();
+ $view->setInlineComment($inline);
+ $view->setOnRight($on_right);
+ $view->setBuildScaffolding(true);
+ $view->setMarkupEngine($engine);
+ $view->setHandles($handles);
+
+ return id(new AphrontAjaxResponse())
+ ->setContent(
+ array(
+ 'inlineCommentID' => $inline->getID(),
+ 'markup' => $view->render(),
+ ));
+ default:
+ $dialog = new AphrontDialogView();
+ $dialog->setUser($user);
+ $dialog->setTitle('New Inline Comment');
+ $dialog->setSubmitURI($submit_uri);
+
+ $dialog->addHiddenInput('op', 'create');
+ $dialog->addHiddenInput('changeset', $changeset);
+ $dialog->addHiddenInput('is_new', $is_new);
+ $dialog->addHiddenInput('on_right', $on_right);
+ $dialog->addHiddenInput('number', $number);
+ $dialog->addHiddenInput('length', $length);
+
+ $dialog->addSubmitButton();
+ $dialog->addCancelButton('#');
+ $dialog->appendChild('<textarea name="text"></textarea>');
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+ }
+
+}
diff --git a/src/applications/differential/controller/inlinecommentedit/__init__.php b/src/applications/differential/controller/inlinecommentedit/__init__.php
new file mode 100644
index 0000000000..59e55ef5fc
--- /dev/null
+++ b/src/applications/differential/controller/inlinecommentedit/__init__.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'aphront/response/ajax');
+phutil_require_module('phabricator', 'aphront/response/dialog');
+phutil_require_module('phabricator', 'applications/differential/controller/base');
+phutil_require_module('phabricator', 'applications/differential/storage/inlinecomment');
+phutil_require_module('phabricator', 'applications/differential/view/inlinecomment');
+phutil_require_module('phabricator', 'view/dialog');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('DifferentialInlineCommentEditController.php');
diff --git a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php
index ba34fd0ffd..edbe717a72 100644
--- a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php
@@ -1,1332 +1,1334 @@
<?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 DifferentialChangesetParser {
protected $visible = array();
protected $new = array();
protected $old = array();
protected $intra = array();
protected $newRender = null;
protected $oldRender = null;
protected $parsedHunk = false;
protected $filename = null;
protected $filetype = null;
protected $changesetID = null;
protected $missingOld = array();
protected $missingNew = array();
protected $comments = array();
protected $specialAttributes = array();
protected $changeset;
protected $whitespaceMode = null;
protected $subparser;
protected $oldChangesetID = null;
protected $noHighlight;
+
+ private $handles;
const CACHE_VERSION = 4;
const ATTR_GENERATED = 'attr:generated';
const ATTR_DELETED = 'attr:deleted';
const ATTR_UNCHANGED = 'attr:unchanged';
const ATTR_WHITELINES = 'attr:white';
const LINES_CONTEXT = 8;
const WHITESPACE_SHOW_ALL = 'show-all';
const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing';
const WHITESPACE_IGNORE_ALL = 'ignore-all';
public function setChangeset($changeset) {
$this->changeset = $changeset;
$this->setFilename($changeset->getFilename());
$this->setChangesetID($changeset->getID());
return $this;
}
public function setWhitespaceMode($whitespace_mode) {
$this->whitespaceMode = $whitespace_mode;
return $this;
}
public function setOldChangesetID($old_changeset_id) {
$this->oldChangesetID = $old_changeset_id;
return $this;
}
public function setChangesetID($changeset_id) {
$this->changesetID = $changeset_id;
return $this;
}
public function getChangesetID() {
return $this->changesetID;
}
public function setFilename($filename) {
$this->filename = $filename;
if (strpos($filename, '.', 1) !== false) {
$this->filetype = end(explode('.', $filename));
}
}
+
+ public function setHandles(array $handles) {
+ $this->handles = $handles;
+ return $this;
+ }
+
+ public function setMarkupEngine(PhutilMarkupEngine $engine) {
+ $this->markupEngine = $engine;
+ return $this;
+ }
public function parseHunk(DifferentialHunk $hunk) {
$this->parsedHunk = true;
$lines = $hunk->getChanges();
// Flatten UTF-8 into "\0". We don't support UTF-8 because the diffing
// algorithms are byte-oriented (not character oriented) and everyone seems
// to be in agreement that it's fairly reasonable not to allow UTF-8 in
// source files. These bytes will later be replaced with a "?" glyph, but
// in the meantime we replace them with "\0" since Pygments is happy to
// deal with that.
$lines = preg_replace('/[\x80-\xFF]/', "\0", $lines);
$lines = str_replace(
array("\t", "\r\n", "\r"),
array(' ', "\n", "\n"),
$lines);
$lines = explode("\n", $lines);
$types = array();
foreach ($lines as $line_index => $line) {
$lines[$line_index] = $line;
if (isset($line[0])) {
$char = $line[0];
if ($char == ' ') {
$types[$line_index] = null;
} else if ($char == '\\' && $line_index > 0) {
$types[$line_index] = $types[$line_index - 1];
} else {
$types[$line_index] = $char;
}
} else {
$types[$line_index] = null;
}
}
$old_line = $hunk->getOldOffset();
$new_line = $hunk->getNewOffset();
$num_lines = count($lines);
if ($old_line > 1) {
$this->missingOld[$old_line] = true;
} else if ($new_line > 1) {
$this->missingNew[$new_line] = true;
}
for ($cursor = 0; $cursor < $num_lines; $cursor++) {
$type = $types[$cursor];
$data = array(
'type' => $type,
'text' => (string)substr($lines[$cursor], 1),
'line' => $new_line,
);
switch ($type) {
case '+':
$this->new[] = $data;
++$new_line;
break;
case '-':
$data['line'] = $old_line;
$this->old[] = $data;
++$old_line;
break;
default:
$this->new[] = $data;
$data['line'] = $old_line;
$this->old[] = $data;
++$new_line;
++$old_line;
break;
}
}
}
public function getDisplayLine($offset, $length) {
$start = 1;
for ($ii = $offset; $ii > 0; $ii--) {
if ($this->new[$ii] && $this->new[$ii]['line']) {
$start = $this->new[$ii]['line'];
break;
}
}
$end = $start;
for ($ii = $offset + $length; $ii < count($this->new); $ii++) {
if ($this->new[$ii] && $this->new[$ii]['line']) {
$end = $this->new[$ii]['line'];
break;
}
}
return "{$start},{$end}";
}
public function parseInlineComment(DifferentialInlineComment $comment) {
$this->comments[] = $comment;
return $this;
}
public function process() {
$old = array();
$new = array();
$n = 0;
$this->old = array_reverse($this->old);
$this->new = array_reverse($this->new);
$whitelines = false;
$changed = false;
$skip_intra = array();
while (count($this->old) || count($this->new)) {
$o_desc = array_pop($this->old);
$n_desc = array_pop($this->new);
$oend = end($this->old);
if ($oend) {
$o_next = $oend['type'];
} else {
$o_next = null;
}
$nend = end($this->new);
if ($nend) {
$n_next = $nend['type'];
} else {
$n_next = null;
}
if ($o_desc) {
$o_type = $o_desc['type'];
} else {
$o_type = null;
}
if ($n_desc) {
$n_type = $n_desc['type'];
} else {
$n_type = null;
}
if (($o_type != null) && ($n_type == null)) {
$old[] = $o_desc;
$new[] = null;
if ($n_desc) {
array_push($this->new, $n_desc);
}
$changed = true;
continue;
}
if (($n_type != null) && ($o_type == null)) {
$old[] = null;
$new[] = $n_desc;
if ($o_desc) {
array_push($this->old, $o_desc);
}
$changed = true;
continue;
}
if ($this->whitespaceMode != self::WHITESPACE_SHOW_ALL) {
$similar = false;
switch ($this->whitespaceMode) {
case self::WHITESPACE_IGNORE_TRAILING:
if (rtrim($o_desc['text']) == rtrim($n_desc['text'])) {
$similar = true;
}
break;
}
if ($similar) {
$o_desc['type'] = null;
$n_desc['type'] = null;
$skip_intra[count($old)] = true;
$whitelines = true;
} else {
$changed = true;
}
} else {
$changed = true;
}
$old[] = $o_desc;
$new[] = $n_desc;
}
$this->old = $old;
$this->new = $new;
if ($this->subparser && false) { // TODO: This is bugged
// Use the subparser's side-by-side line information -- notably, the
// change types -- but replace all the line text with ours. This lets us
// render whitespace-only changes without marking them as different.
$old = $this->subparser->old;
$new = $this->subparser->new;
$old_text = ipull($this->old, 'text', 'line');
$new_text = ipull($this->new, 'text', 'line');
foreach ($old as $k => $desc) {
if (empty($desc)) {
continue;
}
$old[$k]['text'] = idx($old_text, $desc['line']);
}
foreach ($new as $k => $desc) {
if (empty($desc)) {
continue;
}
$new[$k]['text'] = idx($new_text, $desc['line']);
}
$this->old = $old;
$this->new = $new;
}
$min_length = min(count($this->old), count($this->new));
for ($ii = 0; $ii < $min_length; $ii++) {
if ($this->old[$ii] || $this->new[$ii]) {
if (isset($this->old[$ii]['text'])) {
$otext = $this->old[$ii]['text'];
} else {
$otext = '';
}
if (isset($this->new[$ii]['text'])) {
$ntext = $this->new[$ii]['text'];
} else {
$ntext = '';
}
if ($otext != $ntext && empty($skip_intra[$ii])) {
$this->intra[$ii] = ArcanistDiffUtils::generateIntralineDiff(
$otext,
$ntext);
}
}
}
$lines_context = self::LINES_CONTEXT;
$max_length = max(count($this->old), count($this->new));
$old = $this->old;
$new = $this->new;
$visible = false;
$last = 0;
for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) {
$offset = $cursor + $lines_context;
if ((isset($old[$offset]) && $old[$offset]['type']) ||
(isset($new[$offset]) && $new[$offset]['type'])) {
$visible = true;
$last = $offset;
} else if ($cursor > $last + $lines_context) {
$visible = false;
}
if ($visible && $cursor > 0) {
$this->visible[$cursor] = 1;
}
}
$old_corpus = ipull($this->old, 'text');
$old_corpus_block = implode("\n", $old_corpus);
$new_corpus = ipull($this->new, 'text');
$new_corpus_block = implode("\n", $new_corpus);
if ($this->noHighlight) {
$this->oldRender = explode("\n", phutil_escape_html($old_corpus_block));
$this->newRender = explode("\n", phutil_escape_html($new_corpus_block));
} else {
$this->oldRender = $this->sourceHighlight($this->old, $old_corpus_block);
$this->newRender = $this->sourceHighlight($this->new, $new_corpus_block);
}
$this->applyIntraline(
$this->oldRender,
ipull($this->intra, 0),
$old_corpus);
$this->applyIntraline(
$this->newRender,
ipull($this->intra, 1),
$new_corpus);
$this->tokenHighlight($this->oldRender);
$this->tokenHighlight($this->newRender);
$unchanged = false;
if ($this->subparser && false) {
$unchanged = $this->subparser->isUnchanged();
$whitelines = $this->subparser->isWhitespaceOnly();
} else if (!$changed) {
$filetype = $this->changeset->getFileType();
if ($filetype == DifferentialChangeType::FILE_TEXT ||
$filetype == DifferentialChangeType::FILE_SYMLINK) {
$unchanged = true;
}
}
$generated = (strpos($new_corpus_block, '@'.'generated') !== false);
$this->specialAttributes = array(
self::ATTR_GENERATED => $generated,
self::ATTR_UNCHANGED => $unchanged,
self::ATTR_DELETED => array_filter($this->old) &&
!array_filter($this->new),
self::ATTR_WHITELINES => $whitelines
);
}
public function loadCache() {
if (!$this->changesetID) {
return false;
}
$data = null;
$changeset = new DifferentialChangeset();
$conn_r = $changeset->establishConnection('r');
$data = queryfx_one(
$conn_r,
'SELECT * FROM %T WHERE id = %d',
$changeset->getTableName().'_parse_cache',
$this->changesetID);
if (!$data) {
return false;
}
$data = json_decode($data['cache'], true);
if (!is_array($data) || !$data) {
return false;
}
foreach (self::getCacheableProperties() as $cache_key) {
if (!array_key_exists($cache_key, $data)) {
// If we're missing a cache key, assume we're looking at an old cache
// and ignore it.
return false;
}
}
if ($data['cacheVersion'] !== self::CACHE_VERSION) {
return false;
}
unset($data['cacheVersion'], $data['cacheHost']);
$cache_prop = array_select_keys($data, self::getCacheableProperties());
foreach ($cache_prop as $cache_key => $v) {
$this->$cache_key = $v;
}
return true;
}
protected static function getCacheableProperties() {
return array(
'visible',
'new',
'old',
'intra',
'newRender',
'oldRender',
'specialAttributes',
'missingOld',
'missingNew',
'cacheVersion',
'cacheHost',
);
}
public function saveCache() {
if (!$this->changesetID) {
return false;
}
$cache = array();
foreach (self::getCacheableProperties() as $cache_key) {
switch ($cache_key) {
case 'cacheVersion':
$cache[$cache_key] = self::CACHE_VERSION;
break;
case 'cacheHost':
$cache[$cache_key] = php_uname('n');
break;
default:
$cache[$cache_key] = $this->$cache_key;
break;
}
}
$cache = json_encode($cache);
try {
$changeset = new DifferentialChangeset();
$conn_w = $changeset->establishConnection('w');
queryfx(
$conn_w,
'INSERT INTO %T (id, cache) VALUES (%d, %s)
ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
$changeset->getTableName().'_parse_cache',
$this->changesetID,
$cache);
} catch (QueryException $ex) {
// TODO: uhoh
}
}
public function isGenerated() {
return idx($this->specialAttributes, self::ATTR_GENERATED, false);
}
public function isDeleted() {
return idx($this->specialAttributes, self::ATTR_DELETED, false);
}
public function isUnchanged() {
return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
}
public function isWhitespaceOnly() {
return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
}
public function getLength() {
return max(count($this->old), count($this->new));
}
protected function applyIntraline(&$render, $intra, $corpus) {
foreach ($render as $key => $text) {
if (isset($intra[$key])) {
$render[$key] = ArcanistDiffUtils::applyIntralineDiff(
$text,
$intra[$key]);
}
if (isset($corpus[$key]) && strlen($corpus[$key]) > 80) {
$render[$key] = $this->lineWrap($render[$key]);
}
}
}
protected function lineWrap($l) {
$c = 0;
$len = strlen($l);
$ins = array();
for ($ii = 0; $ii < $len; ++$ii) {
if ($l[$ii] == '&') {
do {
++$ii;
} while ($l[$ii] != ';');
++$c;
} else if ($l[$ii] == '<') {
do {
++$ii;
} while ($l[$ii] != '>');
} else {
++$c;
}
if ($c == 80) {
$ins[] = ($ii + 1);
$c = 0;
}
}
while (($pos = array_pop($ins))) {
$l = substr_replace(
$l,
"<span class=\"over-the-line\">\xE2\xAC\x85</span><br />",
$pos,
0);
}
return $l;
}
protected function tokenHighlight(&$render) {
foreach ($render as $key => $text) {
$render[$key] = str_replace(
"\0",
'<span class="uu">'."\xEF\xBF\xBD".'</span>',
$text);
}
}
protected function sourceHighlight($data, $corpus) {
$result = $this->highlightEngine->highlightSource(
$this->filetype,
$corpus);
$result_lines = explode("\n", $result);
foreach ($data as $key => $info) {
if (!$info) {
unset($result_lines[$key]);
}
}
return $result_lines;
}
private function tryCacheStuff() {
$whitespace_mode = $this->whitespaceMode;
switch ($whitespace_mode) {
case self::WHITESPACE_SHOW_ALL:
case self::WHITESPACE_IGNORE_TRAILING:
break;
default:
$whitespace_mode = self::WHITESPACE_IGNORE_ALL;
break;
}
$skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_ALL);
$this->whitespaceMode = $whitespace_mode;
$changeset = $this->changeset;
if ($changeset->getFileType() == DifferentialChangeType::FILE_TEXT ||
$changeset->getFileType() == DifferentialChangeType::FILE_SYMLINK) {
if ($skip_cache || !$this->loadCache()) {
if ($this->whitespaceMode == self::WHITESPACE_IGNORE_ALL) {
// Huge mess. Generate a "-bw" (ignore all whitespace changes) diff,
// parse it out, and then play a shell game with the parsed format
// in process() so we highlight only changed lines but render
// whitespace differences. If we don't do this, we either fail to
// render whitespace changes (which is incredibly confusing,
// especially for python) or often produce a much larger set of
// differences than necessary.
$old_tmp = new TempFile();
$new_tmp = new TempFile();
Filesystem::writeFile($old_tmp, $changeset->makeOldFile());
Filesystem::writeFile($new_tmp, $changeset->makeNewFile());
list($err, $diff) = exec_manual(
'diff -bw -U9999 %s %s',
$old_tmp,
$new_tmp);
if (!strlen($diff)) {
// If there's no diff text, that means the files are identical
// except for whitespace changes. Build a synthetic, changeless
// diff. TODO: this is incredibly hacky.
$entire_file = explode("\n", $changeset->makeOldFile());
foreach ($entire_file as $k => $line) {
$entire_file[$k] = ' '.$line;
}
$len = count($entire_file);
$entire_file = implode("\n", $entire_file);
$diff = <<<EOSYNTHETIC
--- ignored 9999-99-99
+++ ignored 9999-99-99
@@ -{$len},{$len} +{$len},{$len} @@
{$entire_file}
EOSYNTHETIC;
}
$changes = id(new ArcanistDiffParser())->parseDiff($diff);
$diff = DifferentialDiff::newFromRawChanges($changes);
$alt_changeset = reset($diff->getChangesets());
$this->subparser = new DifferentialChangesetParser();
$this->subparser->setChangeset($alt_changeset);
$this->subparser->setWhitespaceMode(self::WHITESPACE_IGNORE_TRAILING);
}
foreach ($changeset->getHunks() as $hunk) {
$this->parseHunk($hunk);
}
$this->process();
if (!$skip_cache) {
$this->saveCache();
}
}
}
}
public function render(
ViewerContext $viewer_context = null,
$range_start = null,
$range_len = null,
$mask_force = array()) {
$this->highlightEngine = new PhutilDefaultSyntaxHighlighterEngine();
$this->tryCacheStuff();
$changeset_id = $this->changesetID;
$feedback_mask = array();
switch ($this->changeset->getFileType()) {
case DifferentialChangeType::FILE_IMAGE:
$old = null;
$cur = null;
$metadata = $this->changeset->getMetadata();
$data = idx($metadata, 'attachment-data');
$old_phid = idx($metadata, 'old:binary-phid');
$new_phid = idx($metadata, 'new:binary-phid');
if ($old_phid || $new_phid) {
if ($old_phid) {
$old_uri = PhabricatorFileURI::getViewURIForPHID($old_phid);
$old = phutil_render_tag(
'img',
array(
'src' => $old_uri,
));
}
if ($new_phid) {
$new_uri = PhabricatorFileURI::getViewURIForPHID($new_phid);
$cur = phutil_render_tag(
'img',
array(
'src' => $new_uri,
));
}
}
$output = $this->renderChangesetTable(
$this->changeset,
'<tr>'.
'<th></th>'.
'<td class="differential-old-image">'.
'<div class="differential-image-stage">'.
$old.
'</div>'.
'</td>'.
'<th></th>'.
'<td class="differential-new-image">'.
'<div class="differential-image-stage">'.
$cur.
'</div>'.
'</td>'.
'</tr>');
return $output;
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
$output = $this->renderChangesetTable($this->changeset, null);
return $output;
}
$shield = null;
if ($range_start === null && $range_len === null) {
if ($this->isGenerated()) {
$shield = $this->renderShield(
"This file contains generated code, which does not normally need ".
"to be reviewed.",
true);
} else if ($this->isUnchanged()) {
if ($this->isWhitespaceOnly()) {
$shield = $this->renderShield(
"This file was changed only by adding or removing trailing ".
"whitespace.",
false);
} else {
$shield = $this->renderShield(
"The contents of this file were not changed.",
false);
}
} else if ($this->isDeleted()) {
$shield = $this->renderShield(
"This file was completely deleted.",
true);
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$lines = number_format($this->changeset->getAffectedLineCount());
$shield = $this->renderShield(
"This file has a very large number of changes ({$lines} lines).",
true);
} else if (preg_match('/\.sql3$/', $this->changeset->getFilename())) {
$shield = $this->renderShield(
".sql3 files are hidden by default.",
true);
}
}
if ($shield) {
return $this->renderChangesetTable($this->changeset, $shield);
}
$old_comments = array();
$new_comments = array();
$old_mask = array();
$new_mask = array();
$feedback_mask = array();
- $handles = array();
if ($this->comments) {
foreach ($this->comments as $comment) {
$start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0);
- $end = $comment->getFinalLine() + self::LINES_CONTEXT;
+ $end = $comment->getLineNumber() +
+ $comment->getLineLength() +
+ self::LINES_CONTEXT;
$new = $this->isCommentInNewFile($comment);
for ($ii = $start; $ii <= $end; $ii++) {
if ($new) {
$new_mask[$ii] = true;
} else {
$old_mask[$ii] = true;
}
}
}
foreach ($this->old as $ii => $old) {
if (isset($old['line']) && isset($old_mask[$old['line']])) {
$feedback_mask[$ii] = true;
}
}
foreach ($this->new as $ii => $new) {
if (isset($new['line']) && isset($new_mask[$new['line']])) {
$feedback_mask[$ii] = true;
}
}
-/*
- $handle_ids = mpull($this->comments, 'getUserPHID');
- $handles = array();
- $handle_data = new ToolsHandleData($handle_ids, $handles);
- $handle_data->needNames();
- prep($handle_data);
-
$this->comments = msort($this->comments, 'getID');
foreach ($this->comments as $comment) {
+ $final = $comment->getLineNumber() +
+ $comment->getLineLength();
if ($this->isCommentInNewFile($comment)) {
- $new_comments[$comment->getFinalLine()][] = $comment;
+ $new_comments[$final][] = $comment;
} else {
- $old_comments[$comment->getFinalLine()][] = $comment;
+ $old_comments[$final][] = $comment;
}
}
-*/
}
$html = $this->renderTextChange(
$range_start,
$range_len,
$mask_force,
$feedback_mask,
$viewer_context,
- $handles,
$old_comments,
$new_comments);
return $this->renderChangesetTable($this->changeset, $html);
}
private function isCommentInNewFile(DifferentialInlineComment $comment) {
if ($this->oldChangesetID) {
return ($comment->getChangesetID() != $this->oldChangesetID);
} else {
return $comment->getIsNewFile();
}
}
protected function renderShield($message, $more) {
$end = $this->getLength();
$changeset_id = $this->getChangesetID();
if ($more) {
$more =
' '.
javelin_render_tag(
'a',
array(
'mustcapture' => true,
'sigil' => 'show-more',
'class' => 'complete',
'href' => '#',
'meta' => array(
'id' => $changeset_id,
'range' => "0-{$end}",
),
),
'Show File Contents');
} else {
$more = null;
}
return javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td class="differential-shield" colspan="4">'.
phutil_escape_html($message).
$more.
'</td>');
}
protected function renderTextChange(
$range_start,
$range_len,
$mask_force,
$feedback_mask,
$viewer_context,
- $handles,
array $old_comments,
array $new_comments) {
$context_not_available = null;
if ($this->missingOld || $this->missingNew) {
$context_not_available = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td colspan="4" class="show-more">'.
'Context not available.'.
'</td>');
$context_not_available = $context_not_available;
}
$html = array();
$rows = max(
count($this->old),
count($this->new));
if ($range_start === null) {
$range_start = 0;
}
if ($range_len === null) {
$range_len = $rows;
}
$range_len = min($range_len, $rows - $range_start);
$gaps = array();
$gap_start = 0;
$in_gap = false;
$mask = $this->visible + $mask_force + $feedback_mask;
$mask[$range_start + $range_len] = true;
for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
if (isset($mask[$ii])) {
if ($in_gap) {
$gap_length = $ii - $gap_start;
if ($gap_length <= self::LINES_CONTEXT) {
for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
$mask[$jj] = true;
}
} else {
$gaps[] = array($gap_start, $gap_length);
}
$in_gap = false;
}
} else {
if (!$in_gap) {
$gap_start = $ii;
$in_gap = true;
}
}
}
$gaps = array_reverse($gaps);
$changeset = $this->changesetID;
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
$gap = array_pop($gaps);
$top = $gap[0];
$len = $gap[1];
$end = $top + $len - 20;
$contents = array();
if ($len > 40) {
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'id' => $changeset,
'range' => "{$top}-{$len}/{$top}-20",
),
),
"\xE2\x96\xB2 Show 20 Lines");
}
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'id' => $changeset,
'range' => "{$top}-{$len}/{$top}-{$len}",
),
),
'Show All '.$len.' Lines');
if ($len > 40) {
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'id' => $changeset,
'range' => "{$top}-{$len}/{$end}-20",
),
),
"\xE2\x96\xBC Show 20 Lines");
};
$container = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td colspan="4" class="show-more">'.
implode(' &bull; ', $contents).
'</td>');
$html[] = $container;
$ii += ($len - 1);
continue;
}
if (isset($this->old[$ii])) {
$o_num = $this->old[$ii]['line'];
$o_text = $this->oldRender[$ii];
$o_attr = null;
if ($this->old[$ii]['type']) {
if (empty($this->new[$ii])) {
$o_attr = ' class="old old-full"';
} else {
$o_attr = ' class="old"';
}
}
} else {
$o_num = null;
$o_text = null;
$o_attr = null;
}
if (isset($this->new[$ii])) {
$n_num = $this->new[$ii]['line'];
$n_text = $this->newRender[$ii];
$n_attr = null;
if ($this->new[$ii]['type']) {
if (empty($this->old[$ii])) {
$n_attr = ' class="new new-full"';
} else {
$n_attr = ' class="new"';
}
}
} else {
$n_num = null;
$n_text = null;
$n_attr = null;
}
if (($o_num && !empty($this->missingOld[$o_num])) ||
($n_num && !empty($this->missingNew[$n_num]))) {
$html[] = $context_not_available;
}
if ($o_num) {
$o_id = ' id="C'.$changeset.'OL'.$o_num.'"';
} else {
$o_id = null;
}
if ($n_num) {
$n_id = ' id="C'.$changeset.'NL'.$n_num.'"';
} else {
$n_id = null;
}
// NOTE: The Javascript is sensitive to whitespace changes in this
// block!
$html[] =
'<tr>'.
'<th'.$o_id.'>'.$o_num.'</th>'.
'<td'.$o_attr.'>'.$o_text.'</td>'.
'<th'.$n_id.'>'.$n_num.'</th>'.
'<td'.$n_attr.'>'.$n_text.'</td>'.
'</tr>';
if ($context_not_available && ($ii == $rows - 1)) {
$html[] = $context_not_available;
}
if ($o_num && isset($old_comments[$o_num])) {
foreach ($old_comments[$o_num] as $comment) {
$xhp = $this->renderInlineComment(
$comment,
- $viewer_context,
- $handles);
+ $viewer_context);
$html[] =
'<tr class="inline"><th /><td>'.
$xhp.
'</td><th /><td /></tr>';
}
}
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $comment) {
$xhp = $this->renderInlineComment(
$comment,
- $viewer_context,
- $handles);
+ $viewer_context);
$html[] =
'<tr class="inline"><th /><td /><th /><td>'.
$xhp.
'</td></tr>';
}
}
}
return implode('', $html);
}
private function renderInlineComment(
DifferentialInlineComment $comment,
- $viewer_context,
- $handles) {
+ $viewer_context) {
$edit = $viewer_context &&
- ($comment->getUserPHID() == $viewer_context->getUserID()) &&
+ ($comment->getAuthorPHID() == $viewer_context->getUserID()) &&
(!$comment->getFeedbackID());
$is_new = $this->isCommentInNewFile($comment);
- return '';
-/*
- return <differential:inline-comment
- inline={$comment}
- edit={$edit}
- isnew={$is_new}
- handle={$handles[$comment->getUserPHID()]} />;
-*/
+ return id(new DifferentialInlineCommentView())
+ ->setInlineComment($comment)
+ ->setOnRight($is_new)
+ ->setHandles($this->handles)
+ ->setMarkupEngine($this->markupEngine)
+ ->render();
}
protected function renderPropertyChangeHeader($changeset) {
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
if ($old === $new) {
return null;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
$new == array('unix:filemode' => '100644')) {
return null;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
$old == array('unix:filemode' => '100644')) {
return null;
}
return null;
/*
+ TODO
+
$table = <table class="differential-property-table" />;
$table->appendChild(
<tr class="property-table-header">
<th>Property Changes</th>
<td class="oval">Old Value</td>
<td class="nval">New Value</td>
</tr>);
$keys = array_keys($old + $new);
sort($keys);
foreach ($keys as $key) {
$oval = idx($old, $key);
$nval = idx($new, $key);
if ($oval !== $nval) {
if ($oval === null) {
$oval = <em>null</em>;
}
if ($nval === null) {
$nval = <em>null</em>;
}
$table->appendChild(
<tr>
<th>{$key}</th>
<td class="oval">{$oval}</td>
<td class="nval">{$nval}</td>
</tr>);
}
}
return $table;
*/
}
protected function renderChangesetTable($changeset, $contents) {
$props = $this->renderPropertyChangeHeader($this->changeset);
$table = null;
if ($contents) {
$table =
'<table class="differential-diff remarkup-code">'.
$contents.
'</table>';
}
if (!$table && !$props) {
$notice = $this->renderChangeTypeHeader($this->changeset, true);
} else {
$notice = $this->renderChangeTypeHeader($this->changeset, false);
}
return implode(
"\n",
array(
$notice,
$props,
$table,
));
}
protected function renderChangeTypeHeader($changeset, $force) {
static $articles = array(
DifferentialChangeType::FILE_IMAGE => 'an',
);
static $files = array(
DifferentialChangeType::FILE_TEXT => 'file',
DifferentialChangeType::FILE_IMAGE => 'image',
DifferentialChangeType::FILE_DIRECTORY => 'directory',
DifferentialChangeType::FILE_BINARY => 'binary file',
DifferentialChangeType::FILE_SYMLINK => 'symlink',
);
static $changes = array(
DifferentialChangeType::TYPE_ADD => 'added',
DifferentialChangeType::TYPE_CHANGE => 'changed',
DifferentialChangeType::TYPE_DELETE => 'deleted',
DifferentialChangeType::TYPE_MOVE_HERE => 'moved from',
DifferentialChangeType::TYPE_COPY_HERE => 'copied from',
DifferentialChangeType::TYPE_MOVE_AWAY => 'moved to',
DifferentialChangeType::TYPE_COPY_AWAY => 'copied to',
DifferentialChangeType::TYPE_MULTICOPY
=> 'deleted after being copied to',
);
$change = $changeset->getChangeType();
$file = $changeset->getFileType();
$message = null;
if ($change == DifferentialChangeType::TYPE_CHANGE &&
$file == DifferentialChangeType::FILE_TEXT) {
if ($force) {
// We have to force something to render because there were no changes
// of other kinds.
$message = "This {$files[$file]} was not modified.";
} else {
// Default case of changes to a text file, no metadata.
return null;
}
} else {
$verb = idx($changes, $change, 'changed');
switch ($change) {
default:
$message = "This {$files[$file]} was <strong>{$verb}</strong>.";
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
case DifferentialChangeType::TYPE_COPY_HERE:
$message =
"This {$files[$file]} was {$verb} ".
"<strong>{$changeset->getOldFile()}</strong>.";
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
case DifferentialChangeType::TYPE_COPY_AWAY:
case DifferentialChangeType::TYPE_MULTICOPY:
$paths = $changeset->getAwayPaths();
if (count($paths) > 1) {
$message =
"This {$files[$file]} was {$verb}: ".
"<strong>".implode(', ', $paths)."</strong>.";
} else {
$message =
"This {$files[$file]} was {$verb} ".
"<strong>".reset($paths)."</strong>.";
}
break;
case DifferentialChangeType::TYPE_CHANGE:
$message = "This is ".idx($articles, $file, 'a')." {$files[$file]}.";
break;
}
}
return
'<div class="differential-meta-notice">'.
$message.
'</div>';
}
public function renderForEmail() {
$ret = '';
$min = min(count($this->old), count($this->new));
for ($i = 0; $i < $min; $i++) {
$o = $this->old[$i];
$n = $this->new[$i];
if (!isset($this->visible[$i])) {
continue;
}
if ($o['line'] && $n['line']) {
// It is quite possible there are better ways to achieve this. For
// example, "white-space: pre;" can do a better job, WERE IT NOT for
// broken email clients like OWA which use newlines to do weird
// wrapping. So dont give them newlines.
if (isset($this->intra[$i])) {
$ret .= sprintf(
"<font color=\"red\">-&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($o['text']))
);
$ret .= sprintf(
"<font color=\"green\">+&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
} else {
$ret .= sprintf("&nbsp;&nbsp;%s<br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
}
} else if ($o['line'] && !$n['line']) {
$ret .= sprintf(
"<font color=\"red\">-&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($o['text']))
);
} else {
$ret .= sprintf(
"<font color=\"green\">+&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
}
}
return $ret;
}
}
diff --git a/src/applications/differential/view/changesetdetailview/DifferentialChangesetDetailView.php b/src/applications/differential/view/changesetdetailview/DifferentialChangesetDetailView.php
index 8a57f48a36..ccba6739db 100644
--- a/src/applications/differential/view/changesetdetailview/DifferentialChangesetDetailView.php
+++ b/src/applications/differential/view/changesetdetailview/DifferentialChangesetDetailView.php
@@ -1,62 +1,66 @@
<?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 DifferentialChangesetDetailView extends AphrontView {
private $changeset;
private $buttons = array();
public function setChangeset($changeset) {
$this->changeset = $changeset;
return $this;
}
public function addButton($button) {
$this->buttons[] = $button;
return $this;
}
public function render() {
require_celerity_resource('differential-changeset-view-css');
require_celerity_resource('syntax-highlighting-css');
$edit = false; // TODO
$changeset = $this->changeset;
$class = 'differential-changeset';
if (!$edit) {
$class .= ' differential-changeset-immutable';
}
$display_filename = $changeset->getDisplayFilename();
$output = javelin_render_tag(
'div',
array(
'sigil' => 'differential-changeset',
+ 'meta' => array(
+ 'left' => $this->changeset->getID(),
+ 'right' => $this->changeset->getID(),
+ ),
'class' => $class,
),
'<a name="#'."TODO".'"></a>'.
implode('', $this->buttons).
'<h1>'.phutil_escape_html($display_filename).'</h1>'.
'<div style="clear: both;"></div>'.
$this->renderChildren());
return $output;
}
}
diff --git a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
index 3a9b21bf17..a4b2261464 100644
--- a/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/changesetlistview/DifferentialChangesetListView.php
@@ -1,134 +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/',
));
if ($this->editable) {
$revision = $this->revision;
Javelin::initBehavior('differential-edit-inline-comments', array(
- 'uri' => '/differential/inline/edit/'.$revision->getID().'/',
+ 'uri' => '/differential/comment/inline/edit/'.$revision->getID().'/',
));
}
return
'<div class="differential-review-stage">'.
implode("\n", $output).
'</div>';
}
}
diff --git a/src/applications/differential/view/inlinecomment/DifferentialInlineCommentView.php b/src/applications/differential/view/inlinecomment/DifferentialInlineCommentView.php
new file mode 100644
index 0000000000..d81e7cd97c
--- /dev/null
+++ b/src/applications/differential/view/inlinecomment/DifferentialInlineCommentView.php
@@ -0,0 +1,131 @@
+<?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.
+ */
+
+final class DifferentialInlineCommentView extends AphrontView {
+
+ private $inlineComment;
+ private $onRight;
+ private $buildScaffolding;
+ private $handles;
+ private $markupEngine;
+
+ public function setInlineComment(DifferentialInlineComment $comment) {
+ $this->inlineComment = $comment;
+ return $this;
+ }
+
+ public function setOnRight($on_right) {
+ $this->onRight = $on_right;
+ return $this;
+ }
+
+ public function setBuildScaffolding($scaffold) {
+ $this->buildScaffolding = $scaffold;
+ return $this;
+ }
+
+ public function setHandles(array $handles) {
+ $this->handles = $handles;
+ return $this;
+ }
+
+ public function setMarkupEngine(PhutilMarkupEngine $engine) {
+ $this->markupEngine = $engine;
+ return $this;
+ }
+
+ public function render() {
+
+ $inline = $this->inlineComment;
+
+ $start = $inline->getLineNumber();
+ $length = $inline->getLineLength();
+ if ($length) {
+ $end = $start + $length;
+ $line = 'Lines '.number_format($start).'-'.number_format($end);
+ } else {
+ $line = 'Line '.number_format($start);
+ }
+
+ $metadata = array(
+ 'number' => $inline->getLineNumber(),
+ 'length' => $inline->getLineLength(),
+ 'on_right' => $this->onRight, // TODO
+ );
+
+ $sigil = 'differential-inline-comment';
+
+ $links = 'xxx';
+ $content = $inline->getContent();
+ $handles = $this->handles;
+
+ if ($links) {
+ $links =
+ '<span class="differential-inline-comment-links">'.
+ $links.
+ '</span>';
+ }
+
+ $content = $this->markupEngine->markupText($content);
+
+ $markup = javelin_render_tag(
+ 'div',
+ array(
+ 'class' => 'differential-inline-comment',
+ 'sigil' => $sigil,
+ 'meta' => $metadata,
+ ),
+ '<div class="differential-inline-comment-head">'.
+ $links.
+ '<span class="differential-inline-comment-line">'.$line.'</span>'.
+ $handles[$inline->getAuthorPHID()]->renderLink().
+ '</div>'.
+ $content);
+
+ return $this->scaffoldMarkup($markup);
+ }
+
+ private function scaffoldMarkup($markup) {
+ if (!$this->buildScaffolding) {
+ return $markup;
+ }
+
+ if ($this->onRight) {
+ return
+ '<table>'.
+ '<tr>'.
+ '<th></th>'.
+ '<td></td>'.
+ '<th></th>'.
+ '<td>'.$markup.'</td>'.
+ '</tr>'.
+ '</table>';
+ } else {
+ return
+ '<table>'.
+ '<tr>'.
+ '<th></th>'.
+ '<td>'.$markup.'</td>'.
+ '<th></th>'.
+ '<td></td>'.
+ '</tr>'.
+ '</table>';
+ }
+ }
+
+}
diff --git a/src/applications/differential/view/inlinecomment/__init__.php b/src/applications/differential/view/inlinecomment/__init__.php
new file mode 100644
index 0000000000..6b3db812a1
--- /dev/null
+++ b/src/applications/differential/view/inlinecomment/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'view/base');
+
+
+phutil_require_source('DifferentialInlineCommentView.php');
diff --git a/src/storage/lisk/dao/LiskDAO.php b/src/storage/lisk/dao/LiskDAO.php
index ab6e88496b..1c8de0e0d0 100644
--- a/src/storage/lisk/dao/LiskDAO.php
+++ b/src/storage/lisk/dao/LiskDAO.php
@@ -1,1109 +1,1111 @@
<?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.
*/
/**
* Simple object-authoritative data access object that makes it easy to build
* stuff that you need to save to a database. Basically, it means that the
* amount of boilerplate code (and, particularly, boilerplate SQL) you need
* to write is greatly reduced.
*
* Lisk makes it fairly easy to build something quickly and end up with
* reasonably high-quality code when you're done (e.g., getters and setters,
* objects, transactions, reasonably structured OO code). It's also very thin:
* you can break past it and use MySQL and other lower-level tools when you
* need to in those couple of cases where it doesn't handle your workflow
* gracefully.
*
* However, Lisk won't scale past one database and lacks many of the features
* of modern DAOs like Hibernate: for instance, it does not support joins or
* polymorphic storage.
*
* This means that Lisk is well-suited for tools like Differential, but often a
* poor choice elsewhere. And it is strictly unsuitable for many projects.
*
* Lisk's model is object-authoritative: the PHP class definition is the
* master authority for what the object looks like.
*
* =Building New Objects=
*
* To create new Lisk objects, extend @{class:LiskDAO} and implement
* @{method:establishConnection}. It should return an AphrontDatabaseConnection;
* this will tell Lisk where to save your objects.
*
* class Dog extends LiskDAO {
*
* protected $name;
* protected $breed;
*
* public function establishConnection() {
* return $some_connection_object;
* }
* }
*
* Now, you should create your table:
*
* CREATE TABLE dog (
* id int unsigned not null auto_increment primary key,
* name varchar(32) not null,
* breed varchar(32) not null,
* dateCreated int unsigned not null,
* dateModified int unsigned not null
* );
*
* For each property in your class, add a column with the same name to the
* table (see getConfiguration() for information about changing this mapping).
* Additionally, you should create the three columns `id`, `dateCreated` and
* `dateModified`. Lisk will automatically manage these, using them to implement
* autoincrement IDs and timestamps. If you do not want to use these features,
* see getConfiguration() for information on disabling them. At a bare minimum,
* you must normally have an `id` column which is a primary or unique key with a
* numeric type, although you can change its name by overriding getIDKey() or
* disable it entirely by overriding getIDKey() to return null. Note that many
* methods rely on a single-part primary key and will no longer work (they will
* throw) if you disable it.
*
* As you add more properties to your class in the future, remember to add them
* to the database table as well.
*
* Lisk will now automatically handle these operations: getting and setting
* properties, saving objects, loading individual objects, loading groups
* of objects, updating objects, managing IDs, updating timestamps whenever
* an object is created or modified, and some additional specialized
* operations.
*
* = Creating, Retrieving, Updating, and Deleting =
*
* To create and persist a Lisk object, use save():
*
* $dog = id(new Dog())
* ->setName('Sawyer')
* ->setBreed('Pug')
* ->save();
*
* Note that **Lisk automatically builds getters and setters for all of your
* object's properties** via __call(). You can override these by defining
* versions yourself.
*
* Calling save() will persist the object to the database. After calling
* save(), you can call getID() to retrieve the object's ID.
*
* To load objects by ID, use the load() method:
*
* $dog = id(new Dog())->load($id);
*
* This will load the Dog record with ID $id into $dog, or ##null## if no such
* record exists (load() is an instance method rather than a static method
* because PHP does not support late static binding, at least until PHP 5.3).
*
* To update an object, change its properties and save it:
*
* $dog->setBreed('Lab')->save();
*
* To delete an object, call delete():
*
* $dog->delete();
*
* That's Lisk CRUD in a nutshell.
*
* = Queries =
*
* Often, you want to load a bunch of objects, or execute a more specialized
* query. Use loadAllWhere() or loadOneWhere() to do this:
*
* $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
* $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
*
* These methods work like @{function:queryfx}, but only take half of a query
* (the part after the WHERE keyword). Lisk will handle the connection, columns,
* and object construction; you are responsible for the rest of it.
* loadAllWhere() returns a list of objects, while loadOneWhere() returns a
* single object (or null).
*
* @task config Configuring Lisk
* @task load Loading Objects
* @task info Examining Objects
* @task save Writing Objects
* @task hook Hooks and Callbacks
* @task util Utilities
*
* @group storage
*/
abstract class LiskDAO {
const CONFIG_OPTIMISTIC_LOCKS = 'enable-locks';
const CONFIG_IDS = 'id-mechanism';
const CONFIG_TIMESTAMPS = 'timestamps';
const CONFIG_AUX_PHID = 'auxiliary-phid';
const CONFIG_SERIALIZATION = 'col-serialization';
const SERIALIZATION_NONE = 'id';
const SERIALIZATION_JSON = 'json';
const SERIALIZATION_PHP = 'php';
const IDS_AUTOINCREMENT = 'ids-auto';
const IDS_PHID = 'ids-phid';
const IDS_MANUAL = 'ids-manual';
/**
* Build an empty object.
*
* @return obj Empty object.
*/
public function __construct() {
$id_key = $this->getIDKey();
if ($id_key) {
$this->$id_key = null;
}
}
abstract protected function establishConnection($mode);
/* -( Configuring Lisk )--------------------------------------------------- */
/**
* Change Lisk behaviors, like optimistic locks and timestamps. If you want
* to change these behaviors, you should override this method in your child
* class and change the options you're interested in. For example:
*
* public function getConfiguration() {
* return array(
* Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
* ) + parent::getConfiguration();
* }
*
* The available options are:
*
* CONFIG_OPTIMISTIC_LOCKS
* Lisk automatically performs optimistic locking on objects, which protects
* you from read-modify-write concurrency problems. Lock failures are
* detected at write time and arise when two users read an object, then both
* save it. In theory, you should detect these failures and accommodate them
* in some sensible way (for instance, by showing the user differences
* between the original record and the copy they are trying to update, and
* prompting them to merge them). In practice, most Lisk tools are quick
* and dirty and don't get to that level of sophistication, but optimistic
* locks can still protect you from yourself sometimes. If you don't want
* to use optimistic locks, you can disable them. The performance cost of
* doing this locking is very very small (optimistic locks were chosen
* because they're simple and cheap, and highly optimized for the case where
* collisions are rare). By default, this option is OFF.
*
* CONFIG_IDS
* Lisk objects need to have a unique identifying ID. The three mechanisms
* available for generating this ID are IDS_AUTOINCREMENT (default, assumes
* the ID column is an autoincrement primary key), IDS_PHID (to generate a
* unique PHID for each object) or IDS_MANUAL (you are taking full
* responsibility for ID management).
*
* CONFIG_TIMESTAMPS
* Lisk can automatically handle keeping track of a `dateCreated' and
* `dateModified' column, which it will update when it creates or modifies
* an object. If you don't want to do this, you may disable this option.
* By default, this option is ON.
*
* CONFIG_AUX_PHID
* This option can be enabled by being set to some truthy value. The meaning
* of this value is defined by your PHID generation mechanism. If this option
* is enabled, a `phid' property will be populated with a unique PHID when an
* object is created (or if it is saved and does not currently have one). You
* need to override generatePHID() and hook it into your PHID generation
* mechanism for this to work. By default, this option is OFF.
*
* CONFIG_SERIALIZATION
* You can optionally provide a column serialization map that will be applied
* to values when they are written to the database. For example:
*
* self::CONFIG_SERIALIZATION => array(
* 'complex' => self::SERIALIZATION_JSON,
* )
*
* This will cause Lisk to JSON-serialize the 'complex' field before it is
* written, and unserialize it when it is read.
*
*
* @return dictionary Map of configuration options to values.
*
* @task config
*/
protected function getConfiguration() {
return array(
self::CONFIG_OPTIMISTIC_LOCKS => false,
self::CONFIG_IDS => self::IDS_AUTOINCREMENT,
self::CONFIG_TIMESTAMPS => true,
);
}
/**
* Determine the setting of a configuration option for this class of objects.
*
* @param const Option name, one of the CONFIG_* constants.
* @return mixed Option value, if configured (null if unavailable).
*
* @task config
*/
public function getConfigOption($option_name) {
static $options = null;
if (!isset($options)) {
$options = $this->getConfiguration();
}
return idx($options, $option_name);
}
/* -( Loading Objects )---------------------------------------------------- */
/**
* Load an object by ID. You need to invoke this as an instance method, not
* a class method, because PHP doesn't have late static binding (until
* PHP 5.3.0). For example:
*
* $dog = id(new Dog())->load($dog_id);
*
* @param int Numeric ID identifying the object to load.
* @return obj|null Identified object, or null if it does not exist.
*
* @task load
*/
public function load($id) {
if (!($id = (int)$id)) {
throw new Exception("Bogus ID provided to load().");
}
return $this->loadOneWhere(
'%C = %d',
$this->getIDKeyForUse(),
$id);
}
/**
* Loads all of the objects, unconditionally.
*
* @return dict Dictionary of all persisted objects of this type, keyed
* on object ID.
*
* @task load
*/
public function loadAll() {
return $this->loadAllWhere('1 = 1');
}
/**
* Load all objects which match a WHERE clause. You provide everything after
* the 'WHERE'; Lisk handles everything up to it. For example:
*
* $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
*
* The pattern and arguments are as per queryfx().
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return dict Dictionary of matching objects, keyed on ID.
*
* @task load
*/
public function loadAllWhere($pattern/*, $arg, $arg, $arg ... */) {
$args = func_get_args();
$data = call_user_func_array(
array($this, 'loadRawDataWhere'),
$args);
return $this->loadAllFromArray($data);
}
/**
* Load a single object identified by a 'WHERE' clause. You provide
* everything after the 'WHERE', and Lisk builds the first half of the
* query. See loadAllWhere(). This method is similar, but returns a single
* result instead of a list.
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return obj|null Matching object, or null if no object matches.
*
* @task load
*/
public function loadOneWhere($pattern/*, $arg, $arg, $arg ... */) {
$args = func_get_args();
$data = call_user_func_array(
array($this, 'loadRawDataWhere'),
$args);
if (count($data) > 1) {
throw new AphrontQueryCountException(
"More than 1 result from loadOneWhere()!");
}
$data = reset($data);
if (!$data) {
return null;
}
return $this->loadFromArray($data);
}
protected function loadRawDataWhere($pattern/*, $arg, $arg, $arg ... */) {
$connection = $this->getConnection('r');
$lock_clause = '';
if ($connection->isReadLocking()) {
$lock_clause = 'FOR UPDATE';
} else if ($connection->isWriteLocking()) {
$lock_clause = 'LOCK IN SHARE MODE';
}
$args = func_get_args();
$args = array_slice($args, 1);
$pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q';
array_unshift($args, $this->getTableName());
array_push($args, $lock_clause);
array_unshift($args, $pattern);
return call_user_func_array(
array($connection, 'queryData'),
$args);
}
/**
* Reload an object from the database, discarding any changes to persistent
* properties. If the object uses optimistic locks and you are in a locking
* mode while transactional, this will effectively synchronize the locks.
* This is pretty heady. It is unlikely you need to use this method.
*
* @return this
*
* @task load
*/
public function reload() {
if (!$this->getID()) {
throw new Exception("Unable to reload object that hasn't been loaded!");
}
$use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS);
if (!$use_locks) {
$result = $this->loadOneWhere(
'%C = %d',
$this->getIDKeyForUse(),
$this->getID());
} else {
$result = $this->loadOneWhere(
'%C = %d AND %C = %d',
$this->getIDKeyForUse(),
$this->getID(),
'version',
$this->getVersion());
}
if (!$result) {
throw new AphrontQueryObjectMissingException($use_locks);
}
return $this;
}
/**
* Initialize this object's properties from a dictionary. Generally, you
* load single objects with loadOneWhere(), but sometimes it may be more
* convenient to pull data from elsewhere directly (e.g., a complicated
* join via queryData()) and then load from an array representation.
*
* @param dict Dictionary of properties, which should be equivalent to
* selecting a row from the table or calling getProperties().
* @return this
*
* @task load
*/
public function loadFromArray(array $row) {
$map = array();
foreach ($row as $k => $v) {
$map[$k] = $v;
}
$this->willReadData($map);
foreach ($map as $prop => $value) {
$this->$prop = $value;
}
$this->didReadData();
return $this;
}
/**
* Initialize a list of objects from a list of dictionaries. Usually you
* load lists of objects with loadAllWhere(), but sometimes that isn't
* flexible enough. One case is if you need to do joins to select the right
* objects:
*
* function loadAllWithOwner($owner) {
* $data = $this->queryData(
* 'SELECT d.*
* FROM owner o
* JOIN owner_has_dog od ON o.id = od.ownerID
* JOIN dog d ON od.dogID = d.id
* WHERE o.id = %d',
* $owner);
* return $this->loadAllFromArray($data);
* }
*
* This is a lot messier than loadAllWhere(), but more flexible.
*
* @param list List of property dictionaries.
* @return dict List of constructed objects, keyed on ID.
*
* @task load
*/
public function loadAllFromArray(array $rows) {
$result = array();
$id_key = $this->getIDKey();
foreach ($rows as $row) {
$obj = clone $this;
if ($id_key) {
$result[$row[$id_key]] = $obj->loadFromArray($row);
} else {
$result[] = $obj->loadFromArray($row);
}
}
return $result;
}
/* -( Examining Objects )-------------------------------------------------- */
/**
* Retrieve the unique, numerical ID identifying this object. This value
* will be null if the object hasn't been persisted.
*
* @return int Unique numerical ID.
*
* @task info
*/
public function getID() {
$id_key = $this->getIDKeyForUse();
return $this->$id_key;
}
/**
* Retrieve a list of all object properties. Note that some may be
* "transient", which means they should not be persisted to the database.
* Transient properties can be identified by calling
* getTransientProperties().
*
* @return dict Dictionary of normalized (lowercase) to canonical (original
* case) property names.
*
* @task info
*/
protected function getProperties() {
static $properties = null;
if (!isset($properties)) {
$class = new ReflectionClass(get_class($this));
$properties = array();
foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
$properties[strtolower($p->getName())] = $p->getName();
}
$id_key = $this->getIDKey();
if ($id_key) {
if (!isset($properties[strtolower($id_key)])) {
$properties[strtolower($id_key)] = $id_key;
}
}
if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) {
$properties['version'] = 'version';
}
if ($this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
$properties['datecreated'] = 'dateCreated';
$properties['datemodified'] = 'dateModified';
}
if (!$this->isPHIDPrimaryID() &&
$this->getConfigOption(self::CONFIG_AUX_PHID)) {
$properties['phid'] = 'phid';
}
}
return $properties;
}
/**
* Check if a property exists on this object.
*
* @return string|null Canonical property name, or null if the property
* does not exist.
*
* @task info
*/
protected function checkProperty($property) {
static $properties = null;
if (!isset($properties)) {
$properties = $this->getProperties();
}
return idx($properties, strtolower($property));
}
/**
* Get or build the database connection for this object.
*
* @return LiskDatabaseConnection Lisk connection object.
*
* @task info
*/
protected function getConnection($mode) {
if ($mode != 'r' && $mode != 'w') {
throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'.");
}
// TODO: We don't do anything with the read/write mode right now, but
// should.
if (!isset($this->__connection)) {
$this->__connection = $this->establishConnection($mode);
}
return $this->__connection;
}
/**
* Convert this object into a property dictionary. This dictionary can be
* restored into an object by using loadFromArray() (unless you're using
* legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should
* just go ahead and die in a fire).
*
* @return dict Dictionary of object properties.
*
* @task info
*/
protected function getPropertyValues() {
$map = array();
foreach ($this->getProperties() as $p) {
// We may receive a warning here for properties we've implicitly added
// through configuration; squelch it.
$map[$p] = @$this->$p;
}
return $map;
}
/**
* Convert this object into a property dictionary containing only properties
* which will be persisted to the database.
*
* @return dict Dictionary of persistent object properties.
*
* @task info
*/
protected function getPersistentPropertyValues() {
$map = $this->getPropertyValues();
foreach ($this->getTransientProperties() as $p) {
unset($map[$p]);
}
return $map;
}
/* -( Writing Objects )---------------------------------------------------- */
/**
* Persist this object to the database. In most cases, this is the only
* method you need to call to do writes. If the object has not yet been
* inserted this will do an insert; if it has, it will do an update.
*
* @return this
*
* @task save
*/
public function save() {
if ($this->shouldInsertWhenSaved()) {
return $this->insert();
} else {
return $this->update();
}
}
/**
* Save this object, forcing the query to use REPLACE regardless of object
* state.
*
* @return this
*
* @task save
*/
public function replace() {
return $this->insertRecordIntoDatabase('REPLACE');
}
/**
* Save this object, forcing the query to use INSERT regardless of object
* state.
*
* @return this
*
* @task save
*/
public function insert() {
return $this->insertRecordIntoDatabase('INSERT');
}
/**
* Save this object, forcing the query to use UPDATE regardless of object
* state.
*
* @return this
*
* @task save
*/
public function update() {
$use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS);
$this->willSaveObject();
$data = $this->getPersistentPropertyValues();
$this->willWriteData($data);
$map = array();
foreach ($data as $k => $v) {
if ($use_locks && $k == 'version') {
continue;
}
$map[$k] = $v;
}
$conn = $this->getConnection('w');
foreach ($map as $key => $value) {
$map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
}
$map = implode(', ', $map);
-
+
if ($use_locks) {
$conn->query(
'UPDATE %T SET %Q, version = version + 1 WHERE %C = %d AND %C = %d',
$this->getTableName(),
$map,
$this->getIDKeyForUse(),
$this->getID(),
'version',
$this->getVersion());
} else {
$conn->query(
'UPDATE %T SET %Q WHERE %C = %d',
$this->getTableName(),
$map,
$this->getIDKeyForUse(),
$this->getID());
}
if ($conn->getAffectedRows() !== 1) {
throw new AphrontQueryObjectMissingException($use_locks);
}
if ($use_locks) {
$this->setVersion($this->getVersion() + 1);
}
$this->didWriteData();
return $this;
}
/**
* Delete this object, permanently.
*
* @return this
*
* @task save
*/
public function delete() {
$this->willDelete();
$conn = $this->getConnection('w');
$conn->query(
'DELETE FROM %T WHERE %C = %d',
$this->getTableName(),
$this->getIDKeyForUse(),
$this->getID());
$this->didDelete();
return $this;
}
/**
* Internal implementation of INSERT and REPLACE.
*
* @param const Either "INSERT" or "REPLACE", to force the desired mode.
*
* @task save
*/
protected function insertRecordIntoDatabase($mode) {
$this->willSaveObject();
$data = $this->getPersistentPropertyValues();
$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
switch ($id_mechanism) {
// If we are using autoincrement IDs, let MySQL assign the value for the
// ID column.
case self::IDS_AUTOINCREMENT:
unset($data[$this->getIDKeyForUse()]);
break;
case self::IDS_PHID:
if (empty($data[$this->getIDKeyForUse()])) {
$phid = $this->generatePHID();
$this->setID($phid);
$data[$this->getIDKeyForUse()] = $phid;
}
break;
case self::IDS_MANUAL:
break;
default:
throw new Exception('Unknown CONFIG_IDs mechanism!');
}
if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) {
$data['version'] = 0;
}
$this->willWriteData($data);
+ $conn = $this->getConnection('w');
+
$columns = array_keys($data);
- foreach ($columns as $k => $property) {
- $columns[$k] = $property;
+
+ foreach ($data as $key => $value) {
+ $data[$key] = qsprintf($conn, '%ns', $value);
}
-
- $conn = $this->getConnection('w');
+ $data = implode(', ', $data);
$conn->query(
- '%Q INTO %T (%LC) VALUES (%Ls)',
+ '%Q INTO %T (%LC) VALUES (%Q)',
$mode,
$this->getTableName(),
$columns,
$data);
// Update the object with the initial Version value
if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) {
$this->setVersion(0);
}
// Only use the insert id if this table is using auto-increment ids
if ($id_mechanism === self::IDS_AUTOINCREMENT) {
$this->setID($conn->getInsertID());
}
$this->didWriteData();
return $this;
}
/**
* Method used to determine whether to insert or update when saving.
*
* @return bool true if the record should be inserted
*/
protected function shouldInsertWhenSaved() {
$key_type = $this->getConfigOption(self::CONFIG_IDS);
$use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS);
if ($key_type == self::IDS_MANUAL) {
if ($use_locks) {
// If we are manually keyed and the object has a version (which means
// that it has been saved to the DB before), do an update, otherwise
// perform an insert.
if ($this->getID() && $this->getVersion() !== null) {
return false;
} else {
return true;
}
} else {
throw new Exception(
'You are not using optimistic locks, but are using manual IDs. You '.
'must override the shouldInsertWhenSaved() method to properly '.
'detect when to insert a new record.');
}
} else {
return !$this->getID();
}
}
/* -( Hooks and Callbacks )------------------------------------------------ */
/**
* Retrieve the database table name. By default, this is the class name.
*
* @return string Table name for object storage.
*
* @task hook
*/
public function getTableName() {
return get_class($this);
}
/**
* Helper: Whether this class is configured to use PHIDs as the primary ID.
* @task internal
*/
private function isPHIDPrimaryID() {
return ($this->getConfigOption(self::CONFIG_IDS) === self::IDS_PHID);
}
/**
* Retrieve the primary key column, "id" by default. If you can not
* reasonably name your ID column "id", override this method.
*
* @return string Name of the ID column.
*
* @task hook
*/
public function getIDKey() {
return
$this->isPHIDPrimaryID() ?
'phid' :
'id';
}
protected function getIDKeyForUse() {
$id_key = $this->getIDKey();
if (!$id_key) {
throw new Exception(
"This DAO does not have a single-part primary key. The method you ".
"called requires a single-part primary key.");
}
return $id_key;
}
/**
* Generate a new PHID, used by CONFIG_AUX_PHID and IDS_PHID.
*
* @return phid Unique, newly allocated PHID.
*
* @task hook
*/
protected function generatePHID() {
throw new Exception(
"To use CONFIG_AUX_PHID or IDS_PHID, you need to overload ".
"generatePHID() to perform PHID generation.");
}
/**
* If your object has properties which you don't want to be persisted to the
* database, you can override this method and specify them.
*
* @return list List of properties which should NOT be persisted.
* Property names should be in normalized (lowercase) form.
* By default, all properties are persistent.
*
* @task hook
*/
protected function getTransientProperties() {
return array();
}
/**
* Hook to apply serialization or validation to data before it is written to
* the database. See also willReadData().
*
* @task hook
*/
protected function willWriteData(array &$data) {
$this->applyLiskDataSerialization($data, false);
}
/**
* Hook to perform actions after data has been written to the database.
*
* @task hook
*/
protected function didWriteData() {}
/**
* Hook to make internal object state changes prior to INSERT, REPLACE or
* UPDATE.
*
* @task hook
*/
protected function willSaveObject() {
$use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
if ($use_timestamps) {
if (!$this->getDateCreated()) {
$this->setDateCreated(time());
}
$this->setDateModified(time());
}
if (($this->isPHIDPrimaryID() && !$this->getID())) {
// If PHIDs are the primary ID, the subclass could have overridden the
// name of the ID column.
$this->setID($this->generatePHID());
} else if ($this->getConfigOption(self::CONFIG_AUX_PHID) &&
!$this->getPHID()) {
// The subclass could still want PHIDs.
$this->setPHID($this->generatePHID());
}
}
/**
* Hook to apply serialization or validation to data as it is read from the
* database. See also willWriteData().
*
* @task hook
*/
protected function willReadData(array &$data) {
$this->applyLiskDataSerialization($data, $deserialize = true);
}
/**
* Hook to perform an action on data after it is read from the database.
*
* @task hook
*/
protected function didReadData() {}
/**
* Hook to perform an action before the deletion of an object.
*
* @task hook
*/
protected function willDelete() {}
/**
* Hook to perform an action after the deletion of an object.
*
* @task hook
*/
protected function didDelete() {}
/* -( Utilities )---------------------------------------------------------- */
/**
* Applies configured serialization to a dictionary of values.
*
* @task util
*/
protected function applyLiskDataSerialization(array &$data, $deserialize) {
$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
if ($serialization) {
foreach (array_intersect_key($serialization, $data) as $col => $format) {
switch ($format) {
case self::SERIALIZATION_NONE:
break;
case self::SERIALIZATION_PHP:
if ($deserialize) {
$data[$col] = unserialize($data[$col]);
} else {
$data[$col] = serialize($data[$col]);
}
break;
case self::SERIALIZATION_JSON:
if ($deserialize) {
$data[$col] = json_decode($data[$col], true);
} else {
$data[$col] = json_encode($data[$col]);
}
break;
default:
throw new Exception("Unknown serialization format '{$format}'.");
}
}
}
}
/**
* Black magic. Builds implied get*() and set*() for all properties.
*
* @param string Method name.
* @param list Argument vector.
* @return mixed get*() methods return the property value. set*() methods
* return $this.
* @task util
*/
public function __call($method, $args) {
if (!strncmp($method, 'get', 3)) {
$property = substr($method, 3);
if (!($property = $this->checkProperty($property))) {
throw new Exception("Bad getter call: {$method}");
}
if (count($args) !== 0) {
throw new Exception("Getter call should have zero args: {$method}");
}
return @$this->$property;
}
if (!strncmp($method, 'set', 3)) {
$property = substr($method, 3);
$property = $this->checkProperty($property);
if (!$property) {
throw new Exception("Bad setter call: {$method}");
}
if (count($args) !== 1) {
throw new Exception("Setter should have exactly one arg: {$method}");
}
if ($property == 'ID') {
$property = $this->getIDKeyForUse();
}
$this->$property = $args[0];
return $this;
}
throw new Exception("Unable to resolve method: {$method}.");
}
}
diff --git a/src/view/dialog/AphrontDialogView.php b/src/view/dialog/AphrontDialogView.php
index e6225f460c..83433b88b1 100755
--- a/src/view/dialog/AphrontDialogView.php
+++ b/src/view/dialog/AphrontDialogView.php
@@ -1,92 +1,126 @@
<?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;
+ private $user;
+ private $hidden = array();
+
+ public function setUser(PhabricatorUser $user) {
+ $this->user = $user;
+ return $this;
+ }
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;
}
+ public function addHiddenInput($key, $value) {
+ $this->hidden[$key] = $value;
+ 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(
+ $buttons[] = javelin_render_tag(
'a',
array(
'href' => $this->cancelURI,
'class' => 'button grey',
+ 'name' => '__cancel__',
+ 'sigil' => 'jx-workflow-button',
),
'Cancel');
}
+ if (!$this->user) {
+ throw new Exception(
+ "You must call setUser() when rendering an AphrontDialogView.");
+ }
+ $csrf = $this->user->getCSRFToken();
+
+ $hidden_inputs = array();
+ foreach ($this->hidden as $key => $value) {
+ $hidden_inputs[] = phutil_render_tag(
+ 'input',
+ array(
+ 'type' => 'hidden',
+ 'name' => $key,
+ 'value' => $value,
+ ));
+ }
+ $hidden_inputs = implode("\n", $hidden_inputs);
+
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" />'.
+ '<input type="hidden" name="__csrf__" value="'.$csrf.'" />'.
+ $hidden_inputs.
'<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/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css
index 7395e5a669..00f316712d 100644
--- a/webroot/rsrc/css/application/differential/changeset-view.css
+++ b/webroot/rsrc/css/application/differential/changeset-view.css
@@ -1,132 +1,166 @@
/**
* @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;
}
+.differential-inline-comment {
+ background: #f9f9f1;
+ border: 1px solid #aaaa88;
+ font-family: Verdana;
+ font-size: 11px;
+ margin: 4px 0px;
+ max-width: 81ch;
+ padding: 8px 10px;
+ width: 100%;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+
+.differential-inline-comment-head {
+ font-weight: bold;
+ color: #333333;
+ border-bottom: 1px solid #ccccaa;
+ padding-bottom: 6px;
+ margin-bottom: 4px;
+}
+
+.differential-inline-comment-links,
+.differential-inline-comment-line {
+ font-weight: normal;
+ font-style: italic;
+ color: #666666;
+ float: right;
+ white-space: nowrap;
+}
+
+.differential-inline-comment-edit {
+ padding-left: 8px;
+ font-style: normal;
+}
diff --git a/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
index a63526bb71..e1294b522d 100644
--- a/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
+++ b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
@@ -1,229 +1,235 @@
/**
* @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) {
+ function isOnRight(node) {
return node.parentNode.firstChild != node;
}
+
+ function isNewFile(node) {
+ var data = JX.Stratcom.getData(root);
+ return isOnRight(node) || (data.left != data.right);
+ }
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;
+ if (isOnRight(target)) {
+ changeset = data.left;
} else {
- changeset = data.nid;
+ changeset = data.right;
}
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)) ||
+ (isOnRight(e.getTarget()) != isOnRight(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
+ is_new: isNewFile(target) ? 1 : 0,
+ on_right: isOnRight(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});
+ JX.Stratcom.invoke('differential-inline-comment-update');
})
.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',
+ 'differential-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;
+ var data = e.getNodeData('differential-inline-comment');
+ var change = e.getNodeData('differential-changeset');
- if (data.is_new) {
- prefix += 'NL';
+ var prefix;
+ if (data.on_right) {
+ prefix = 'C' + (change.left) + 'NL';
} else {
- prefix += 'OL';
+ prefix = 'C' + (change.right) + 'OL';
}
origin = JX.$(prefix + data.number);
- target = JX.$(prefix + (data.number + data.length));
+ target = JX.$(prefix + (parseInt(data.number, 10) +
+ parseInt(data.length, 10)));
updateReticle();
}
});
JX.Stratcom.listen(
'click',
- [['inline-comment', 'delete'],
- ['inline-comment', 'edit']],
+ [['differential-inline-comment', 'delete'],
+ ['differential-inline-comment', 'edit']],
function(e) {
var data = {
op: e.getNode('edit') ? 'edit' : 'delete',
- id: e.getNodeData('inline-comment').id
+ id: e.getNodeData('differential-inline-comment').id
};
new JX.Workflow(config.uri, data)
.setHandler(function(r) {
- var base_row = e.getNode('inline-comment').parentNode.parentNode;
+ var base_row = e.getNode('differential-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/javelin.dev.js b/webroot/rsrc/js/javelin/javelin.dev.js
index 7fc3cfbe28..77cc59f031 100644
--- a/webroot/rsrc/js/javelin/javelin.dev.js
+++ b/webroot/rsrc/js/javelin/javelin.dev.js
@@ -1,2949 +1,2951 @@
/** @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;
}
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('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) {
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 : '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 : {},
_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 (data-meta attribute) on a node looks like this:
*
* 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.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 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) {
var path = [];
var nodes = {};
var push = function(key, node) {
// we explicitly only store the first occurrence of each key
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 && cursor.getAttribute) {
push('tag:' + cursor.nodeName.toLowerCase(), cursor);
var id = cursor.id;
if (id) {
push('id:' + id, 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 proxy = new JX.Event()
.setRawEvent(event)
.setType(etype)
.setTarget(target)
.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'});
}
},
/**
* 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.
*
* @task sigil
*/
hasSigil : function(node, sigil) {
if (__DEV__) {
if (!node || !node.getAttribute) {
throw new Error(
'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.');
}
}
var sigils = node.getAttribute('data-sigil');
return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1;
},
/**
* 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
*/
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. 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 || !node.getAttribute) {
throw new Error(
'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 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];
}
}
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;
},
/**
* 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
*/
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);
},
/**
* @task internal
*/
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 (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 (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', 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.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".');
}
}
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 && 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 list? A path to listen on, or a list of paths.
* @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);
+ var id = ['id:' + JX.DOM.uniqID(node)];
+ path = JX.$AX(path || []);
+ for (var ii = 0; ii < path.length; ii++) {
+ path[ii] = id.concat(JX.$AX(path[ii]));
+ }
+ return JX.Stratcom.listen(type, 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/workflow.dev.js b/webroot/rsrc/js/javelin/workflow.dev.js
index 27fc83a262..94710abf32 100644
--- a/webroot/rsrc/js/javelin/workflow.dev.js
+++ b/webroot/rsrc/js/javelin/workflow.dev.js
@@ -1,239 +1,240 @@
/** @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-button'], ['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
}
});

File Metadata

Mime Type
text/x-diff
Expires
Jan 19 2025, 21:16 (6 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1128848
Default Alt Text
(250 KB)

Event Timeline