[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 /** 2 * @provides javelin-behavior-pholio-mock-view 3 * @requires javelin-behavior 4 * javelin-util 5 * javelin-stratcom 6 * javelin-dom 7 * javelin-vector 8 * javelin-magical-init 9 * javelin-request 10 * javelin-history 11 * javelin-workflow 12 * javelin-mask 13 * javelin-behavior-device 14 * phabricator-keyboard-shortcut 15 */ 16 JX.behavior('pholio-mock-view', function(config) { 17 var is_dragging = false; 18 19 var drag_begin; 20 var drag_end; 21 var panel = JX.$(config.panelID); 22 var viewport = JX.$(config.viewportID); 23 24 var selection_reticle; 25 var active_image; 26 27 var inline_comments = {}; 28 29 30 /* -( Stage )-------------------------------------------------------------- */ 31 32 33 var stage = (function() { 34 var loading = false; 35 var stageElement = JX.$(config.panelID); 36 var viewElement = JX.$(config.viewportID); 37 var reticles = []; 38 39 function begin_load() { 40 if (loading) { 41 return; 42 } 43 loading = true; 44 clear_stage(); 45 draw_loading(); 46 } 47 48 function end_load() { 49 if (!loading) { 50 return; 51 } 52 loading = false; 53 draw_loading(); 54 } 55 56 function draw_loading() { 57 JX.DOM.alterClass(stageElement, 'pholio-image-loading', loading); 58 } 59 60 function add_reticle(reticle, id) { 61 mark_ref(reticle, id); 62 reticles.push(reticle); 63 viewElement.appendChild(reticle); 64 } 65 66 function clear_stage() { 67 var ii; 68 for (ii = 0; ii < reticles.length; ii++) { 69 JX.DOM.remove(reticles[ii]); 70 } 71 reticles = []; 72 } 73 74 function mark_ref(node, id) { 75 JX.Stratcom.addSigil(node, 'pholio-inline-ref'); 76 JX.Stratcom.addData(node, {inlineID: id}); 77 } 78 79 return { 80 beginLoad: begin_load, 81 endLoad: end_load, 82 addReticle: add_reticle, 83 clearStage: clear_stage 84 }; 85 })(); 86 87 JX.enableDispatch(document.body, 'mouseenter'); 88 JX.enableDispatch(document.body, 'mouseleave'); 89 90 JX.Stratcom.listen( 91 ['mouseenter', 'mouseover'], 92 'mock-panel', 93 function(e) { 94 JX.DOM.alterClass(e.getNode('mock-panel'), 'mock-has-cursor', true); 95 }); 96 97 JX.Stratcom.listen('mouseleave', 'mock-panel', function(e) { 98 var node = e.getNode('mock-panel'); 99 if (e.getTarget() == node) { 100 JX.DOM.alterClass(node, 'mock-has-cursor', false); 101 } 102 }); 103 104 function get_image_index(id) { 105 for (var ii = 0; ii < config.images.length; ii++) { 106 if (config.images[ii].id == id) { 107 return ii; 108 } 109 } 110 return null; 111 } 112 113 function get_image_navindex(id) { 114 for (var ii = 0; ii < config.navsequence.length; ii++) { 115 if (config.navsequence[ii] == id) { 116 return ii; 117 } 118 } 119 return null; 120 } 121 122 function get_image(id) { 123 var idx = get_image_index(id); 124 if (idx === null) { 125 return idx; 126 } 127 return config.images[idx]; 128 } 129 130 function onload_image(id) { 131 if (active_image.id != id) { 132 // The user has clicked another image before this one loaded, so just 133 // bail. 134 return; 135 } 136 137 active_image.tag = this; 138 redraw_image(); 139 } 140 141 function switch_image(delta) { 142 if (!active_image) { 143 return; 144 } 145 var idx = get_image_navindex(active_image.id); 146 if (idx === null) { 147 return; 148 } 149 idx = (idx + delta + config.navsequence.length) % config.navsequence.length; 150 select_image(config.navsequence[idx]); 151 } 152 153 function redraw_image() { 154 var new_y; 155 156 // If we don't have an image yet, just scale the stage relative to the 157 // entire viewport height so the jump isn't too jumpy when the image loads. 158 if (!active_image || !active_image.tag) { 159 new_y = (JX.Vector.getViewport().y * 0.80); 160 new_y = Math.max(320, new_y); 161 panel.style.height = new_y + 'px'; 162 163 return; 164 } 165 166 var tag = active_image.tag; 167 168 // If the image is too wide for the viewport, scale it down so it fits. 169 // If it is too tall, just let the viewport scroll. 170 var w = JX.Vector.getDim(panel); 171 172 // Leave 24px margins on either side of the image. 173 w.x -= 48; 174 175 var scale = 1; 176 if (w.x < tag.naturalWidth) { 177 scale = Math.min(scale, w.x / tag.naturalWidth); 178 } 179 180 if (scale < 1) { 181 tag.width = Math.floor(scale * tag.naturalWidth); 182 tag.height = Math.floor(scale * tag.naturalHeight); 183 } else { 184 tag.width = tag.naturalWidth; 185 tag.height = tag.naturalHeight; 186 } 187 188 // Scale the viewport's vertical size to the image's adjusted size. 189 new_y = Math.max(320, tag.height + 48); 190 panel.style.height = new_y + 'px'; 191 192 viewport.style.top = Math.floor((new_y - tag.height) / 2) + 'px'; 193 194 stage.endLoad(); 195 196 JX.DOM.setContent(viewport, tag); 197 198 redraw_inlines(active_image.id); 199 } 200 201 function select_image(image_id) { 202 active_image = get_image(image_id); 203 active_image.tag = null; 204 205 stage.beginLoad(); 206 207 var img = JX.$N('img', {className: 'pholio-mock-image'}); 208 img.onload = JX.bind(img, onload_image, active_image.id); 209 img.src = active_image.stageURI; 210 211 var thumbs = JX.DOM.scry( 212 JX.$('pholio-mock-thumb-grid'), 213 'a', 214 'mock-thumbnail'); 215 216 for(var k in thumbs) { 217 var thumb_meta = JX.Stratcom.getData(thumbs[k]); 218 219 JX.DOM.alterClass( 220 thumbs[k], 221 'pholio-mock-thumb-grid-current', 222 (active_image.id == thumb_meta.imageID)); 223 } 224 225 load_inline_comments(); 226 if (image_id != config.selectedID) { 227 JX.History.replace(active_image.pageURI); 228 } 229 } 230 231 JX.Stratcom.listen( 232 'click', 233 'mock-thumbnail', 234 function(e) { 235 if (!e.isNormalMouseEvent()) { 236 return; 237 } 238 e.kill(); 239 select_image(e.getNodeData('mock-thumbnail').imageID); 240 }); 241 242 select_image(config.selectedID); 243 244 JX.Stratcom.listen('mousedown', 'mock-viewport', function(e) { 245 if (!e.isNormalMouseEvent()) { 246 return; 247 } 248 249 if (JX.Device.getDevice() != 'desktop') { 250 return; 251 } 252 253 if (JX.Stratcom.pass()) { 254 return; 255 } 256 257 if (is_dragging) { 258 return; 259 } 260 261 e.kill(); 262 263 if (!active_image.isImage) { 264 // If this is a PDF or something like that, we eat the event but we 265 // don't let users add inlines to the thumbnail. 266 return; 267 } 268 269 is_dragging = true; 270 drag_begin = get_image_xy(JX.$V(e)); 271 drag_end = drag_begin; 272 273 redraw_selection(); 274 }); 275 276 277 JX.enableDispatch(document.body, 'mousemove'); 278 JX.Stratcom.listen('mousemove', null, function(e) { 279 if (!is_dragging) { 280 return; 281 } 282 drag_end = get_image_xy(JX.$V(e)); 283 redraw_selection(); 284 }); 285 286 JX.Stratcom.listen( 287 'mousedown', 288 'pholio-inline-ref', 289 function(e) { 290 e.kill(); 291 292 var id = e.getNodeData('pholio-inline-ref').inlineID; 293 294 var active_id = active_image.id; 295 var handler = function(r) { 296 var inlines = inline_comments[active_id]; 297 298 for (var ii = 0; ii < inlines.length; ii++) { 299 if (inlines[ii].id == id) { 300 if (r.id) { 301 inlines[ii] = r; 302 } else { 303 inlines.splice(ii, 1); 304 } 305 break; 306 } 307 } 308 309 redraw_inlines(active_id); 310 JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh'); 311 }; 312 313 new JX.Workflow('/pholio/inline/' + id + '/') 314 .setHandler(handler) 315 .start(); 316 }); 317 318 JX.Stratcom.listen( 319 'mouseup', 320 null, 321 function(e) { 322 if (!is_dragging) { 323 return; 324 } 325 326 is_dragging = false; 327 if (!config.loggedIn) { 328 new JX.Workflow(config.logInLink).start(); 329 return; 330 } 331 332 drag_end = get_image_xy(JX.$V(e)); 333 334 resize_selection(16); 335 336 var data = { 337 mockID: config.mockID, 338 imageID: active_image.id, 339 startX: Math.min(drag_begin.x, drag_end.x), 340 startY: Math.min(drag_begin.y, drag_end.y), 341 endX: Math.max(drag_begin.x, drag_end.x), 342 endY: Math.max(drag_begin.y, drag_end.y) 343 }; 344 345 var handler = function(r) { 346 if (!inline_comments[active_image.id]) { 347 inline_comments[active_image.id] = []; 348 } 349 inline_comments[active_image.id].push(r); 350 351 redraw_inlines(active_image.id); 352 JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh'); 353 }; 354 355 clear_selection(); 356 357 new JX.Workflow('/pholio/inline/', data) 358 .setHandler(handler) 359 .start(); 360 }); 361 362 function resize_selection(min_size) { 363 var start = { 364 x: Math.min(drag_begin.x, drag_end.x), 365 y: Math.min(drag_begin.y, drag_end.y) 366 }; 367 var end = { 368 x: Math.max(drag_begin.x, drag_end.x), 369 y: Math.max(drag_begin.y, drag_end.y) 370 }; 371 372 var width = end.x - start.x; 373 var height = end.y - start.y; 374 var addon; 375 376 if (width < min_size) { 377 addon = (min_size-width)/2; 378 379 start.x = Math.max(0, start.x - addon); 380 end.x = Math.min(active_image.tag.naturalWidth, end.x + addon); 381 382 if (start.x === 0) { 383 end.x = Math.min(min_size, active_image.tag.naturalWidth); 384 } else if (end.x == active_image.tag.naturalWidth) { 385 start.x = Math.max(0, active_image.tag.naturalWidth - min_size); 386 } 387 } 388 389 if (height < min_size) { 390 addon = (min_size-height)/2; 391 392 start.y = Math.max(0, start.y - addon); 393 end.y = Math.min(active_image.tag.naturalHeight, end.y + addon); 394 395 if (start.y === 0) { 396 end.y = Math.min(min_size, active_image.tag.naturalHeight); 397 } else if (end.y == active_image.tag.naturalHeight) { 398 start.y = Math.max(0, active_image.tag.naturalHeight - min_size); 399 } 400 } 401 402 drag_begin = start; 403 drag_end = end; 404 redraw_selection(); 405 } 406 407 function render_image_header(image) { 408 // Render image dimensions and visible size. If we have this infomation 409 // from the server we can display some of it immediately; otherwise, we need 410 // to wait for the image to load so we can read dimension information from 411 // it. 412 413 var image_x = image.width; 414 var image_y = image.height; 415 var display_x = null; 416 if (image.tag) { 417 image_x = image.tag.naturalWidth; 418 image_y = image.tag.naturalHeight; 419 display_x = image.tag.width; 420 } 421 422 var visible = []; 423 if (image_x) { 424 if (display_x) { 425 var area = Math.round(100 * (display_x / image_x)); 426 visible.push( 427 JX.$N( 428 'span', 429 {className: 'pholio-visible-size'}, 430 [area, '%'])); 431 visible.push(' '); 432 } 433 visible.push(['(', image_x, ' \u00d7 ', image_y, ')']); 434 } 435 436 return visible; 437 } 438 439 function redraw_inlines(id) { 440 if (!active_image) { 441 return; 442 } 443 444 if (active_image.id != id) { 445 return; 446 } 447 448 stage.clearStage(); 449 var comment_holder = JX.$('mock-image-description'); 450 JX.DOM.setContent(comment_holder, render_image_info(active_image)); 451 452 var image_header = JX.$('mock-image-header'); 453 JX.DOM.setContent(image_header, render_image_header(active_image)); 454 455 var inlines = inline_comments[active_image.id]; 456 if (!inlines || !inlines.length) { 457 return; 458 } 459 460 for (var ii = 0; ii < inlines.length; ii++) { 461 var inline = inlines[ii]; 462 463 if (!active_image.tag) { 464 // The image itself hasn't loaded yet, so we can't draw the inline 465 // reticles. 466 continue; 467 } 468 469 var classes = []; 470 if (!inline.transactionPHID) { 471 classes.push('pholio-mock-reticle-draft'); 472 } else { 473 classes.push('pholio-mock-reticle-final'); 474 } 475 476 var inline_selection = render_reticle(classes, 477 'pholio-mock-comment-icon phui-font-fa fa-comment'); 478 stage.addReticle(inline_selection, inline.id); 479 position_inline_rectangle(inline, inline_selection); 480 } 481 } 482 483 function position_inline_rectangle(inline, rect) { 484 var scale = get_image_scale(); 485 486 JX.$V(scale * inline.x, scale * inline.y).setPos(rect); 487 JX.$V(scale * inline.width, scale * inline.height).setDim(rect); 488 } 489 490 function get_image_xy(p) { 491 var img = active_image.tag; 492 var imgp = JX.$V(img); 493 494 var scale = 1 / get_image_scale(); 495 496 var x = scale * Math.max(0, Math.min(p.x - imgp.x, img.width)); 497 var y = scale * Math.max(0, Math.min(p.y - imgp.y, img.height)); 498 499 return { 500 x: x, 501 y: y 502 }; 503 } 504 505 function get_image_scale() { 506 var img = active_image.tag; 507 return Math.min( 508 img.width / img.naturalWidth, 509 img.height / img.naturalHeight); 510 } 511 512 function redraw_selection() { 513 var classes = ['pholio-mock-reticle-selection']; 514 selection_reticle = selection_reticle || render_reticle(classes, ''); 515 516 var p = JX.$V( 517 Math.min(drag_begin.x, drag_end.x), 518 Math.min(drag_begin.y, drag_end.y)); 519 520 var d = JX.$V( 521 Math.max(drag_begin.x, drag_end.x) - p.x, 522 Math.max(drag_begin.y, drag_end.y) - p.y); 523 524 var scale = get_image_scale(); 525 526 p.x *= scale; 527 p.y *= scale; 528 d.x *= scale; 529 d.y *= scale; 530 531 viewport.appendChild(selection_reticle); 532 p.setPos(selection_reticle); 533 d.setDim(selection_reticle); 534 } 535 536 function clear_selection() { 537 selection_reticle && JX.DOM.remove(selection_reticle); 538 selection_reticle = null; 539 } 540 541 function load_inline_comments() { 542 var id = active_image.id; 543 var inline_comments_uri = '/pholio/inline/list/' + id + '/'; 544 545 new JX.Request(inline_comments_uri, function(r) { 546 inline_comments[id] = r; 547 redraw_inlines(id); 548 }).send(); 549 } 550 551 load_inline_comments(); 552 if (config.loggedIn && config.commentFormID) { 553 JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh'); 554 } 555 556 JX.Stratcom.listen('resize', null, redraw_image); 557 redraw_image(); 558 559 560 /* -( Keyboard Shortcuts )------------------------------------------------- */ 561 562 563 new JX.KeyboardShortcut(['j', 'right'], 'Show next image.') 564 .setHandler(function() { 565 switch_image(1); 566 }) 567 .register(); 568 569 new JX.KeyboardShortcut(['k', 'left'], 'Show previous image.') 570 .setHandler(function() { 571 switch_image(-1); 572 }) 573 .register(); 574 575 JX.DOM.listen(panel, 'gesture.swipe.end', null, function(e) { 576 var data = e.getData(); 577 578 if (data.length <= (JX.Vector.getDim(panel) / 2)) { 579 // If the user didn't move their finger far enough, don't switch. 580 return; 581 } 582 583 switch_image(data.direction == 'right' ? -1 : 1); 584 }); 585 586 /* -( Render )------------------------------------------------------------- */ 587 588 589 function render_image_info(image) { 590 var info = []; 591 592 var buttons = []; 593 594 var classes = ['pholio-image-button']; 595 596 if (image.isViewable) { 597 classes.push('pholio-image-button-active'); 598 } else { 599 classes.push('pholio-image-button-disabled'); 600 } 601 602 buttons.push( 603 JX.$N( 604 'div', 605 { 606 className: classes.join(' ') 607 }, 608 JX.$N( 609 image.isViewable ? 'a' : 'span', 610 { 611 href: image.fullURI, 612 target: '_blank', 613 className: 'pholio-image-button-link' 614 }, 615 JX.$H(config.fullIcon)))); 616 617 classes = ['pholio-image-button', 'pholio-image-button-active']; 618 619 buttons.push( 620 JX.$N( 621 'form', 622 { 623 className: classes.join(' '), 624 action: image.downloadURI, 625 method: 'POST', 626 sigil: 'download' 627 }, 628 JX.$N( 629 'button', 630 { 631 href: image.downloadURI, 632 className: 'pholio-image-button-link' 633 }, 634 JX.$H(config.downloadIcon)))); 635 636 if (image.title === '') { 637 image.title = 'Untitled Masterpiece'; 638 } 639 var title = JX.$N( 640 'div', 641 {className: 'pholio-image-title'}, 642 image.title); 643 info.push(title); 644 645 if (!image.isObsolete) { 646 var img_len = config.currentSetSize; 647 var rev = JX.$N( 648 'div', 649 {className: 'pholio-image-revision'}, 650 JX.$H('Current Revision (' + img_len + ' images)')); 651 info.push(rev); 652 } else { 653 var prev = JX.$N( 654 'div', 655 {className: 'pholio-image-revision'}, 656 JX.$H('(Previous Revision)')); 657 info.push(prev); 658 } 659 660 for (var ii = 0; ii < info.length; ii++) { 661 info[ii] = JX.$N('div', {className: 'pholio-image-info-item'}, info[ii]); 662 } 663 info = JX.$N('div', {className: 'pholio-image-info'}, info); 664 665 if (image.descriptionMarkup === '') { 666 return [buttons, info]; 667 } else { 668 var desc = JX.$N( 669 'div', 670 {className: 'pholio-image-description'}, 671 JX.$H(image.descriptionMarkup)); 672 return [buttons, info, desc]; 673 } 674 } 675 676 function render_reticle(classes, inner_classes) { 677 var inner = JX.$N('div', {className: inner_classes}); 678 var outer = JX.$N( 679 'div', 680 {className: ['pholio-mock-reticle'].concat(classes).join(' ')}, inner); 681 return outer; 682 } 683 684 685 /* -( Device Lightbox )---------------------------------------------------- */ 686 687 // On devices, we show images full-size when the user taps them instead of 688 // attempting to implement inlines. 689 690 var lightbox = null; 691 692 JX.Stratcom.listen('click', 'mock-viewport', function(e) { 693 if (!e.isNormalMouseEvent()) { 694 return; 695 } 696 if (JX.Device.getDevice() == 'desktop') { 697 return; 698 } 699 lightbox_attach(); 700 e.kill(); 701 }); 702 703 JX.Stratcom.listen('click', 'pholio-device-lightbox', lightbox_detach); 704 JX.Stratcom.listen('resize', null, lightbox_resize); 705 706 function lightbox_attach() { 707 JX.DOM.alterClass(document.body, 'lightbox-attached', true); 708 JX.Mask.show('jx-dark-mask'); 709 710 lightbox = lightbox_render(); 711 var image = JX.$N('img'); 712 image.onload = lightbox_loaded; 713 setTimeout(function() { 714 image.src = active_image.stageURI; 715 }, 1000); 716 JX.DOM.setContent(lightbox, image); 717 JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', true); 718 719 lightbox_resize(); 720 721 document.body.appendChild(lightbox); 722 } 723 724 function lightbox_detach() { 725 JX.DOM.remove(lightbox); 726 JX.Mask.hide(); 727 JX.DOM.alterClass(document.body, 'lightbox-attached', false); 728 lightbox = null; 729 } 730 731 function lightbox_resize() { 732 if (!lightbox) { 733 return; 734 } 735 JX.Vector.getScroll().setPos(lightbox); 736 JX.Vector.getViewport().setDim(lightbox); 737 } 738 739 function lightbox_loaded() { 740 JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', false); 741 } 742 743 function lightbox_render() { 744 var el = JX.$N('div', {className: 'pholio-device-lightbox'}); 745 JX.Stratcom.addSigil(el, 'pholio-device-lightbox'); 746 return el; 747 } 748 749 750 /* -( Preload )------------------------------------------------------------ */ 751 752 var preload = []; 753 for (var ii = 0; ii < config.images.length; ii++) { 754 preload.push(config.images[ii].stageURI); 755 } 756 757 function preload_next() { 758 next_src = preload[0]; 759 if (!next_src) { 760 return; 761 } 762 preload.splice(0, 1); 763 764 var img = JX.$N('img'); 765 img.onload = preload_next; 766 img.onerror = preload_next; 767 img.src = next_src; 768 } 769 770 preload_next(); 771 772 773 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |