diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php index 5b200ed4f1..31e6b2b087 100644 --- a/src/infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php +++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php @@ -1,67 +1,75 @@ inlineComments = $comments; return $this; } public function getInlineComments() { return $this->inlineComments; } public function setOwnerPHID($owner_phid) { $this->ownerPHID = $owner_phid; return $this; } public function getOwnerPHID() { return $this->ownerPHID; } public function render() { $viewer = $this->getViewer(); + $config = array( + 'pht' => array( + 'view' => pht('View'), + ), + ); + + Javelin::initBehavior('diff-preview-link', $config); + $inlines = $this->getInlineComments(); foreach ($inlines as $key => $inline) { $inlines[$key] = DifferentialInlineComment::newFromModernComment( $inline); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($inlines as $inline) { $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); $owner_phid = $this->getOwnerPHID(); $handles = $viewer->loadHandles(array($viewer->getPHID())); $handles = iterator_to_array($handles); $views = array(); foreach ($inlines as $inline) { $views[] = id(new PHUIDiffInlineCommentDetailView()) ->setUser($viewer) ->setInlineComment($inline) ->setMarkupEngine($engine) ->setHandles($handles) ->setEditable(false) ->setPreview(true) ->setCanMarkDone(false) ->setObjectOwnerPHID($owner_phid); } return $views; } } diff --git a/webroot/rsrc/js/application/diff/behavior-preview-link.js b/webroot/rsrc/js/application/diff/behavior-preview-link.js new file mode 100644 index 0000000000..71cd2ecf04 --- /dev/null +++ b/webroot/rsrc/js/application/diff/behavior-preview-link.js @@ -0,0 +1,36 @@ +/** + * @provides javelin-behavior-diff-preview-link + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + */ + +JX.behavior('diff-preview-link', function(config, statics) { + if (statics.initialized) { + return; + } + statics.initialized = true; + + var pht = JX.phtize(config.pht); + + // After inline comment previews are rendered, hook up the links to the + // comments that are visible on the current page. + function link_inline_preview(e) { + var root = e.getData().rootNode; + var links = JX.DOM.scry(root, 'a', 'differential-inline-preview-jump'); + + for (var ii = 0; ii < links.length; ii++) { + var data = JX.Stratcom.getData(links[ii]); + try { + JX.$(data.anchor); + links[ii].href = '#' + data.anchor; + JX.DOM.setContent(links[ii], pht('view')); + } catch (ignored) { + // This inline comment isn't visible, e.g. on some other diff. + } + } + + } + + JX.Stratcom.listen('EditEngine.didCommentPreview', null, link_inline_preview); +}); diff --git a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js index e1c8aee533..3383b26a24 100644 --- a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js +++ b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js @@ -1,214 +1,228 @@ /** * @provides javelin-behavior-comment-actions * @requires javelin-behavior * javelin-stratcom * javelin-workflow * javelin-dom * phuix-form-control-view * phuix-icon-view * javelin-behavior-phabricator-gesture */ JX.behavior('comment-actions', function(config) { var action_map = config.actions; var action_node = JX.$(config.actionID); var form_node = JX.$(config.formID); var input_node = JX.$(config.inputID); var place_node = JX.$(config.placeID); var rows = {}; JX.DOM.listen(action_node, 'change', null, function() { var option = find_option(action_node.value); action_node.value = '+'; if (option) { add_row(option); } }); function find_option(key) { var options = action_node.options; var option; for (var ii = 0; ii < options.length; ii++) { option = options[ii]; if (option.value == key) { return option; } } return null; } function add_row(option) { var action = action_map[option.value]; if (!action) { return; } option.disabled = true; var icon = new JX.PHUIXIconView() .setIcon('fa-times-circle'); var remove = JX.$N('a', {href: '#'}, icon.getNode()); var control = new JX.PHUIXFormControl() .setLabel(action.label) .setError(remove) .setControl(action.type, action.spec) .setClass('phui-comment-action'); var node = control.getNode(); JX.Stratcom.addSigil(node, 'touchable'); var remove_action = function() { JX.DOM.remove(node); delete rows[action.key]; option.disabled = false; }; JX.DOM.listen(node, 'gesture.swipe.end', null, function(e) { var data = e.getData(); if (data.direction != 'left') { // Didn't swipe left. return; } if (data.length <= (JX.Vector.getDim(node).x / 2)) { // Didn't swipe far enough. return; } remove_action(); }); rows[action.key] = control; JX.DOM.listen(remove, 'click', null, function(e) { e.kill(); remove_action(); }); place_node.parentNode.insertBefore(node, place_node); return control; } function serialize_actions() { var data = []; for (var k in rows) { data.push({ type: k, value: rows[k].getValue(), initialValue: action_map[k].initialValue || null }); } return JX.JSON.stringify(data); } function get_data() { var data = JX.DOM.convertFormToDictionary(form_node); data.__preview__ = 1; data[input_node.name] = serialize_actions(); return data; } function restore_draft_actions(drafts) { var draft; var option; var control; for (var ii = 0; ii < drafts.length; ii++) { draft = drafts[ii]; option = find_option(draft); if (!option) { continue; } control = add_row(option); } } function onresponse(response) { if (JX.Device.getDevice() != 'desktop') { return; } var panel = JX.$(config.panelID); if (!response.xactions.length) { JX.DOM.hide(panel); } else { + var preview_root = JX.$(config.timelineID); JX.DOM.setContent( - JX.$(config.timelineID), + preview_root, [ JX.$H(response.xactions.join('')), JX.$H(response.previewContent) ]); JX.DOM.show(panel); + + // NOTE: Resonses are currently processed before associated behaviors are + // registered. We need to defer invoking this event so that any behaviors + // accompanying the response are registered. + var invoke_preview = function() { + JX.Stratcom.invoke( + 'EditEngine.didCommentPreview', + null, + { + rootNode: preview_root + }); + }; + setTimeout(invoke_preview, 0); } } JX.DOM.listen(form_node, ['submit', 'didSyntheticSubmit'], null, function() { input_node.value = serialize_actions(); }); if (config.showPreview) { var request = new JX.PhabricatorShapedRequest( config.actionURI, onresponse, get_data); var trigger = JX.bind(request, request.trigger); JX.DOM.listen(form_node, 'keydown', null, trigger); var always_trigger = function() { new JX.Request(config.actionURI, onresponse) .setData(get_data()) .send(); }; JX.DOM.listen(form_node, 'shouldRefresh', null, always_trigger); request.start(); var old_device = JX.Device.getDevice(); var ondevicechange = function() { var new_device = JX.Device.getDevice(); var panel = JX.$(config.panelID); if (new_device == 'desktop') { request.setRateLimit(500); // Force an immediate refresh if we switched from another device type // to desktop. if (old_device != new_device) { always_trigger(); } } else { // On mobile, don't show live previews and only save drafts every // 10 seconds. request.setRateLimit(10000); JX.DOM.hide(panel); } old_device = new_device; }; ondevicechange(); JX.Stratcom.listen('phabricator-device-change', null, ondevicechange); } restore_draft_actions(config.drafts || []); });