diff --git a/src/applications/pholio/view/PholioInlineCommentView.php b/src/applications/pholio/view/PholioInlineCommentView.php index 65f3626065..eb2707083f 100644 --- a/src/applications/pholio/view/PholioInlineCommentView.php +++ b/src/applications/pholio/view/PholioInlineCommentView.php @@ -1,95 +1,99 @@ getImageID() === null) { throw new Exception("Comment provided is not inline comment"); } $this->inlineComment = $inline_comment; return $this; } public function setHandle(PhabricatorObjectHandle $handle) { $this->handle = $handle; return $this; } public function setEditable($editable) { $this->editable = $editable; return $this; } public function render() { if (!$this->inlineComment) { throw new Exception("Call setInlineComment() before render()!"); } $actions = null; if ($this->editable) { $edit_action = javelin_tag( 'a', array( 'href' => '/pholio/inline/edit/'.$this->inlineComment->getID(), 'sigil' => 'inline-edit', 'meta' => array( 'phid' => $this->inlineComment->getPHID(), 'id' => $this->inlineComment->getID() ) ), pht('Edit')); $delete_action = javelin_tag( 'a', array( 'href' => '/pholio/inline/delete/'.$this->inlineComment->getID(), 'sigil' => 'inline-delete', 'meta' => array( 'phid' => $this->inlineComment->getPHID(), 'id' => $this->inlineComment->getID() ) ), pht('Delete')); $actions = phutil_tag( 'span', array( 'class' => 'pholio-inline-head-links' ), array($edit_action, $delete_action)); } $comment_header = phutil_tag( 'div', array( 'class' => 'pholio-inline-comment-header' ), array($this->handle->getName(), $actions)); $comment_body = phutil_tag( 'div', array( ), $this->inlineComment->getContent()); $comment_block = javelin_tag( 'div', array( 'id' => $this->inlineComment->getPHID()."_comment", - 'class' => 'pholio-inline-comment' + 'class' => 'pholio-inline-comment', + 'sigil' => 'inline_comment', + 'meta' => array( + 'phid' => $this->inlineComment->getPHID() + ) ), array($comment_header, $comment_body)); return $this->renderSingleView($comment_block); } } diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 7fe3a2eb09..1764244dd5 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -1,118 +1,119 @@ mock = $mock; return $this; } public function render() { if (!$this->mock) { throw new Exception("Call setMock() before render()!"); } $mock = $this->mock; require_celerity_resource('javelin-behavior-pholio-mock-view'); $images = array(); $panel_id = celerity_generate_unique_node_id(); $viewport_id = celerity_generate_unique_node_id(); foreach ($mock->getImages() as $image) { $images[] = array( 'id' => $image->getID(), 'fullURI' => $image->getFile()->getBestURI(), ); } $config = array( 'mockID' => $mock->getID(), 'panelID' => $panel_id, 'viewportID' => $viewport_id, 'images' => $images, ); Javelin::initBehavior('pholio-mock-view', $config); $mockview = ''; $mock_wrapper = phutil_tag( 'div', array( 'id' => $viewport_id, 'class' => 'pholio-mock-image-viewport' ), ''); $mock_wrapper = javelin_tag( 'div', array( 'id' => $panel_id, 'sigil' => 'mock-panel', 'class' => 'pholio-mock-image-panel', ), $mock_wrapper); $inline_comments_holder = javelin_tag( 'div', array( 'id' => 'mock-inline-comments', 'sigil' => 'mock-inline-comments', 'class' => 'pholio-mock-inline-comments' ), ''); $mockview[] = phutil_tag( 'div', array( 'class' => 'pholio-mock-image-container', 'id' => 'pholio-mock-image-container' ), array($mock_wrapper, $inline_comments_holder)); if (count($mock->getImages()) > 1) { $thumbnails = array(); foreach ($mock->getImages() as $image) { $thumbfile = $image->getFile(); $dimensions = PhabricatorImageTransformer::getPreviewDimensions( $thumbfile, 140); $tag = phutil_tag( 'img', array( 'width' => $dimensions['sdx'], 'height' => $dimensions['sdy'], 'src' => $thumbfile->getPreview140URI(), 'class' => 'pholio-mock-carousel-thumbnail', 'style' => 'top: '.floor((140 - $dimensions['sdy'] ) / 2).'px', )); $thumbnails[] = javelin_tag( 'div', array( 'sigil' => 'mock-thumbnail', 'class' => 'pholio-mock-carousel-thumb-item', 'meta' => array( 'imageID' => $image->getID(), ), ), $tag); } $mockview[] = phutil_tag( 'div', array( + 'id' => 'pholio-mock-carousel', 'class' => 'pholio-mock-carousel', ), $thumbnails); } return $this->renderSingleView($mockview); } } diff --git a/webroot/rsrc/css/application/pholio/pholio.css b/webroot/rsrc/css/application/pholio/pholio.css index bbda5bdc1a..a742015bd9 100644 --- a/webroot/rsrc/css/application/pholio/pholio.css +++ b/webroot/rsrc/css/application/pholio/pholio.css @@ -1,104 +1,104 @@ /** * @provides pholio-css */ .pholio-mock-image-container { background-color: #282828; text-align: center; vertical-align: middle; position: relative; } .pholio-mock-carousel { background-color: #202020; text-align: center; border-top: 1px solid #101010; } .pholio-mock-carousel-thumb-item { display: inline-block; cursor: pointer; width: 140px; height: 140px; padding: 5px; margin: 3px; background: #282828; vertical-align: middle; border: 1px solid #383838; position: relative; } -.device-desktop .pholio-mock-carousel-thumb-item:hover { +.device-desktop .pholio-mock-carousel-thumb-item:hover, + .pholio-mock-carousel-thumb-current { background: #383838; border-color: #686868; } .pholio-mock-carousel-thumbnail { margin: auto; position: relative; } .pholio-mock-image { margin: auto; cursor: crosshair; } .pholio-mock-select-border { position: absolute; background: #ffffff; opacity: 0.25; box-sizing: border-box; border: 1px solid #000000; } .pholio-mock-select-fill { position: absolute; border: 1px dashed #ffffff; box-sizing: border-box; } .pholio-mock-image-panel { padding: 20px; margin-right: 320px; } .pholio-mock-image-viewport { position: relative; margin: auto; display: inline-block; } .pholio-mock-inline-comments { width: 320px; border-left: 1px solid #101010; position: absolute; top: 0; bottom: 0; right: 0; overflow-x: auto; text-align: left; } .pholio-inline-comment { border: 1px solid #aa8; background: #f9f9f1; margin: 2px; padding: 8px 10px; } .pholio-inline-comment-header { border-bottom: 1px solid #cca; color: #333; font-weight: bold; padding-bottom: 6px; margin-bottom: 4px; } .pholio-mock-inline-comment-highlight { background-color: #F0B160; } .pholio-inline-head-links a { font-weight: normal; margin-left: 5px; } - diff --git a/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js b/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js index cc8c48ce42..6ee7ab7833 100644 --- a/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js +++ b/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js @@ -1,469 +1,497 @@ /** * @provides javelin-behavior-pholio-mock-view * @requires javelin-behavior * javelin-util * javelin-stratcom * javelin-dom * javelin-vector * javelin-magical-init * javelin-request */ JX.behavior('pholio-mock-view', function(config) { var is_dragging = false; var drag_begin; var drag_end; var panel = JX.$(config.panelID); var viewport = JX.$(config.viewportID); var selection_border; var selection_fill; var active_image; var inline_comments = {}; function get_image(id) { for (var ii = 0; ii < config.images.length; ii++) { if (config.images[ii].id == id) { return config.images[ii]; } } return null; } function onload_image(id) { if (active_image.id != id) { // The user has clicked another image before this one loaded, so just // bail. return; } active_image.tag = this; redraw_image(); } function redraw_image() { if (!active_image || !active_image.tag) { return; } var tag = active_image.tag; // If the image is too wide for the viewport, scale it down so it fits. // (If it is too tall, we just let the user scroll.) var w = JX.Vector.getDim(panel); w.x -= 40; if (w.x < tag.naturalWidth) { var scale = w.x / tag.naturalWidth; tag.width = Math.floor(scale * tag.naturalWidth); tag.height = Math.floor(scale * tag.naturalHeight); } else { tag.width = tag.naturalWidth; tag.height = tag.naturalHeight; } var new_y = (JX.Vector.getViewport().y * 0.85) - 150; new_y = Math.max(320, new_y); if (tag.height + 40 < new_y) { panel.style.height = new_y + 'px'; viewport.style.top = Math.floor(((new_y + 40) - tag.height) / 2) + 'px'; } else { panel.style.height = ''; viewport.style.top = ''; } // NOTE: This also clears inline comment reticles. JX.DOM.setContent(viewport, tag); redraw_inlines(active_image.id); } function select_image(image_id) { active_image = get_image(image_id); active_image.tag = null; var img = JX.$N('img', {className: 'pholio-mock-image'}); img.onload = JX.bind(img, onload_image, active_image.id); img.src = active_image.fullURI; + var thumbs = JX.DOM.scry( + JX.$('pholio-mock-carousel'), + 'div', + 'mock-thumbnail'); + + for(var k in thumbs) { + var thumb_meta = JX.Stratcom.getData(thumbs[k]); + + JX.DOM.alterClass( + thumbs[k], + 'pholio-mock-carousel-thumb-current', + (active_image.id == thumb_meta.imageID)); + } + load_inline_comments(); } JX.Stratcom.listen( 'click', 'mock-thumbnail', function(e) { e.kill(); select_image(e.getNodeData('mock-thumbnail').imageID); }); // Select and show the first image. select_image(config.images[0].id); JX.Stratcom.listen('mousedown', 'mock-panel', function(e) { if (!e.isNormalMouseEvent()) { return; } if (drag_begin) { return; } e.kill(); is_dragging = true; drag_begin = get_image_xy(JX.$V(e)); drag_end = drag_begin; redraw_selection(); }); JX.enableDispatch(document.body, 'mousemove'); JX.Stratcom.listen('mousemove', null, function(e) { if (!is_dragging) { return; } drag_end = get_image_xy(JX.$V(e)); redraw_selection(); }); JX.Stratcom.listen( ['mouseover', 'mouseout'], 'image_selection', function(e) { var data = e.getNodeData('image_selection'); var comment = JX.$(data.phid + "_comment"); var highlight = (e.getType() == 'mouseover'); JX.DOM.alterClass( comment, 'pholio-mock-inline-comment-highlight', highlight); }); + JX.Stratcom.listen( + ['mouseover', 'mouseout'], + 'inline_comment', + function(e) { + var data = e.getNodeData('inline_comment'); + var selection = JX.$(data.phid + "_selection"); + var highlight = (e.getType() == 'mouseover'); + + JX.DOM.alterClass( + selection, + 'pholio-mock-inline-comment-highlight', + highlight); + }); + JX.Stratcom.listen( 'mouseup', null, function(e) { if (!is_dragging) { return; } is_dragging = false; drag_end = get_image_xy(JX.$V(e)); var create_inline = new JX.Request("/pholio/inline/save/", function(r) { JX.DOM.appendContent(JX.$('pholio-mock-image-container'), JX.$H(r)); var dialog = JX.$('pholio-new-inline-comment-dialog'); var viewportVector = JX.$V(viewport); var viewportDimensions = JX.Vector.getDim(viewport); JX.$V( // TODO: This is a little funky for now. Math.max(drag_begin.x, drag_end.x), Math.max(drag_begin.y, drag_end.y) ).setPos(dialog); }); create_inline.addData({mockID: config.mockID}); create_inline.send(); }); function redraw_inlines(id) { if (!active_image) { return; } if (active_image.id != id) { return; } var comment_holder = JX.$('mock-inline-comments'); JX.DOM.setContent(comment_holder, ''); var inlines = inline_comments[active_image.id]; if (!inlines || !inlines.length) { return; } for (var ii = 0; ii < inlines.length; ii++) { var inline = inlines[ii]; JX.DOM.appendContent(comment_holder, JX.$H(inline.contentHTML)); if (!active_image.tag) { // The image itself hasn't loaded yet, so we can't draw the inline // reticles. continue; } var inlineSelection = JX.$N( 'div', { id: inline.phid + "_selection", className: 'pholio-mock-select-border' }); JX.Stratcom.addData( inlineSelection, {phid: inline.phid}); JX.Stratcom.addSigil(inlineSelection, "image_selection"); JX.DOM.appendContent(viewport, inlineSelection); position_inline_rectangle(inline, inlineSelection); if (!inline.transactionphid) { var inlineDraft = JX.$N( 'div', { className: 'pholio-mock-select-fill', id: inline.phid + "_fill" }); position_inline_rectangle(inline, inlineDraft); JX.Stratcom.addData( inlineDraft, {phid: inline.phid}); JX.Stratcom.addSigil(inlineDraft, "image_selection"); JX.DOM.appendContent(viewport, inlineDraft); } } } function position_inline_rectangle(inline, rect) { var scale = active_image.tag.width / active_image.tag.naturalWidth; JX.$V(scale * inline.x, scale * inline.y).setPos(rect); JX.$V(scale * inline.width, scale * inline.height).setDim(rect); } function get_image_xy(p) { var img = active_image.tag; var imgp = JX.$V(img); var scale = 1 / get_image_scale(); var x = scale * Math.max(0, Math.min(p.x - imgp.x, img.width)); var y = scale * Math.max(0, Math.min(p.y - imgp.y, img.height)); return { x: x, y: y }; } function get_image_scale() { var img = active_image.tag; return img.width / img.naturalWidth; } function redraw_selection() { selection_border = selection_border || JX.$N( 'div', {className: 'pholio-mock-select-border'}); selection_fill = selection_fill || JX.$N( 'div', {className: 'pholio-mock-select-fill'}); var p = JX.$V( Math.min(drag_begin.x, drag_end.x), Math.min(drag_begin.y, drag_end.y)); var d = JX.$V( Math.max(drag_begin.x, drag_end.x) - p.x, Math.max(drag_begin.y, drag_end.y) - p.y); var scale = get_image_scale(); p.x *= scale; p.y *= scale; d.x *= scale; d.y *= scale; var nodes = [selection_border, selection_fill]; for (var ii = 0; ii < nodes.length; ii++) { var node = nodes[ii]; viewport.appendChild(node); p.setPos(node); d.setDim(node); } } function clear_selection() { selection_fill && JX.DOM.remove(selection_fill); selection_border && JX.DOM.remove(selection_border); } function load_inline_comments() { var comment_holder = JX.$('mock-inline-comments'); JX.DOM.setContent(comment_holder, ''); var id = active_image.id; var inline_comments_uri = "/pholio/inline/" + id + "/"; new JX.Request(inline_comments_uri, function(r) { inline_comments[id] = r; redraw_inlines(id); }).send(); } JX.Stratcom.listen( 'click', 'inline-delete', function(e) { var data = e.getNodeData('inline-delete'); e.kill(); interrupt_typing(); JX.DOM.hide( JX.$(data.phid + "_comment"), JX.$(data.phid + "_fill"), JX.$(data.phid + "_selection")); var deleteURI = '/pholio/inline/delete/' + data.id + '/'; var del = new JX.Request(deleteURI, function(r) { }); del.send(); }); JX.Stratcom.listen( 'click', 'inline-edit', function(e) { var data = e.getNodeData('inline-edit'); e.kill(); interrupt_typing(); var editURI = "/pholio/inline/edit/" + data.id + '/'; var edit_dialog = new JX.Request(editURI, function(r) { var dialog = JX.$N( 'div', { className: 'pholio-edit-inline-popup' }, JX.$H(r)); JX.DOM.setContent(JX.$(data.phid + '_comment'), dialog); }); edit_dialog.send(); }); JX.Stratcom.listen( 'click', 'inline-edit-cancel', function(e) { var data = e.getNodeData('inline-edit-cancel'); e.kill(); load_inline_comment(data.id); }); JX.Stratcom.listen( 'click', 'inline-edit-submit', function(e) { var data = e.getNodeData('inline-edit-submit'); var editURI = "/pholio/inline/edit/" + data.id + '/'; e.kill(); var edit = new JX.Request(editURI, function(r) { load_inline_comment(data.id); }); edit.addData({ op: 'update', content: JX.DOM.find(JX.$(data.phid + '_comment'), 'textarea').value }); edit.send(); }); JX.Stratcom.listen( 'click', 'inline-save-cancel', function(e) { e.kill(); interrupt_typing(); } ); JX.Stratcom.listen( 'click', 'inline-save-submit', function(e) { e.kill(); var new_content = JX.DOM.find( JX.$('pholio-new-inline-comment-dialog'), 'textarea').value; if (new_content == null || new_content.length == 0) { alert("Empty comment") return; } var saveURI = "/pholio/inline/save/"; var inlineComment = new JX.Request(saveURI, function(r) { if (!inline_comments[active_image.id]) { inline_comments[active_image.id] = []; } inline_comments[active_image.id].push(r); interrupt_typing(); redraw_inlines(active_image.id); }); var commentToAdd = { mockID: config.mockID, op: 'save', imageID: active_image.id, startX: Math.min(drag_begin.x, drag_end.x), startY: Math.min(drag_begin.y, drag_end.y), endX: Math.max(drag_begin.x, drag_end.x), endY: Math.max(drag_begin.y, drag_end.y), comment: new_content }; inlineComment.addData(commentToAdd); inlineComment.send(); } ); function load_inline_comment(id) { var viewInlineURI = '/pholio/inline/view/' + id + '/'; var inline_comment = new JX.Request(viewInlineURI, function(r) { JX.DOM.replace(JX.$(r.phid + '_comment'), JX.$H(r.contentHTML)); }); inline_comment.send(); } function interrupt_typing() { clear_selection(); try { JX.DOM.remove(JX.$('pholio-new-inline-comment-dialog')); } catch (x) { // TODO: For now, ignore this. } drag_begin = null; } load_inline_comments(); JX.Stratcom.listen('resize', null, redraw_image); redraw_image(); });