1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-23 14:00:56 +01:00
phorge-phorge/webroot/rsrc/js/application/pholio/behavior-pholio-mock-view.js

856 lines
20 KiB
JavaScript
Raw Normal View History

/**
* @provides javelin-behavior-pholio-mock-view
* @requires javelin-behavior
* javelin-util
* javelin-stratcom
* javelin-dom
* javelin-vector
* javelin-magical-init
* javelin-request
* javelin-history
* javelin-workflow
* javelin-mask
* javelin-behavior-device
* phabricator-keyboard-shortcut
*/
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 = {};
/* -( Stage )-------------------------------------------------------------- */
var stage = (function() {
var loading = false;
var stageElement = JX.$(config.panelID);
var viewElement = JX.$(config.viewportID);
var gutterElement = JX.$('mock-inline-comments');
var reticles = [];
var cards = [];
var inline_phid_map = {};
function begin_load() {
if (loading) {
return;
}
loading = true;
clear_stage();
draw_loading();
}
function end_load() {
if (!loading) {
return;
}
loading = false;
draw_loading();
}
function draw_loading() {
JX.DOM.alterClass(stageElement, 'pholio-image-loading', loading);
}
function add_inline_node(node, phid) {
inline_phid_map[phid] = (inline_phid_map[phid] || []);
inline_phid_map[phid].push(node);
}
function add_reticle(reticle, phid) {
mark_ref(reticle, phid);
reticles.push(reticle);
add_inline_node(reticle, phid);
viewElement.appendChild(reticle);
}
function clear_stage() {
var ii;
for (ii = 0; ii < reticles.length; ii++) {
JX.DOM.remove(reticles[ii]);
}
for (ii = 0; ii < cards.length; ii++) {
JX.DOM.remove(cards[ii]);
}
reticles = [];
cards = [];
inline_phid_map = {};
}
function highlight_inline(phid, show) {
var nodes = inline_phid_map[phid] || [];
var cls = 'pholio-mock-inline-comment-highlight';
for (var ii = 0; ii < nodes.length; ii++) {
JX.DOM.alterClass(nodes[ii], cls, show);
}
}
function remove_inline(phid) {
var nodes = inline_phid_map[phid] || [];
for (var ii = 0; ii < nodes.length; ii++) {
JX.DOM.remove(nodes[ii]);
}
delete inline_phid_map[phid];
}
function mark_ref(node, phid) {
JX.Stratcom.addSigil(node, 'pholio-inline-ref');
JX.Stratcom.addData(node, {phid: phid});
}
function add_card(card, phid) {
mark_ref(card, phid);
cards.push(card);
add_inline_node(card, phid);
gutterElement.appendChild(card);
}
return {
beginLoad: begin_load,
endLoad: end_load,
addReticle: add_reticle,
clearStage: clear_stage,
highlightInline: highlight_inline,
removeInline: remove_inline,
addCard: add_card
};
})();
function get_image_index(id) {
for (var ii = 0; ii < config.images.length; ii++) {
if (config.images[ii].id == id) {
return ii;
}
}
return null;
}
function get_image(id) {
var idx = get_image_index(id);
if (idx === null) {
return idx;
}
return config.images[idx];
}
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 switch_image(delta) {
if (!active_image) {
return;
}
var idx = get_image_index(active_image.id);
idx = (idx + delta + config.images.length) % config.images.length;
select_image(config.images[idx].id);
}
function redraw_image() {
// Force the stage to scale as a function of the viewport size. Broadly,
// we make the stage 95% of the height of the viewport, then scale images
// to fit within it.
var new_y = (JX.Vector.getViewport().y * 0.90) - 150;
new_y = Math.max(320, new_y);
panel.style.height = new_y + 'px';
if (!active_image || !active_image.tag) {
return;
}
var tag = active_image.tag;
// If the image is too wide or tall for the viewport, scale it down so it
// fits.
var w = JX.Vector.getDim(panel);
w.x -= 40;
w.y -= 40;
var scale = 1;
if (w.x < tag.naturalWidth) {
scale = Math.min(scale, w.x / tag.naturalWidth);
}
if (w.y < tag.naturalHeight) {
scale = Math.min(scale, w.y / tag.naturalHeight);
}
if (scale < 1) {
tag.width = Math.floor(scale * tag.naturalWidth);
tag.height = Math.floor(scale * tag.naturalHeight);
} else {
tag.width = tag.naturalWidth;
tag.height = tag.naturalHeight;
}
viewport.style.top = Math.floor((new_y - tag.height) / 2) + 'px';
stage.endLoad();
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;
stage.beginLoad();
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'),
'a',
'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();
if (image_id != config.selectedID) {
JX.History.replace(active_image.pageURI);
}
}
JX.Stratcom.listen(
['mousedown', 'click'],
'mock-thumbnail',
function(e) {
if (!e.isNormalMouseEvent()) {
return;
}
e.kill();
select_image(e.getNodeData('mock-thumbnail').imageID);
});
select_image(config.selectedID);
JX.Stratcom.listen('mousedown', 'mock-viewport', function(e) {
if (!e.isNormalMouseEvent()) {
return;
}
if (JX.Device.getDevice() != 'desktop') {
return;
}
if (config.viewMode == 'history') {
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'],
'pholio-inline-ref',
function(e) {
var phid = e.getNodeData('pholio-inline-ref').phid;
var show = (e.getType() == 'mouseover');
stage.highlightInline(phid, show);
});
JX.Stratcom.listen(
'mouseup',
null,
function(e) {
if (!is_dragging) {
return;
}
is_dragging = false;
if (!config.loggedIn) {
new JX.Workflow(config.logInLink).start();
return;
}
drag_end = get_image_xy(JX.$V(e));
var scale = get_image_scale();
resize_selection(16);
var data = {mockID: config.mockID};
var handler = function(r) {
var dialog = JX.$H(r).getFragment().firstChild;
JX.DOM.appendContent(viewport, dialog);
var x = Math.min(drag_begin.x * scale, drag_end.x * scale);
var y = Math.max(drag_begin.y * scale, drag_end.y * scale) + 4;
JX.$V(x, y).setPos(dialog);
JX.DOM.focus(JX.DOM.find(dialog, 'textarea'));
};
new JX.Workflow('/pholio/inline/save/', data)
.setHandler(handler)
.start();
});
function resize_selection(min_size) {
var start = {
x: Math.min(drag_begin.x, drag_end.x),
y: Math.min(drag_begin.y, drag_end.y)
};
var end = {
x: Math.max(drag_begin.x, drag_end.x),
y: Math.max(drag_begin.y, drag_end.y)
};
var width = end.x - start.x;
var height = end.y - start.y;
var addon;
if (width < min_size) {
addon = (min_size-width)/2;
start.x = Math.max(0, start.x - addon);
end.x = Math.min(active_image.tag.naturalWidth, end.x + addon);
if (start.x === 0) {
end.x = Math.min(min_size, active_image.tag.naturalWidth);
} else if (end.x == active_image.tag.naturalWidth) {
start.x = Math.max(0, active_image.tag.naturalWidth - min_size);
}
}
if (height < min_size) {
addon = (min_size-height)/2;
start.y = Math.max(0, start.y - addon);
end.y = Math.min(active_image.tag.naturalHeight, end.y + addon);
if (start.y === 0) {
end.y = Math.min(min_size, active_image.tag.naturalHeight);
} else if (end.y == active_image.tag.naturalHeight) {
start.y = Math.max(0, active_image.tag.naturalHeight - min_size);
}
}
drag_begin = start;
drag_end = end;
redraw_selection();
}
function redraw_inlines(id) {
if (!active_image) {
return;
}
if (active_image.id != id) {
return;
}
stage.clearStage();
var comment_holder = JX.$('mock-inline-comments');
JX.DOM.setContent(comment_holder, render_image_info(active_image));
var inlines = inline_comments[active_image.id];
if (!inlines || !inlines.length) {
return;
}
for (var ii = 0; ii < inlines.length; ii++) {
var inline = inlines[ii];
var card = JX.$H(inline.contentHTML).getFragment().firstChild;
stage.addCard(card, inline.phid);
if (!active_image.tag) {
// The image itself hasn't loaded yet, so we can't draw the inline
// reticles.
continue;
}
var inline_selection = render_reticle_fill();
stage.addReticle(inline_selection, inline.phid);
position_inline_rectangle(inline, inline_selection);
if (!inline.transactionphid) {
var inline_draft = render_reticle_border();
stage.addReticle(inline_draft, inline.phid);
position_inline_rectangle(inline, inline_draft);
}
}
}
function position_inline_rectangle(inline, rect) {
var scale = get_image_scale();
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 Math.min(
img.width / img.naturalWidth,
img.height / img.naturalHeight);
}
function redraw_selection() {
selection_border = selection_border || render_reticle_border();
selection_fill = selection_fill || render_reticle_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_fill, selection_border];
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 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();
stage.removeInline(data.phid);
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);
JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh');
});
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 form = JX.$('pholio-new-inline-comment-dialog');
var text = JX.DOM.find(form, 'textarea').value;
if (!text.length) {
interrupt_typing();
return;
}
var data = {
mockID: config.mockID,
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)
};
var handler = 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);
JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh');
};
JX.Workflow.newFromForm(form, data)
.setHandler(handler)
.start();
}
);
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();
if (config.loggedIn && config.commentFormID) {
JX.DOM.invoke(JX.$(config.commentFormID), 'shouldRefresh');
}
JX.Stratcom.listen('resize', null, redraw_image);
redraw_image();
/* -( Keyboard Shortcuts )------------------------------------------------- */
new JX.KeyboardShortcut(['j', 'right'], 'Show next image.')
.setHandler(function() {
switch_image(1);
})
.register();
new JX.KeyboardShortcut(['k', 'left'], 'Show previous image.')
.setHandler(function() {
switch_image(-1);
})
.register();
JX.DOM.listen(panel, 'gesture.swipe.end', null, function(e) {
var data = e.getData();
if (data.length <= (JX.Vector.getDim(panel) / 2)) {
// If the user didn't move their finger far enough, don't switch.
return;
}
switch_image(data.direction == 'right' ? -1 : 1);
});
/* -( Render )------------------------------------------------------------- */
function render_image_info(image) {
var info = [];
var title = JX.$N(
'div',
{className: 'pholio-image-title'},
image.title);
info.push(title);
var desc = JX.$N(
'div',
{className: 'pholio-image-description'},
image.desc);
info.push(desc);
if (!image.isObsolete) {
var embed = JX.$N(
'div',
{className: 'pholio-image-embedding'},
JX.$H('Embed this image:<br />{M' + config.mockID +
', image=' + image.id + '}'));
info.push(embed);
}
// Render image dimensions and visible size. If we have this infomation
// from the server we can display some of it immediately; otherwise, we need
// to wait for the image to load so we can read dimension information from
// it.
var image_x = image.width;
var image_y = image.height;
var display_x = null;
if (image.tag) {
image_x = image.tag.naturalWidth;
image_y = image.tag.naturalHeight;
display_x = image.tag.width;
}
var visible = [];
if (image_x) {
visible.push([image_x, '\u00d7', image_y, 'px']);
if (display_x) {
var area = Math.round(100 * (display_x / image_x));
visible.push(' ');
visible.push(
JX.$N(
'span',
{className: 'pholio-visible-size'},
['(', area, '%', ')']));
}
}
if (visible.length) {
info.push(visible);
}
var full_link = JX.$N(
'a',
{href: image.fullURI, target: '_blank'},
'View Full Image');
info.push(full_link);
if (config.viewMode != 'history') {
var history_link = JX.$N(
'a',
{ href: image.historyURI },
'View Image History');
info.push(history_link);
}
for (var ii = 0; ii < info.length; ii++) {
info[ii] = JX.$N('div', {className: 'pholio-image-info-item'}, info[ii]);
}
info = JX.$N('div', {className: 'pholio-image-info'}, info);
return info;
}
function render_reticle_border() {
return JX.$N(
'div',
{className: 'pholio-mock-select-border'});
}
function render_reticle_fill() {
return JX.$N(
'div',
{className: 'pholio-mock-select-fill'});
}
/* -( Device Lightbox )---------------------------------------------------- */
// On devices, we show images full-size when the user taps them instead of
// attempting to implement inlines.
var lightbox = null;
JX.Stratcom.listen('click', 'mock-viewport', function(e) {
if (!e.isNormalMouseEvent()) {
return;
}
if (JX.Device.getDevice() == 'desktop') {
return;
}
lightbox_attach();
e.kill();
});
JX.Stratcom.listen('click', 'pholio-device-lightbox', lightbox_detach);
JX.Stratcom.listen('resize', null, lightbox_resize);
function lightbox_attach() {
JX.DOM.alterClass(document.body, 'lightbox-attached', true);
JX.Mask.show('jx-dark-mask');
lightbox = lightbox_render();
var image = JX.$N('img');
image.onload = lightbox_loaded;
setTimeout(function() {
image.src = active_image.fullURI;
}, 1000);
JX.DOM.setContent(lightbox, image);
JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', true);
lightbox_resize();
document.body.appendChild(lightbox);
}
function lightbox_detach() {
JX.DOM.remove(lightbox);
JX.Mask.hide();
JX.DOM.alterClass(document.body, 'lightbox-attached', false);
lightbox = null;
}
function lightbox_resize(e) {
if (!lightbox) {
return;
}
JX.Vector.getScroll().setPos(lightbox);
JX.Vector.getViewport().setDim(lightbox);
}
function lightbox_loaded() {
JX.DOM.alterClass(lightbox, 'pholio-device-lightbox-loading', false);
}
function lightbox_render() {
var el = JX.$N('div', {className: 'pholio-device-lightbox'});
JX.Stratcom.addSigil(el, 'pholio-device-lightbox');
return el;
}
/* -( Preload )------------------------------------------------------------ */
var preload = [];
for (var ii = 0; ii < config.images.length; ii++) {
preload.push(config.images[ii].fullURI);
}
function preload_next() {
next_src = preload[0];
if (!next_src) {
return;
}
preload.splice(0, 1);
var img = JX.$N('img');
img.onload = preload_next;
img.onerror = preload_next;
img.src = next_src;
}
preload_next();
});