From 9dac0ed9f1de3a93504a0ce14dc9579b49f2e8aa Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 1 Feb 2011 15:52:04 -0800 Subject: [PATCH] Bring in JX.Workflow and the inline commenting behavior, plus sync Javelin. --- conf/default.conf.php | 12 +- scripts/daemons/metamta/metamta_mta.php | 18 +- src/__celerity_resource_map__.php | 79 +++-- src/__phutil_library_map__.php | 2 + .../DifferentialRevisionViewController.php | 2 + .../DifferentialInlineComment.php | 33 ++ .../storage/inlinecomment/__init__.php | 12 + .../DifferentialChangesetListView.php | 28 +- .../storage/mail/PhabricatorMetaMTAMail.php | 16 +- .../metamta/storage/mail/__init__.php | 3 + src/infrastructure/javelin/markup/markup.php | 13 +- src/view/dialog/AphrontDialogView.php | 3 +- src/view/dialog/__init__.php | 1 + .../standard/PhabricatorStandardPageView.php | 1 + webroot/rsrc/css/aphront/dialog-view.css | 22 +- .../differential/changeset-view.css | 11 + webroot/rsrc/css/core/dialog.css | 55 ++++ .../behavior-edit-inline-comments.js | 229 ++++++++++++++ webroot/rsrc/js/javelin/init.dev.js | 5 +- webroot/rsrc/js/javelin/init.min.js | 2 +- webroot/rsrc/js/javelin/javelin.dev.js | 297 +++++++++--------- webroot/rsrc/js/javelin/javelin.min.js | 3 +- webroot/rsrc/js/javelin/typeahead.dev.js | 89 +++--- webroot/rsrc/js/javelin/typeahead.min.js | 2 +- webroot/rsrc/js/javelin/workflow.dev.js | 239 ++++++++++++++ webroot/rsrc/js/javelin/workflow.min.js | 3 + 26 files changed, 934 insertions(+), 246 deletions(-) create mode 100644 src/applications/differential/storage/inlinecomment/DifferentialInlineComment.php create mode 100644 src/applications/differential/storage/inlinecomment/__init__.php create mode 100644 webroot/rsrc/css/core/dialog.css create mode 100644 webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js create mode 100644 webroot/rsrc/js/javelin/workflow.dev.js create mode 100644 webroot/rsrc/js/javelin/workflow.min.js diff --git a/conf/default.conf.php b/conf/default.conf.php index ee9cbf31bb..a668c27d79 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -31,10 +31,10 @@ return array( // The username to use when connecting to MySQL. 'mysql.user' => 'root', - + // The password to use when connecting to MySQL. 'mysql.pass' => '', - + // The MySQL server to connect to. 'mysql.host' => 'localhost', @@ -56,22 +56,22 @@ return array( // Is Recaptcha enabled? If disabled, captchas will not appear. 'recaptcha.enabled' => false, - + // Your Recaptcha public key, obtained from Recaptcha. 'recaptcha.public-key' => null, - + // Your Recaptcha private key, obtained from Recaptcha. 'recaptcha.private-key' => null, 'user.default-profile-image-phid' => 'PHID-FILE-f57aaefce707fc4060ef', - + // When email is sent, try to hand it off to the MTA immediately. The only // reason to disable this is if your MTA infrastructure is completely // terrible. If you disable this option, you must run the 'metamta_mta.php' // daemon or mail won't be handed off to the MTA. 'metamta.send-immediately' => true, - + ); diff --git a/scripts/daemons/metamta/metamta_mta.php b/scripts/daemons/metamta/metamta_mta.php index e3906efd34..6e6b7b9d0f 100644 --- a/scripts/daemons/metamta/metamta_mta.php +++ b/scripts/daemons/metamta/metamta_mta.php @@ -1,3 +1,19 @@ array( - 'uri' => '/res/771b987d/rsrc/css/aphront/dialog-view.css', + 'uri' => '/res/d98e6292/rsrc/css/aphront/dialog-view.css', 'type' => 'css', 'requires' => array( @@ -188,6 +188,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/core/core.css', ), + 'phabricator-core-dialog-css' => + array( + 'uri' => '/res/d9580553/rsrc/css/core/dialog.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/core/dialog.css', + ), 'phabricator-remarkup-css' => array( 'uri' => '/res/786989c3/rsrc/css/core/remarkup.css', @@ -226,6 +235,16 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/differential/behavior-comment-preview.js', ), + 'javelin-behavior-differential-edit-inline-comments' => + array( + 'uri' => '/res/51d7da98/rsrc/js/application/differential/behavior-edit-inline-comments.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-lib-dev', + ), + 'disk' => '/rsrc/js/application/differential/behavior-edit-inline-comments.js', + ), 'javelin-behavior-differential-populate' => array( 'uri' => '/res/f7efbf62/rsrc/js/application/differential/behavior-populate.js', @@ -246,9 +265,9 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/differential/behavior-show-more.js', ), - 'javelin-init-dev' => + 'javelin-magical-init' => array( - 'uri' => '/res/c57a9e89/rsrc/js/javelin/init.dev.js', + 'uri' => '/res/76614f84/rsrc/js/javelin/init.dev.js', 'type' => 'js', 'requires' => array( @@ -257,7 +276,7 @@ celerity_register_resource_map(array( ), 'javelin-init-prod' => array( - 'uri' => '/res/f0172c54/rsrc/js/javelin/init.min.js', + 'uri' => '/res/ce6bff38/rsrc/js/javelin/init.min.js', 'type' => 'js', 'requires' => array( @@ -266,7 +285,7 @@ celerity_register_resource_map(array( ), 'javelin-lib-dev' => array( - 'uri' => '/res/3e747182/rsrc/js/javelin/javelin.dev.js', + 'uri' => '/res/29c9b6b4/rsrc/js/javelin/javelin.dev.js', 'type' => 'js', 'requires' => array( @@ -275,7 +294,7 @@ celerity_register_resource_map(array( ), 'javelin-lib-prod' => array( - 'uri' => '/res/9438670e/rsrc/js/javelin/javelin.min.js', + 'uri' => '/res/ef13c830/rsrc/js/javelin/javelin.min.js', 'type' => 'js', 'requires' => array( @@ -284,7 +303,7 @@ celerity_register_resource_map(array( ), 'javelin-typeahead-dev' => array( - 'uri' => '/res/c81c0f01/rsrc/js/javelin/typeahead.dev.js', + 'uri' => '/res/6de6ae59/rsrc/js/javelin/typeahead.dev.js', 'type' => 'js', 'requires' => array( @@ -293,17 +312,35 @@ celerity_register_resource_map(array( ), 'javelin-typeahead-prod' => array( - 'uri' => '/res/1da2d984/rsrc/js/javelin/typeahead.min.js', + 'uri' => '/res/593d9bb8/rsrc/js/javelin/typeahead.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/typeahead.min.js', ), + 'javelin-workflow-dev' => + array( + 'uri' => '/res/2d740661/rsrc/js/javelin/workflow.dev.js', + 'type' => 'js', + 'requires' => + array( + ), + 'disk' => '/rsrc/js/javelin/workflow.dev.js', + ), + 'javelin-workflow-prod' => + array( + 'uri' => '/res/b758e0a0/rsrc/js/javelin/workflow.min.js', + 'type' => 'js', + 'requires' => + array( + ), + 'disk' => '/rsrc/js/javelin/workflow.min.js', + ), ), array ( 'packages' => array ( - 'd348c79d' => + 'dc2390af' => array ( 'name' => 'core.pkg.css', 'symbols' => @@ -320,7 +357,7 @@ celerity_register_resource_map(array( 9 => 'aphront-typeahead-control-css', 10 => 'phabricator-directory-css', ), - 'uri' => '/res/pkg/d348c79d/core.pkg.css', + 'uri' => '/res/pkg/dc2390af/core.pkg.css', 'type' => 'css', ), '69b11588' => @@ -340,17 +377,17 @@ celerity_register_resource_map(array( ), 'reverse' => array ( - 'phabricator-core-css' => 'd348c79d', - 'phabricator-core-buttons-css' => 'd348c79d', - 'phabricator-standard-page-view' => 'd348c79d', - 'aphront-dialog-view-css' => 'd348c79d', - 'aphront-form-view-css' => 'd348c79d', - 'aphront-panel-view-css' => 'd348c79d', - 'aphront-side-nav-view-css' => 'd348c79d', - 'aphront-table-view-css' => 'd348c79d', - 'aphront-tokenizer-control-css' => 'd348c79d', - 'aphront-typeahead-control-css' => 'd348c79d', - 'phabricator-directory-css' => 'd348c79d', + 'phabricator-core-css' => 'dc2390af', + 'phabricator-core-buttons-css' => 'dc2390af', + 'phabricator-standard-page-view' => 'dc2390af', + 'aphront-dialog-view-css' => 'dc2390af', + 'aphront-form-view-css' => 'dc2390af', + 'aphront-panel-view-css' => 'dc2390af', + 'aphront-side-nav-view-css' => 'dc2390af', + 'aphront-table-view-css' => 'dc2390af', + 'aphront-tokenizer-control-css' => 'dc2390af', + 'aphront-typeahead-control-css' => 'dc2390af', + 'phabricator-directory-css' => 'dc2390af', 'differential-core-view-css' => '69b11588', 'differential-changeset-view-css' => '69b11588', 'differential-revision-detail-css' => '69b11588', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9a5aacd4d4..b40da8afca 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -90,6 +90,7 @@ phutil_register_library_map(array( 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents', 'DifferentialDiffViewController' => 'applications/differential/controller/diffview', 'DifferentialHunk' => 'applications/differential/storage/hunk', + 'DifferentialInlineComment' => 'applications/differential/storage/inlinecomment', 'DifferentialLintStatus' => 'applications/differential/constants/lintstatus', 'DifferentialMail' => 'applications/differential/mail/base', 'DifferentialMarkupEngineFactory' => 'applications/differential/parser/markup', @@ -264,6 +265,7 @@ phutil_register_library_map(array( 'DifferentialDiffTableOfContentsView' => 'AphrontView', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialHunk' => 'DifferentialDAO', + 'DifferentialInlineComment' => 'DifferentialDAO', 'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', 'DifferentialReviewRequestMail' => 'DifferentialMail', 'DifferentialRevision' => 'DifferentialDAO', diff --git a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php index ef077224d0..75bac79d0e 100644 --- a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php @@ -79,6 +79,8 @@ class DifferentialRevisionViewController extends DifferentialController { $changeset_view = new DifferentialChangesetListView(); $changeset_view->setChangesets($changesets); + $changeset_view->setEditable(true); + $changeset_view->setRevision($revision); $comment_form = new DifferentialAddCommentView(); $comment_form->setRevision($revision); diff --git a/src/applications/differential/storage/inlinecomment/DifferentialInlineComment.php b/src/applications/differential/storage/inlinecomment/DifferentialInlineComment.php new file mode 100644 index 0000000000..8b2446c759 --- /dev/null +++ b/src/applications/differential/storage/inlinecomment/DifferentialInlineComment.php @@ -0,0 +1,33 @@ +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'); @@ -105,20 +117,14 @@ class DifferentialChangesetListView extends AphrontView { Javelin::initBehavior('differential-show-more', array( 'uri' => '/differential/changeset/', )); -/* - - Javelin::initBehavior('differential-context', array( - 'uri' => $render_uri, - )); - - if ($edit) { - require_static('remarkup-css'); - Javelin::initBehavior('differential-inline', array( - 'uri' => '/differential/feedback/'.$revision->getID().'/', + if ($this->editable) { + $revision = $this->revision; + Javelin::initBehavior('differential-edit-inline-comments', array( + 'uri' => '/differential/inline/edit/'.$revision->getID().'/', )); } -*/ + return '
'. implode("\n", $output). diff --git a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php index 96e6730ced..6c76198c6c 100644 --- a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php @@ -114,21 +114,21 @@ class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { $this->setParam('simulated-failures', $count); return $this; } - + public function save() { $try_send = (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) && (!$this->getID()); - + $ret = parent::save(); - + if ($try_send) { $mailer = new PhabricatorMailImplementationPHPMailerLiteAdapter(); $this->sendNow($force_send = false, $mailer); } - + return $ret; } - + public function sendNow( $force_send = false, @@ -143,7 +143,7 @@ class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { throw new Exception("Trying to send an email before next retry!"); } } - + try { $parameters = $this->parameters; $phids = array(); @@ -161,10 +161,10 @@ class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { break; } } - + $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); - + foreach ($this->parameters as $key => $value) { switch ($key) { case 'from': diff --git a/src/applications/metamta/storage/mail/__init__.php b/src/applications/metamta/storage/mail/__init__.php index 43e0257082..715dad61dd 100644 --- a/src/applications/metamta/storage/mail/__init__.php +++ b/src/applications/metamta/storage/mail/__init__.php @@ -6,7 +6,10 @@ +phutil_require_module('phabricator', 'applications/metamta/adapter/phpmailerlite'); phutil_require_module('phabricator', 'applications/metamta/storage/base'); +phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phutil', 'utils'); diff --git a/src/infrastructure/javelin/markup/markup.php b/src/infrastructure/javelin/markup/markup.php index db10cf84c0..eb3f87f595 100644 --- a/src/infrastructure/javelin/markup/markup.php +++ b/src/infrastructure/javelin/markup/markup.php @@ -24,31 +24,24 @@ function javelin_render_tag( if (isset($attributes['sigil']) || isset($attributes['meta']) || isset($attributes['mustcapture'])) { - $classes = array(); foreach ($attributes as $k => $v) { switch ($k) { case 'sigil': - $classes[] = 'FN_'.$v; + $attributes['data-sigil'] = $v; unset($attributes[$k]); break; case 'meta': $response = CelerityAPI::getStaticResourceResponse(); $id = $response->addMetadata($v); - $classes[] = 'FD_'.$id; + $attributes['data-meta'] = $id; unset($attributes[$k]); break; case 'mustcapture': - $classes[] = 'FI_CAPTURE'; + $attributes['data-mustcapture'] = '1'; unset($attributes[$k]); break; } } - - if (isset($attributes['class'])) { - $classes[] = $attributes['class']; - } - - $attributes['class'] = implode(' ', $classes); } return phutil_render_tag($tag, $attributes, $content); diff --git a/src/view/dialog/AphrontDialogView.php b/src/view/dialog/AphrontDialogView.php index 600adbe158..e6225f460c 100755 --- a/src/view/dialog/AphrontDialogView.php +++ b/src/view/dialog/AphrontDialogView.php @@ -68,12 +68,13 @@ class AphrontDialogView extends AphrontView { 'Cancel'); } - return phutil_render_tag( + return javelin_render_tag( 'form', array( 'class' => 'aphront-dialog-view', 'action' => $this->submitURI, 'method' => 'post', + 'sigil' => 'jx-dialog', ), ''. '
'. diff --git a/src/view/dialog/__init__.php b/src/view/dialog/__init__.php index 8524dfbb6f..8ddedcb0bb 100644 --- a/src/view/dialog/__init__.php +++ b/src/view/dialog/__init__.php @@ -7,6 +7,7 @@ phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/markup'); phutil_require_module('phabricator', 'view/base'); phutil_require_module('phutil', 'markup'); diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php index 26a15aeeba..8c7a214603 100755 --- a/src/view/page/standard/PhabricatorStandardPageView.php +++ b/src/view/page/standard/PhabricatorStandardPageView.php @@ -70,6 +70,7 @@ class PhabricatorStandardPageView extends AphrontPageView { require_celerity_resource('phabricator-standard-page-view'); require_celerity_resource('javelin-lib-dev'); + require_celerity_resource('javelin-workflow-dev'); $this->bodyContent = $this->renderChildren(); } diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index bc80a7961f..6c47fc9c69 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -2,8 +2,6 @@ * @provides aphront-dialog-view-css */ - - .aphront-dialog-view { width: 480px; padding: 8px; @@ -40,3 +38,23 @@ margin-left: .5em; } +.jx-client-dialog { + position: absolute; + z-index: 6; +} + +.jx-mask { + opacity: .5; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)"; + filter: alpha(opacity=75); + background: #999; + position: absolute; + z-index: 5; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + min-height: 100%; +} diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 3ad2468c3f..7395e5a669 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -119,3 +119,14 @@ margin: 0.5em 0; padding: 10px 0px 20px; } + +.differential-reticle { + background: #ffeeaa; + border: 1px solid #ffcc00; + position: absolute; + z-index: 2; + opacity: 0.5; + top: 0px; + left: 0px; +} + diff --git a/webroot/rsrc/css/core/dialog.css b/webroot/rsrc/css/core/dialog.css new file mode 100644 index 0000000000..ece9beaf74 --- /dev/null +++ b/webroot/rsrc/css/core/dialog.css @@ -0,0 +1,55 @@ +/** + * @provides phabricator-core-dialog-css + */ + +.jx-dialog { + display: block; + width: 480px; + padding: 8px; + background: #666; + margin: auto; +} + +.jx-client-dialog { + position: absolute; + z-index: 6; +} + +.jx-dialog .dialog-title { + background: #6d84b4; + border: none; + font-size: 15px; + font-weight: bold; + padding: 5px 12px 6px; + color: #ffffff; +} +.jx-dialog .dialog-body { + background: #ffffff; + padding: 16px 12px; + border: none; + overflow: hidden; +} + +.jx-dialog .dialog-foot { + border: none; + background: #ededed; + padding: .5em; + text-align: right; +} + +.jx-dialog button { + margin-left: 6px; +} + +.jx-dialog input { + padding: 4px; +} + +.jx-dialog .fields { + margin-top: 10px; +} + +.jx-dialog input.block { + display: block; + margin: 3px 0 0 0; +} diff --git a/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js new file mode 100644 index 0000000000..a63526bb71 --- /dev/null +++ b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js @@ -0,0 +1,229 @@ +/** + * @provides javelin-behavior-differential-edit-inline-comments + * @requires javelin-lib-dev + */ + +JX.behavior('differential-edit-inline-comments', function(config) { + + var selecting = false; + var reticle = JX.$N('div', {className: 'differential-reticle'}); + JX.DOM.hide(reticle); + document.body.appendChild(reticle); + + var origin = null; + var target = null; + var root = null; + var changeset = null; + var workflow = false; + var is_new = false; + + function updateReticle() { + var top = origin; + var bot = target; + if (JX.$V(top).y > JX.$V(bot).y) { + var tmp = top; + top = bot; + bot = tmp; + } + + var code = target.nextSibling; + + var pos = JX.$V(top).add(1 + JX.$V.getDim(target).x, 0); + var dim = JX.$V.getDim(code).add(-4, 0); + dim.y = (JX.$V(bot).y - pos.y) + JX.$V.getDim(bot).y; + + pos.setPos(reticle); + dim.setDim(reticle); + + JX.DOM.show(reticle); + } + + function hideReticle() { + JX.DOM.hide(reticle); + } + + function finishSelect() { + selecting = false; + workflow = false; + hideReticle(); + } + + function drawInlineComment(table, anchor, r) { + copyRows(table, JX.$N('div', JX.HTML(r.markup)), anchor); + finishSelect(); + } + + function isNewFile(node) { + return node.parentNode.firstChild != node; + } + + function getRowNumber(th_node) { + try { + return parseInt(th_node.id.match(/^C\d+[ON]L(\d+)$/)[1], 10); + } catch (x) { + return undefined; + } + } + + JX.Stratcom.listen( + 'mousedown', + ['differential-changeset', 'tag:th'], + function(e) { + if (workflow || + selecting || + getRowNumber(e.getTarget()) === undefined) { + return; + } + + selecting = true; + root = e.getNode('differential-changeset'); + + origin = target = e.getTarget(); + + var data = e.getNodeData('differential-changeset'); + if (isNewFile(target)) { + changeset = data.oid; + } else { + changeset = data.nid; + } + + updateReticle(); + + e.kill(); + }); + + JX.Stratcom.listen( + 'mouseover', + ['differential-changeset', 'tag:th'], + function(e) { + if (!selecting || + workflow || + (getRowNumber(e.getTarget()) === undefined) || + (isNewFile(e.getTarget()) != isNewFile(origin)) || + (e.getNode('differential-changeset') !== root)) { + return; + } + + target = e.getTarget(); + + updateReticle(); + }); + + JX.Stratcom.listen( + 'mouseup', + null, + function(e) { + if (workflow || !selecting) { + return; + } + + var o = getRowNumber(origin); + var t = getRowNumber(target); + + var insert; + var len; + if (t < o) { + len = (o - t); + o = t; + insert = origin.parentNode; + } else { + len = (t - o); + insert = target.parentNode; + } + + var data = { + op: 'new', + changeset: changeset, + number: o, + length: len, + is_new: isNewFile(target) ? 1 : 0 + }; + + workflow = true; + + var w = new JX.Workflow(config.uri, data) + .setHandler(function(r) { + // Skip over any rows which contain inline feedback. Don't mimic this! + // We're shipping around raw HTML here for performance reasons, but + // normally you should use sigils to encode this kind of data on + // the document. + var target = insert.nextSibling; + while (target && + (!JX.DOM.isType(target, 'tr') + || target.className.indexOf('inline') !== -1)) { + target = target.nextSibling; + } + drawInlineComment(insert.parentNode, target, r); + finishSelect(); + JX.Stratcom.invoke('inline-comment-update', + null, + {id : r.inlineCommentID}); + }) + .setCloseHandler(finishSelect); + + + w.listen('error', function(e) { + // TODO: uh, tell the user I guess + finishSelect(); + JX.Stratcom.context().stop(); + }); + + w.start(); + + e.kill(); + }); + + JX.Stratcom.listen( + ['mouseover', 'mouseout'], + 'inline-comment', + function(e) { + if (selecting || workflow) { + return; + } + + if (e.getType() == 'mouseout') { + hideReticle(); + } else { + var data = e.getNodeData('inline-comment'); + var change = e.getNodeData('differential-changeset'); + + root = e.getNode('differential-changeset'); + + var prefix = 'C' + change; + + if (data.is_new) { + prefix += 'NL'; + } else { + prefix += 'OL'; + } + + origin = JX.$(prefix + data.number); + target = JX.$(prefix + (data.number + data.length)); + + updateReticle(); + } + }); + + JX.Stratcom.listen( + 'click', + [['inline-comment', 'delete'], + ['inline-comment', 'edit']], + function(e) { + var data = { + op: e.getNode('edit') ? 'edit' : 'delete', + id: e.getNodeData('inline-comment').id + }; + new JX.Workflow(config.uri, data) + .setHandler(function(r) { + var base_row = e.getNode('inline-comment').parentNode.parentNode; + if (data.op == 'edit' && r.markup) { + drawInlineComment(base_row.parentNode, base_row, r); + } + JX.DOM.remove(base_row); + JX.Stratcom.invoke('differential-inline-comment-update'); + }) + .start(); + e.kill(); + }); + +}); diff --git a/webroot/rsrc/js/javelin/init.dev.js b/webroot/rsrc/js/javelin/init.dev.js index eadfd68a8e..402f8e3b5f 100644 --- a/webroot/rsrc/js/javelin/init.dev.js +++ b/webroot/rsrc/js/javelin/init.dev.js @@ -1,4 +1,3 @@ -/** @provides javelin-init-dev */ /** * Javelin core; installs Javelin and Stratcom event delegation. * @@ -31,7 +30,6 @@ JX.__rawEventQueue = function(what) { master_event_queue.push(what); - // Evade static analysis - JX.Stratcom var Stratcom = JX['Stratcom']; if (Stratcom && Stratcom.ready) { @@ -63,7 +61,8 @@ var target = what.srcElement || what.target; if (target && (what.type in {click: 1, submit: 1}) && - (/ FI_CAPTURE /).test(' ' + target.className + ' ')) { + target.getAttribute && + target.getAttribute('data-mustcapture') === '1') { what.returnValue = false; what.preventDefault && what.preventDefault(); document.body.id = 'event_capture'; diff --git a/webroot/rsrc/js/javelin/init.min.js b/webroot/rsrc/js/javelin/init.min.js index 264f75659c..0ba48fc10f 100644 --- a/webroot/rsrc/js/javelin/init.min.js +++ b/webroot/rsrc/js/javelin/init.min.js @@ -1,2 +1,2 @@ /** @provides javelin-init-prod */ -(function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})(); \ No newline at end of file +(function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})(); diff --git a/webroot/rsrc/js/javelin/javelin.dev.js b/webroot/rsrc/js/javelin/javelin.dev.js index 7f877a124f..7fc3cfbe28 100644 --- a/webroot/rsrc/js/javelin/javelin.dev.js +++ b/webroot/rsrc/js/javelin/javelin.dev.js @@ -824,7 +824,6 @@ JX.install('Event', { * } * }); * - * * @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'. @@ -836,21 +835,17 @@ JX.install('Event', { return null; } - var c = r.keyCode; - do { - c = JX.Event._keymap[c] || null; - } while (c && JX.Event._keymap[c]) - - return c; + return JX.Event._keymap[r.keyCode] || null; }, + /** * Get the node corresponding to the specified key in this event's node map. * This is a simple helper method that makes the API for accessing nodes * less ugly. * * JX.Stratcom.listen('click', 'tag:a', function(e) { - * var a = e.getNode('nearest:a'); + * var a = e.getNode('tag:a'); * // do something with the link that was clicked * }); * @@ -862,10 +857,29 @@ JX.install('Event', { * - sigil - first node of each sigil * @task info */ - getNode: function(key) { + getNode : function(key) { return this.getNodes()[key] || null; - } + }, + + /** + * Get the metadata associated with the node that corresponds to the key + * in this event's node map. This is a simple helper method that makes + * the API for accessing metadata associated with specific nodes less ugly. + * + * JX.Stratcom.listen('click', 'tag:a', function(event) { + * var anchorData = event.getNodeData('tag:a'); + * // do something with the metadata of the link that was clicked + * }); + * + * @param string sigil or stratcom node key + * @return dict dictionary of the node's metadata + * @task info + */ + getNodeData : function(key) { + // Evade static analysis - JX.Stratcom + return JX['Stratcom'].getData(this.getNode(key)); + } }, statics : { @@ -878,10 +892,10 @@ JX.install('Event', { 38 : 'up', 39 : 'right', 40 : 'down', - 63232 : 38, - 63233 : 40, - 62234 : 37, - 62235 : 39 + 63232 : 'up', + 63233 : 'down', + 62234 : 'left', + 62235 : 'right' } }, @@ -1008,6 +1022,7 @@ JX.install('Event', { * @task listen Listening to Events * @task handle Responding to Events * @task sigil Managing Sigils + * @task meta Managing Metadata * @task internal Internals */ JX.install('Stratcom', { @@ -1016,8 +1031,6 @@ JX.install('Stratcom', { _targets : {}, _handlers : [], _need : {}, - _matchName : /\bFN_([^ ]+)/, - _matchData : /\bFD_([^ ]+)_([^ ]+)/, _auto : '*', _data : {}, _execContext : [], @@ -1039,13 +1052,13 @@ JX.install('Stratcom', { /** * Within each datablock, data is identified by a unique index. The data - * pointer on a node looks like this: + * pointer (data-meta attribute) on a node looks like this: * - * FD_1_2 + * 1_2 * * ...where 1 is the block, and 2 is the index within that block. Normally, * blocks are filled on the server side, so index allocation takes place - * there. However, when data is provided with JX.Stratcom.sigilize(), we + * there. However, when data is provided with JX.Stratcom.addData(), we * need to allocate indexes on the client. */ _dataIndex : 0, @@ -1185,9 +1198,12 @@ JX.install('Stratcom', { if (path[kk] == 'tag:#document') { throw new Error( 'JX.Stratcom.listen(..., "tag:#document", ...): ' + - 'listen for document events as "tag:window", not ' + - '"tag:#document", in order to get consistent behavior ' + - 'across browsers.'); + 'listen for all events using null, not "tag:#document"'); + } + if (path[kk] == 'tag:window') { + throw new Error( + 'JX.Stratcom.listen(..., "tag:window", ...): ' + + 'listen for window events using null, not "tag:window"'); } } if (!type_target[path[kk]]) { @@ -1219,29 +1235,27 @@ JX.install('Stratcom', { * @task internal */ dispatch : function(event) { - // TODO: simplify this :P - var target; - try { - target = event.srcElement || event.target; - if (target === window || (!target || target.nodeName == '#document')) { - target = {nodeName: 'window'}; - } - } catch (x) { - target = null; - } - var path = []; var nodes = {}; var push = function(key, node) { // we explicitly only store the first occurrence of each key - if (!(key in nodes)) { + if (!nodes.hasOwnProperty(key)) { nodes[key] = node; path.push(key); } }; + var target = event.srcElement || event.target; + + // Since you can only listen by tag, id or sigil, which are all + // attributes of an Element (the DOM interface), we unset the target + // if it isn't an Element (window and Document are Nodes but not Elements) + if (!target || !target.getAttribute) { + target = null; + } + var cursor = target; - while (cursor) { + while (cursor && cursor.getAttribute) { push('tag:' + cursor.nodeName.toLowerCase(), cursor); var id = cursor.id; @@ -1249,11 +1263,12 @@ JX.install('Stratcom', { push('id:' + id, cursor); } - var source = cursor.className || ''; - // className is an SVGAnimatedString for SVG elements, use baseVal - var token = ((source.baseVal || source).match(this._matchName) || [])[1]; - if (token) { - push(token, cursor); + var sigils = cursor.getAttribute('data-sigil'); + if (sigils) { + sigils = sigils.split(' '); + for (var ii = 0; ii < sigils.length; ii++) { + push(sigils[ii], cursor); + } } cursor = cursor.parentNode; @@ -1264,16 +1279,10 @@ JX.install('Stratcom', { etype = this._typeMap[etype]; } - var data = {}; - for (var key in nodes) { - data[key] = this.getData(nodes[key]); - } - var proxy = new JX.Event() .setRawEvent(event) .setType(etype) .setTarget(target) - .setData(data) .setNodes(nodes) .setPath(path.reverse()); @@ -1408,40 +1417,6 @@ JX.install('Stratcom', { }, - /** - * Attach a sigil (and, optionally, metadata) to a node. Note that you can - * not overwrite, remove or replace a sigil. - * - * @param Node Node without any sigil. - * @param string Sigil to name the node with. - * @param object? Optional metadata object to attach to the node. - * @return void - * @task sigil - */ - sigilize : function(node, sigil, data) { - if (__DEV__) { - if (node.className.match(this._matchName)) { - throw new Error( - 'JX.Stratcom.sigilize(, ' + sigil + ', ...): ' + - 'node already has a sigil, sigils may not be overwritten.'); - } - if (typeof data != 'undefined' && - (data === null || typeof data != 'object')) { - throw new Error( - 'JX.Stratcom.sigilize(..., ..., ): ' + - 'data to attach to node is not an object. You must use ' + - 'objects, not primitives, for metadata.'); - } - } - - if (data) { - JX.Stratcom._setData(node, data); - } - - node.className = 'FN_' + sigil + ' ' + node.className; - }, - - /** * Determine if a node has a specific sigil. * @@ -1452,75 +1427,120 @@ JX.install('Stratcom', { * @task sigil */ hasSigil : function(node, sigil) { - if (!node.className) { - // Some nodes don't have a className, notably 'document'. We hit - // 'document' when following .parentNode chains, e.g. in - // JX.DOM.nearest(), so exit early if we don't have a className to avoid - // fataling on 'node.className.match' being undefined. - return false; + if (__DEV__) { + if (!node || !node.getAttribute) { + throw new Error( + 'JX.Stratcom.hasSigil(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); + } } - return (node.className.match(this._matchName) || [])[1] == sigil; + + 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(, ...): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have sigils.'); + } + } + + var sigils = node.getAttribute('data-sigil'); + if (sigils && !JX.Stratcom.hasSigil(node, sigil)) { + sigil = sigils + ' ' + sigil; + } + + node.setAttribute('data-sigil', sigil); }, /** * Retrieve a node's metadata. * - * @param Node Node from which to retrieve data. - * @return object Data attached to the node, or an empty dictionary if - * the node has no data attached. In this case, the empty - * dictionary is set as the node's metadata -- i.e., - * subsequent calls to getData() will retrieve the same - * object. - * - * @task sigil + * @param Node Node from which to retrieve data. + * @return object Data attached to the node. If no data has been attached + * to the node yet, an empty object will be returned, but + * subsequent calls to this method will always retrieve the + * same object. + * @task meta */ getData : function(node) { if (__DEV__) { - if (!node) { + if (!node || !node.getAttribute) { throw new Error( - 'JX.Stratcom.getData(): ' + - 'you must provide a node to get associated data from.'); + 'JX.Stratcom.getData(): ' + + 'node is not an element. Most likely, you\'re passing window or ' + + 'document, which are not elements and can\'t have data.'); } } - var matches = (node.className || '').match(this._matchData); - if (matches) { - var block = this._data[matches[1]]; - var index = matches[2]; + var meta_id = (node.getAttribute('data-meta') || '').split('_'); + if (meta_id[0] && meta_id[1]) { + var block = this._data[meta_id[0]]; + var index = meta_id[1]; if (block && (index in block)) { return block[index]; } } - return JX.Stratcom._setData(node, {}); - }, - - /** - - * @task internal - */ - allocateMetadataBlock : function() { - return this._dataBlock++; - }, - - /** - * Attach metadata to a node. This data can later be retrieved through - * @{JX.Stratcom.getData()}, or @{JX.Event.getData()}. - * - * @param Node Node which data should be attached to. - * @param object Data to attach. - * @return object Attached data. - * - * @task internal - */ - _setData : function(node, data) { - if (!this._data[1]) { // data block 1 is reserved for javascript + var data = {}; + if (!this._data[1]) { // data block 1 is reserved for JavaScript this._data[1] = {}; } this._data[1][this._dataIndex] = data; - node.className = 'FD_1_' + (this._dataIndex++) + ' ' + node.className; + 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(, ...): ' + + '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(..., ): ' + + '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++; } } }); @@ -1535,7 +1555,7 @@ JX.install('Stratcom', { JX.behavior = function(name, control_function) { if (__DEV__) { - if (name in JX.behavior._behaviors) { + if (JX.behavior._behaviors.hasOwnProperty(name)) { throw new Error( 'JX.behavior("'+name+'", ...): '+ 'behavior is already registered.'); @@ -1566,7 +1586,7 @@ JX.initBehaviors = function(map) { } var configs = map[name]; if (!configs.length) { - if (name in JX.behavior._initialized) { + if (JX.behavior._initialized.hasOwnProperty(name)) { continue; } else { configs = [null]; @@ -1827,7 +1847,7 @@ JX.install('Request', { }, initialize : function() { - JX.Stratcom.listen('unload', 'tag:window', JX.Request.shutdown); + JX.Stratcom.listen('unload', null, JX.Request.shutdown); } }); @@ -2410,8 +2430,12 @@ JX.$N = function(tag, attr, content) { } if (attr.sigil) { - JX.Stratcom.sigilize(node, attr.sigil, attr.meta); + JX.Stratcom.addSigil(node, attr.sigil); delete attr.sigil; + } + + if (attr.meta) { + JX.Stratcom.addData(node, attr.meta); delete attr.meta; } @@ -2421,17 +2445,6 @@ JX.$N = function(tag, attr, content) { '$N(' + tag + ', ...): ' + 'use the key "meta" to specify metadata, not "data" or "metadata".'); } - if (attr.meta) { - throw new Error( - '$N(' + tag + ', ...): ' + - 'if you specify "meta" metadata, you must also specify a "sigil".'); - } - } - - // prevent sigil from being wiped by blind copying the className - if (attr.className) { - JX.DOM.alterClass(node, attr.className, true); - delete attr.className; } JX.copy(node, attr); @@ -2595,7 +2608,7 @@ JX.install('DOM', { * @author jgabbard */ nearest : function(node, sigil) { - while (node && !JX.Stratcom.hasSigil(node, sigil)) { + while (node && node.getAttribute && !JX.Stratcom.hasSigil(node, sigil)) { node = node.parentNode; } return node; diff --git a/webroot/rsrc/js/javelin/javelin.min.js b/webroot/rsrc/js/javelin/javelin.min.js index e199548c52..596bfadc85 100644 --- a/webroot/rsrc/js/javelin/javelin.min.js +++ b/webroot/rsrc/js/javelin/javelin.min.js @@ -1,2 +1,3 @@ /** @provides javelin-lib-prod */ -JX.$A=function(b){var c=[];for(var a=0;a=300){this._z();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._z();return;}try{if(response.error){this._z(response.error);}else{JX.Stratcom.mergeData(this._v,response.javelin_metadata||{});this._zb(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_z:function(a){this._za();this.invoke('error',a,this);this.invoke('finally');},_zb:function(b){this._za();if(b.onload)for(var a=0;a-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>');},show:function(){for(var a=0;a')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a-1;},addSigil:function(a,b){var c=a.getAttribute('data-sigil');if(c&&!JX.Stratcom.hasSigil(a,b))b=c+' '+b;a.setAttribute('data-sigil',b);},getData:function(e){var d=(e.getAttribute('data-meta')||'').split('_');if(d[0]&&d[1]){var a=this._h[d[0]];var c=d[1];if(a&&(c in a))return a[c];}var b={};if(!this._h[1])this._h[1]={};this._h[1][this._l]=b;e.setAttribute('data-meta','1_'+(this._l++));return b;},addData:function(b,a){return JX.copy(JX.Stratcom.getData(b),a);},allocateMetadataBlock:function(){return this._k++;}}});JX.behavior=function(b,a){JX.behavior._n[b]=a;};JX.initBehaviors=function(c){for(var d in c){var a=c[d];if(!a.length)if(JX.behavior._o.hasOwnProperty(d)){continue;}else a=[null];for(var b=0;b=300){this._w();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._w();return;}try{if(response.error){this._w(response.error);}else{JX.Stratcom.mergeData(this._s,response.javelin_metadata||{});this._y(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_w:function(a){this._x();this.invoke('error',a,this);this.invoke('finally');},_y:function(b){this._x();if(b.onload)for(var a=0;a-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>');},show:function(){for(var a=0;a')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a=0&&this._i=0&&this._d[this._i]){this._f(this._d[this._i]);return true;}else{result=this.invoke('query',this._b.value);if(result.getPrevented())return true;}return false;},setValue:function(a){this._b.value=a;},getValue:function(){return this._b.value;},_m:function(event){var a=event&&event.getSpecialKey();if(a&&event.getType()=='keydown')switch(a){case 'up':if(this._d.length&&this._j(-1))event.prevent();break;case 'down':if(this._d.length&&this._j(1))event.prevent();break;case 'return':if(this.submit()){event.prevent();return;}break;case 'esc':if(this._d.length&&this.getAllowNullSelection()){this.hide();event.prevent();}break;case 'tab':return;}JX.defer(JX.bind(this,function(){if(this._g==this._b.value)return;this.refresh();}));},handleEvent:function(a){if(this._h||a.getPrevented())return;var b=a.getType();if(b=='blur'){this.hide();}else this._m(a);}}});JX.install('TypeaheadNormalizer',{statics:{normalize:function(a){return (''+a).toLowerCase().replace(/[^a-z0-9 ]/g,'').replace(/ +/g,' ').replace(/^\s*|\s*$/g,'');}}});JX.install('TypeaheadSource',{construct:function(){this._n={};this._o={};this.setNormalizer(JX.TypeaheadNormalizer.normalize);},properties:{normalizer:null,transformer:null,maximumResultCount:5},members:{_n:null,_o:null,_p:null,_q:null,bindToTypeahead:function(a){this._p=a;a.listen('change',JX.bind(this,this.didChange));a.listen('start',JX.bind(this,this.didStart));},didChange:function(a){return;},didStart:function(){return;},addResult:function(b){b=(this.getTransformer()||this._r)(b);if(b.id in this._n)return;this._n[b.id]=b;var c=this.tokenize(b.name);for(var a=0;a=0&&this._i=0&&this._d[this._i]){this._f(this._d[this._i]);return true;}else{result=this.invoke('query',this._b.value);if(result.getPrevented())return true;}return false;},setValue:function(a){this._b.value=a;},getValue:function(){return this._b.value;},_m:function(event){var a=event&&event.getSpecialKey();if(a&&event.getType()=='keydown')switch(a){case 'up':if(this._d.length&&this._j(-1))event.prevent();break;case 'down':if(this._d.length&&this._j(1))event.prevent();break;case 'return':if(this.submit()){event.prevent();return;}break;case 'esc':if(this._d.length&&this.getAllowNullSelection()){this.hide();event.prevent();}break;case 'tab':return;}JX.defer(JX.bind(this,function(){if(this._g==this._b.value)return;this.refresh();}));},handleEvent:function(a){if(this._h||a.getPrevented())return;var b=a.getType();if(b=='blur'){this.hide();}else this._m(a);}}});JX.install('TypeaheadNormalizer',{statics:{normalize:function(a){return (''+a).toLowerCase().replace(/[^a-z0-9 ]/g,'').replace(/ +/g,' ').replace(/^\s*|\s*$/g,'');}}});JX.install('TypeaheadSource',{construct:function(){this._n={};this._o={};this.setNormalizer(JX.TypeaheadNormalizer.normalize);},properties:{normalizer:null,transformer:null,maximumResultCount:5},members:{_n:null,_o:null,_p:null,_q:null,bindToTypeahead:function(a){this._p=a;a.listen('change',JX.bind(this,this.didChange));a.listen('start',JX.bind(this,this.didStart));},didChange:function(a){return;},didStart:function(){return;},addResult:function(b){b=(this.getTransformer()||this._r)(b);if(b.id in this._n)return;this._n[b.id]=b;var c=this.tokenize(b.name);for(var a=0;a, ...): '+ + 'bogus URI provided when creating workflow.'); + } + } + this.setURI(uri); + this.setData(data || {}); + }, + + events : ['error', 'finally', 'submit'], + + statics : { + _stack : [], + newFromForm : function(form, data) { + var inputs = [].concat( + JX.DOM.scry(form, 'input'), + JX.DOM.scry(form, 'button'), + JX.DOM.scry(form, 'textarea')); + + for (var ii = 0; ii < inputs.length; ii++) { + if (inputs[ii].disabled) { + delete inputs[ii]; + } else { + inputs[ii].disabled = true; + } + } + + var workflow = new JX.Workflow( + form.getAttribute('action'), + JX.copy(data || {}, JX.DOM.serialize(form))); + workflow.setMethod(form.getAttribute('method')); + workflow.listen('finally', function() { + for (var ii = 0; ii < inputs.length; ii++) { + inputs[ii] && (inputs[ii].disabled = false); + } + }); + return workflow; + }, + newFromLink : function(link) { + var workflow = new JX.Workflow(link.href); + return workflow; + }, + _push : function(workflow) { + JX.Mask.show(); + JX.Workflow._stack.push(workflow); + }, + _pop : function() { + var dialog = JX.Workflow._stack.pop(); + (dialog.getCloseHandler() || JX.bag)(); + dialog._destroy(); + JX.Mask.hide(); + }, + disable : function() { + JX.Workflow._disabled = true; + }, + _onbutton : function(event) { + + if (JX.Stratcom.pass()) { + return; + } + + if (JX.Workflow._disabled) { + return; + } + var t = event.getTarget(); + if (t.name == '__cancel__' || t.name == '__close__') { + JX.Workflow._pop(); + } else { + + var form = event.getNode('jx-dialog'); + var data = JX.DOM.serialize(form); + data[t.name] = true; + data.__wflow__ = true; + + var active = JX.Workflow._stack[JX.Workflow._stack.length - 1]; + var e = active.invoke('submit', {form: form, data: data}); + if (!e.getStopped()) { + active._destroy(); + active + .setURI(form.getAttribute('action') || active.getURI()) + .setData(data) + .start(); + } + } + event.prevent(); + } + }, + + members : { + _root : null, + _pushed : false, + _onload : function(r) { + // It is permissible to send back a falsey redirect to force a page + // reload, so we need to take this branch if the key is present. + if (r && (typeof r.redirect != 'undefined')) { + JX.go(r.redirect, true); + } else if (r && r.dialog) { + this._push(); + this._root = JX.$N( + 'div', + {className: 'jx-client-dialog'}, + JX.HTML(r.dialog)); + JX.DOM.listen( + this._root, + 'click', + 'tag:button', + JX.Workflow._onbutton); + document.body.appendChild(this._root); + var d = JX.$V.getDim(this._root); + var v = JX.$V.getViewport(); + var s = JX.$V.getScroll(); + JX.$V((v.x - d.x) / 2, s.y + 100).setPos(this._root); + try { + JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__')); + var inputs = JX.DOM.scry(this._root, 'input') + .concat(JX.DOM.scry(this._root, 'textarea')); + var miny = Number.POSITIVE_INFINITY; + var target = null; + for (var ii = 0; ii < inputs.length; ++ii) { + if (inputs[ii].type != 'hidden') { + // Find the topleft-most displayed element. + var p = JX.$V(inputs[ii]); + if (p.y < miny) { + miny = p.y; + target = inputs[ii]; + } + } + } + target && JX.DOM.focus(target); + } catch (_ignored) {} + } else if (this.getHandler()) { + this.getHandler()(r); + this._pop(); + } else if (r) { + if (__DEV__) { + throw new Error('Response to workflow request went unhandled.'); + } + } + }, + _push : function() { + if (!this._pushed) { + this._pushed = true; + JX.Workflow._push(this); + } + }, + _pop : function() { + if (this._pushed) { + this._pushed = false; + JX.Workflow._pop(); + } + }, + _destroy : function() { + if (this._root) { + JX.DOM.remove(this._root); + this._root = null; + } + }, + start : function() { + var uri = this.getURI(); + var method = this.getMethod(); + var r = new JX.Request(uri, JX.bind(this, this._onload)); + r.setData(this.getData()); + r.setDataSerializer(this.getDataSerializer()); + if (method) { + r.setMethod(method); + } + r.listen('finally', JX.bind(this, this.invoke, 'finally')); + r.listen('error', JX.bind(this, function(error) { + var e = this.invoke('error', error); + if (e.getStopped()) { + return; + } + // TODO: Default error behavior? On Facebook Lite, we just shipped the + // user to "/error/". We could emit a blanket 'workflow-failed' type + // event instead. + })); + r.send(); + } + }, + + properties : { + handler : null, + closeHandler : null, + data : null, + dataSerializer : null, + method : null, + URI : null + } + +}); diff --git a/webroot/rsrc/js/javelin/workflow.min.js b/webroot/rsrc/js/javelin/workflow.min.js new file mode 100644 index 0000000000..33b51ba0df --- /dev/null +++ b/webroot/rsrc/js/javelin/workflow.min.js @@ -0,0 +1,3 @@ +/** @provides javelin-workflow-prod */ + +JX.install('Mask',{statics:{_a:0,_b:null,show:function(){if(!JX.Mask._a){JX.Mask._b=JX.$N('div',{className:'jx-mask'});document.body.appendChild(JX.Mask._b);JX.$V.getDocument().setDim(JX.Mask._b);}++JX.Mask._a;},hide:function(){--JX.Mask._a;if(!JX.Mask._a){JX.DOM.remove(JX.Mask._b);JX.Mask._b=null;}}}});JX.install('Workflow',{construct:function(b,a){this.setURI(b);this.setData(a||{});},events:['error','finally','submit'],statics:{_c:[],newFromForm:function(b,a){var d=[].concat(JX.DOM.scry(b,'input'),JX.DOM.scry(b,'button'),JX.DOM.scry(b,'textarea'));for(var c=0;c