Path: blob/master/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js
12241 views
/**1* @provides javelin-behavior-pholio-mock-view2* @requires javelin-behavior3* javelin-util4* javelin-stratcom5* javelin-dom6* javelin-vector7* javelin-magical-init8* javelin-request9* javelin-history10* javelin-workflow11* javelin-mask12* javelin-behavior-device13* phabricator-keyboard-shortcut14*/15JX.behavior('pholio-mock-view', function(config, statics) {16var is_dragging = false;1718var drag_begin;19var drag_end;2021var selection_reticle;22var active_image;2324var inline_comments = {};2526function get_image_index(id) {27for (var ii = 0; ii < statics.images.length; ii++) {28if (statics.images[ii].id == id) {29return ii;30}31}32return null;33}3435function get_image_navindex(id) {36for (var ii = 0; ii < statics.navsequence.length; ii++) {37if (statics.navsequence[ii] == id) {38return ii;39}40}41return null;42}4344function get_image(id) {45var idx = get_image_index(id);46if (idx === null) {47return idx;48}49return statics.images[idx];50}5152function onload_image(id) {53if (active_image.id != id) {54// The user has clicked another image before this one loaded, so just55// bail.56return;57}5859active_image.tag = this;60redraw_image();61}6263function switch_image(delta) {64if (!active_image) {65return;66}67var idx = get_image_navindex(active_image.id);68if (idx === null) {69return;70}71idx = (idx + delta + statics.navsequence.length) %72statics.navsequence.length;73select_image(statics.navsequence[idx]);74}7576function redraw_image() {77if (!statics.enabled) {78return;79}80var new_y;8182// If we don't have an image yet, just scale the stage relative to the83// entire viewport height so the jump isn't too jumpy when the image loads.84if (!active_image || !active_image.tag) {85new_y = (JX.Vector.getViewport().y * 0.80);86new_y = Math.max(320, new_y);87statics.panel.style.height = new_y + 'px';8889return;90}9192var tag = active_image.tag;9394// If the image is too wide for the viewport, scale it down so it fits.95// If it is too tall, just let the viewport scroll.96var w = JX.Vector.getDim(statics.panel);9798// Leave 24px margins on either side of the image.99w.x -= 48;100101var scale = 1;102if (w.x < tag.naturalWidth) {103scale = Math.min(scale, w.x / tag.naturalWidth);104}105106if (scale < 1) {107tag.width = Math.floor(scale * tag.naturalWidth);108tag.height = Math.floor(scale * tag.naturalHeight);109} else {110tag.width = tag.naturalWidth;111tag.height = tag.naturalHeight;112}113114// Scale the viewport's vertical size to the image's adjusted size.115new_y = Math.max(320, tag.height + 48);116statics.panel.style.height = new_y + 'px';117118statics.viewport.style.top = Math.floor((new_y - tag.height) / 2) + 'px';119120statics.stage.endLoad();121122JX.DOM.setContent(statics.viewport, tag);123124redraw_inlines(active_image.id);125}126127function select_image(image_id) {128active_image = get_image(image_id);129active_image.tag = null;130131statics.stage.beginLoad();132133var img = JX.$N('img', {className: 'pholio-mock-image'});134img.onload = JX.bind(img, onload_image, active_image.id);135img.src = active_image.stageURI;136137var thumbs = JX.DOM.scry(138JX.$('pholio-mock-thumb-grid'),139'a',140'mock-thumbnail');141142for(var k in thumbs) {143var thumb_meta = JX.Stratcom.getData(thumbs[k]);144145JX.DOM.alterClass(146thumbs[k],147'pholio-mock-thumb-grid-current',148(active_image.id == thumb_meta.imageID));149}150151load_inline_comments();152if (image_id != statics.selectedID) {153JX.History.replace(active_image.pageURI);154}155}156157function resize_selection(min_size) {158var start = {159x: Math.min(drag_begin.x, drag_end.x),160y: Math.min(drag_begin.y, drag_end.y)161};162var end = {163x: Math.max(drag_begin.x, drag_end.x),164y: Math.max(drag_begin.y, drag_end.y)165};166167var width = end.x - start.x;168var height = end.y - start.y;169var addon;170171if (width < min_size) {172addon = (min_size-width)/2;173174start.x = Math.max(0, start.x - addon);175end.x = Math.min(active_image.tag.naturalWidth, end.x + addon);176177if (start.x === 0) {178end.x = Math.min(min_size, active_image.tag.naturalWidth);179} else if (end.x == active_image.tag.naturalWidth) {180start.x = Math.max(0, active_image.tag.naturalWidth - min_size);181}182}183184if (height < min_size) {185addon = (min_size-height)/2;186187start.y = Math.max(0, start.y - addon);188end.y = Math.min(active_image.tag.naturalHeight, end.y + addon);189190if (start.y === 0) {191end.y = Math.min(min_size, active_image.tag.naturalHeight);192} else if (end.y == active_image.tag.naturalHeight) {193start.y = Math.max(0, active_image.tag.naturalHeight - min_size);194}195}196197drag_begin = start;198drag_end = end;199redraw_selection();200}201202function render_image_header(image) {203// Render image dimensions and visible size. If we have this information204// from the server we can display some of it immediately; otherwise, we need205// to wait for the image to load so we can read dimension information from206// it.207208var image_x = image.width;209var image_y = image.height;210var display_x = null;211if (image.tag) {212image_x = image.tag.naturalWidth;213image_y = image.tag.naturalHeight;214display_x = image.tag.width;215}216217var visible = [];218if (image_x) {219if (display_x) {220var area = Math.round(100 * (display_x / image_x));221visible.push(222JX.$N(223'span',224{className: 'pholio-visible-size'},225[area, '%']));226visible.push(' ');227}228visible.push(['(', image_x, ' \u00d7 ', image_y, ')']);229}230231return visible;232}233234function redraw_inlines(id) {235if (!statics.enabled) {236return;237}238239if (!active_image) {240return;241}242243if (active_image.id != id) {244return;245}246247statics.stage.clearStage();248var comment_holder = JX.$('mock-image-description');249JX.DOM.setContent(comment_holder, render_image_info(active_image));250251var image_header = JX.$('mock-image-header');252JX.DOM.setContent(image_header, render_image_header(active_image));253254var inlines = inline_comments[active_image.id];255if (!inlines || !inlines.length) {256return;257}258259for (var ii = 0; ii < inlines.length; ii++) {260var inline = inlines[ii];261262if (!active_image.tag) {263// The image itself hasn't loaded yet, so we can't draw the inline264// reticles.265continue;266}267268var classes = [];269if (!inline.transactionPHID) {270classes.push('pholio-mock-reticle-draft');271} else {272classes.push('pholio-mock-reticle-final');273}274275var inline_selection = render_reticle(classes,276'pholio-mock-comment-icon phui-font-fa fa-comment');277statics.stage.addReticle(inline_selection, inline.id);278position_inline_rectangle(inline, inline_selection);279}280}281282function position_inline_rectangle(inline, rect) {283var scale = get_image_scale();284285JX.$V(scale * inline.x, scale * inline.y).setPos(rect);286JX.$V(scale * inline.width, scale * inline.height).setDim(rect);287}288289function get_image_xy(p) {290var img = active_image.tag;291var imgp = JX.$V(img);292293var scale = 1 / get_image_scale();294295var x = scale * Math.max(0, Math.min(p.x - imgp.x, img.width));296var y = scale * Math.max(0, Math.min(p.y - imgp.y, img.height));297298return {299x: x,300y: y301};302}303304function get_image_scale() {305var img = active_image.tag;306return Math.min(307img.width / img.naturalWidth,308img.height / img.naturalHeight);309}310311function redraw_selection() {312if (!statics.enabled) {313return;314}315316var classes = ['pholio-mock-reticle-selection'];317selection_reticle = selection_reticle || render_reticle(classes, '');318319var p = JX.$V(320Math.min(drag_begin.x, drag_end.x),321Math.min(drag_begin.y, drag_end.y));322323var d = JX.$V(324Math.max(drag_begin.x, drag_end.x) - p.x,325Math.max(drag_begin.y, drag_end.y) - p.y);326327var scale = get_image_scale();328329p.x *= scale;330p.y *= scale;331d.x *= scale;332d.y *= scale;333334statics.viewport.appendChild(selection_reticle);335p.setPos(selection_reticle);336d.setDim(selection_reticle);337}338339function clear_selection() {340selection_reticle && JX.DOM.remove(selection_reticle);341selection_reticle = null;342}343344function load_inline_comments() {345var id = active_image.id;346var inline_comments_uri = '/pholio/inline/list/' + id + '/';347348new JX.Request(inline_comments_uri, function(r) {349inline_comments[id] = r;350redraw_inlines(id);351}).send();352}353354355/* -( Render )------------------------------------------------------------- */356357358function render_image_info(image) {359var info = [];360361var buttons = [];362363var classes = ['pholio-image-button'];364365if (image.isViewable) {366classes.push('pholio-image-button-active');367} else {368classes.push('pholio-image-button-disabled');369}370371buttons.push(372JX.$N(373'div',374{375className: classes.join(' ')376},377JX.$N(378image.isViewable ? 'a' : 'span',379{380href: image.fullURI,381target: '_blank',382className: 'pholio-image-button-link'383},384JX.$H(statics.fullIcon))));385386classes = ['pholio-image-button', 'pholio-image-button-active'];387388buttons.push(389JX.$N(390'form',391{392className: classes.join(' '),393action: image.downloadURI,394method: 'POST',395sigil: 'download'396},397JX.$N(398'button',399{400href: image.downloadURI,401className: 'pholio-image-button-link'402},403JX.$H(statics.downloadIcon))));404405if (image.title === '') {406image.title = 'Untitled Masterpiece';407}408var title = JX.$N(409'div',410{className: 'pholio-image-title'},411image.title);412info.push(title);413414if (!image.isObsolete) {415var img_len = statics.currentSetSize;416var rev = JX.$N(417'div',418{className: 'pholio-image-revision'},419JX.$H('Current Revision (' + img_len + ' images)'));420info.push(rev);421} else {422var prev = JX.$N(423'div',424{className: 'pholio-image-revision'},425JX.$H('(Previous Revision)'));426info.push(prev);427}428429for (var ii = 0; ii < info.length; ii++) {430info[ii] = JX.$N('div', {className: 'pholio-image-info-item'}, info[ii]);431}432info = JX.$N('div', {className: 'pholio-image-info'}, info);433434if (image.descriptionMarkup === '') {435return [buttons, info];436} else {437var desc = JX.$N(438'div',439{className: 'pholio-image-description'},440JX.$H(image.descriptionMarkup));441return [buttons, info, desc];442}443}444445function render_reticle(classes, inner_classes) {446var inner = JX.$N('div', {className: inner_classes});447var outer = JX.$N(448'div',449{className: ['pholio-mock-reticle'].concat(classes).join(' ')}, inner);450return outer;451}452453454/* -( Device Lightbox )---------------------------------------------------- */455456// On devices, we show images full-size when the user taps them instead of457// attempting to implement inlines.458459var lightbox = null;460461function lightbox_attach() {462JX.DOM.alterClass(document.body, 'lightbox-attached', true);463JX.Mask.show('jx-dark-mask');464465lightbox = lightbox_render();466var image = JX.$N('img');467image.onload = lightbox_loaded;468setTimeout(function() {469image.src = active_image.stageURI;470}, 1000);471JX.DOM.setContent(lightbox, image);472JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', true);473474lightbox_resize();475476document.body.appendChild(lightbox);477}478479function lightbox_detach() {480JX.DOM.remove(lightbox);481JX.Mask.hide();482JX.DOM.alterClass(document.body, 'lightbox-attached', false);483lightbox = null;484}485486function lightbox_resize() {487if (!statics.enabled) {488return;489}490if (!lightbox) {491return;492}493JX.Vector.getScroll().setPos(lightbox);494JX.Vector.getViewport().setDim(lightbox);495}496497function lightbox_loaded() {498JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', false);499}500501function lightbox_render() {502var el = JX.$N('div', {className: 'pholio-device-lightbox'});503JX.Stratcom.addSigil(el, 'pholio-device-lightbox');504return el;505}506507508/* -( Preload )------------------------------------------------------------ */509510511function preload_next() {512var next_src = statics.preload[0];513if (!next_src) {514return;515}516statics.preload.splice(0, 1);517518var img = JX.$N('img');519img.onload = preload_next;520img.onerror = preload_next;521img.src = next_src;522}523524525/* -( Installaton )-------------------------------------------------------- */526527528function update_statics(data) {529statics.enabled = true;530531statics.mockID = data.mockID;532statics.commentFormID = data.commentFormID;533statics.images = data.images;534statics.selectedID = data.selectedID;535statics.loggedIn = data.loggedIn;536statics.logInLink = data.logInLink;537statics.navsequence = data.navsequence;538statics.downloadIcon = data.downloadIcon;539statics.fullIcon = data.fullIcon;540statics.currentSetSize = data.currentSetSize;541542statics.stage = (function() {543var loading = false;544var stageElement = JX.$(data.panelID);545var viewElement = JX.$(data.viewportID);546var reticles = [];547548function begin_load() {549if (loading) {550return;551}552loading = true;553clear_stage();554draw_loading();555}556557function end_load() {558if (!loading) {559return;560}561loading = false;562draw_loading();563}564565function draw_loading() {566JX.DOM.alterClass(stageElement, 'pholio-image-loading', loading);567}568569function add_reticle(reticle, id) {570mark_ref(reticle, id);571reticles.push(reticle);572viewElement.appendChild(reticle);573}574575function clear_stage() {576var ii;577for (ii = 0; ii < reticles.length; ii++) {578JX.DOM.remove(reticles[ii]);579}580reticles = [];581}582583function mark_ref(node, id) {584JX.Stratcom.addSigil(node, 'pholio-inline-ref');585JX.Stratcom.addData(node, {inlineID: id});586}587588return {589beginLoad: begin_load,590endLoad: end_load,591addReticle: add_reticle,592clearStage: clear_stage593};594})();595596statics.panel = JX.$(data.panelID);597statics.viewport = JX.$(data.viewportID);598599select_image(data.selectedID);600601load_inline_comments();602if (data.loggedIn && data.commentFormID) {603JX.DOM.invoke(JX.$(data.commentFormID), 'shouldRefresh');604}605redraw_image();606607statics.preload = [];608for (var ii = 0; ii < data.images.length; ii++) {609statics.preload.push(data.images[ii].stageURI);610}611612preload_next();613614}615616function install_extra_listeners() {617JX.DOM.listen(statics.panel, 'gesture.swipe.end', null, function(e) {618var data = e.getData();619620if (data.length <= (JX.Vector.getDim(statics.panel) / 2)) {621// If the user didn't move their finger far enough, don't switch.622return;623}624switch_image(data.direction == 'right' ? -1 : 1);625});626}627628function install_mock_view() {629JX.enableDispatch(document.body, 'mouseenter');630JX.enableDispatch(document.body, 'mouseleave');631632JX.Stratcom.listen(633['mouseenter', 'mouseover'],634'mock-panel',635function(e) {636JX.DOM.alterClass(e.getNode('mock-panel'), 'mock-has-cursor', true);637});638639JX.Stratcom.listen('mouseleave', 'mock-panel', function(e) {640var node = e.getNode('mock-panel');641if (e.getTarget() == node) {642JX.DOM.alterClass(node, 'mock-has-cursor', false);643}644});645646JX.Stratcom.listen(647'click',648'mock-thumbnail',649function(e) {650if (!e.isNormalMouseEvent()) {651return;652}653e.kill();654select_image(e.getNodeData('mock-thumbnail').imageID);655});656657JX.Stratcom.listen('mousedown', 'mock-viewport', function(e) {658if (!e.isNormalMouseEvent()) {659return;660}661662if (JX.Device.getDevice() != 'desktop') {663return;664}665666if (JX.Stratcom.pass()) {667return;668}669670if (is_dragging) {671return;672}673674e.kill();675676if (!active_image.isImage) {677// If this is a PDF or something like that, we eat the event but we678// don't let users add inlines to the thumbnail.679return;680}681682is_dragging = true;683drag_begin = get_image_xy(JX.$V(e));684drag_end = drag_begin;685686redraw_selection();687});688689JX.enableDispatch(document.body, 'mousemove');690JX.Stratcom.listen('mousemove', null, function(e) {691if (!statics.enabled) {692return;693}694if (!is_dragging) {695return;696}697drag_end = get_image_xy(JX.$V(e));698redraw_selection();699});700701JX.Stratcom.listen(702'mousedown',703'pholio-inline-ref',704function(e) {705e.kill();706707var id = e.getNodeData('pholio-inline-ref').inlineID;708709var active_id = active_image.id;710var handler = function(r) {711var inlines = inline_comments[active_id];712713for (var ii = 0; ii < inlines.length; ii++) {714if (inlines[ii].id == id) {715if (r.id) {716inlines[ii] = r;717} else {718inlines.splice(ii, 1);719}720break;721}722}723724redraw_inlines(active_id);725JX.DOM.invoke(JX.$(statics.commentFormID), 'shouldRefresh');726};727728new JX.Workflow('/pholio/inline/' + id + '/')729.setHandler(handler)730.start();731});732733JX.Stratcom.listen(734'mouseup',735null,736function(e) {737if (!statics.enabled) {738return;739}740if (!is_dragging) {741return;742}743744is_dragging = false;745if (!statics.loggedIn) {746new JX.Workflow(statics.logInLink).start();747return;748}749750drag_end = get_image_xy(JX.$V(e));751752resize_selection(16);753754var data = {755mockID: statics.mockID,756imageID: active_image.id,757startX: Math.min(drag_begin.x, drag_end.x),758startY: Math.min(drag_begin.y, drag_end.y),759endX: Math.max(drag_begin.x, drag_end.x),760endY: Math.max(drag_begin.y, drag_end.y)761};762763var handler = function(r) {764if (!inline_comments[active_image.id]) {765inline_comments[active_image.id] = [];766}767inline_comments[active_image.id].push(r);768769redraw_inlines(active_image.id);770JX.DOM.invoke(JX.$(statics.commentFormID), 'shouldRefresh');771};772773clear_selection();774775new JX.Workflow('/pholio/inline/', data)776.setHandler(handler)777.start();778});779780JX.Stratcom.listen('resize', null, redraw_image);781782783/* Keyboard Shortcuts */784new JX.KeyboardShortcut(['j', 'right'], 'Show next image.')785.setHandler(function() {786switch_image(1);787})788.register();789790new JX.KeyboardShortcut(['k', 'left'], 'Show previous image.')791.setHandler(function() {792switch_image(-1);793})794.register();795796797/* Lightbox listeners */798JX.Stratcom.listen('click', 'mock-viewport', function(e) {799if (!e.isNormalMouseEvent()) {800return;801}802if (JX.Device.getDevice() == 'desktop') {803return;804}805lightbox_attach();806e.kill();807});808JX.Stratcom.listen('click', 'pholio-device-lightbox', lightbox_detach);809JX.Stratcom.listen('resize', null, lightbox_resize);810811JX.Stratcom.listen(812'quicksand-redraw',813null,814function (e) {815var data = e.getData();816var new_config;817if (!data.newResponse.mockViewConfig) {818statics.enabled = false;819return;820}821if (data.fromServer) {822new_config = data.newResponse.mockViewConfig;823} else {824new_config = statics.mockViewConfigCache[data.newResponseID];825}826update_statics(new_config);827if (data.fromServer) {828install_extra_listeners();829}830});831}832833if (!statics.installed) {834var current_page_id = JX.Quicksand.getCurrentPageID();835statics.mockViewConfigCache = {};836statics.mockViewConfigCache[current_page_id] = config;837update_statics(config);838839statics.installed = install_mock_view();840install_extra_listeners();841}842843});844845846