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