Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/js/application/diff/DiffChangesetList.js
12242 views
1
/**
2
* @provides phabricator-diff-changeset-list
3
* @requires javelin-install
4
* phuix-button-view
5
* phabricator-diff-tree-view
6
* @javelin
7
*/
8
9
JX.install('DiffChangesetList', {
10
11
construct: function() {
12
this._changesets = [];
13
14
var onload = JX.bind(this, this._ifawake, this._onload);
15
JX.Stratcom.listen('click', 'differential-load', onload);
16
17
var onmore = JX.bind(this, this._ifawake, this._onmore);
18
JX.Stratcom.listen('click', 'show-more', onmore);
19
20
var onmenu = JX.bind(this, this._ifawake, this._onmenu);
21
JX.Stratcom.listen('click', 'differential-view-options', onmenu);
22
23
var onexpand = JX.bind(this, this._ifawake, this._oncollapse, false);
24
JX.Stratcom.listen('click', 'reveal-inline', onexpand);
25
26
var onresize = JX.bind(this, this._ifawake, this._onresize);
27
JX.Stratcom.listen('resize', null, onresize);
28
29
var onscroll = JX.bind(this, this._ifawake, this._onscroll);
30
JX.Stratcom.listen('scroll', null, onscroll);
31
32
JX.enableDispatch(window, 'selectstart');
33
34
var onselect = JX.bind(this, this._ifawake, this._onselect);
35
JX.Stratcom.listen(
36
['mousedown', 'selectstart'],
37
['differential-inline-comment', 'differential-inline-header'],
38
onselect);
39
40
var onhover = JX.bind(this, this._ifawake, this._onhover);
41
JX.Stratcom.listen(
42
['mouseover', 'mouseout'],
43
'differential-inline-comment',
44
onhover);
45
46
var onrangedown = JX.bind(this, this._ifawake, this._onrangedown);
47
JX.Stratcom.listen(
48
'mousedown',
49
['differential-changeset', 'tag:td'],
50
onrangedown);
51
52
var onrangemove = JX.bind(this, this._ifawake, this._onrangemove);
53
JX.Stratcom.listen(
54
['mouseover', 'mouseout'],
55
['differential-changeset', 'tag:td'],
56
onrangemove);
57
58
var onrangeup = JX.bind(this, this._ifawake, this._onrangeup);
59
JX.Stratcom.listen(
60
'mouseup',
61
null,
62
onrangeup);
63
64
var onrange = JX.bind(this, this._ifawake, this._onSelectRange);
65
JX.enableDispatch(window, 'selectionchange');
66
JX.Stratcom.listen('selectionchange', null, onrange);
67
68
this._setupInlineCommentListeners();
69
},
70
71
properties: {
72
translations: null,
73
inlineURI: null,
74
inlineListURI: null,
75
isStandalone: false,
76
formationView: null
77
},
78
79
members: {
80
_initialized: false,
81
_asleep: true,
82
_changesets: null,
83
84
_cursorItem: null,
85
86
_focusNode: null,
87
_focusStart: null,
88
_focusEnd: null,
89
90
_hoverInline: null,
91
_hoverOrigin: null,
92
_hoverTarget: null,
93
94
_rangeActive: false,
95
_rangeOrigin: null,
96
_rangeTarget: null,
97
98
_bannerNode: null,
99
_unsavedButton: null,
100
_unsubmittedButton: null,
101
_doneButton: null,
102
_doneMode: null,
103
104
_dropdownMenu: null,
105
_menuButton: null,
106
_menuItems: null,
107
_selectedChangeset: null,
108
109
sleep: function() {
110
this._asleep = true;
111
112
this._redrawFocus();
113
this._redrawSelection();
114
this.resetHover();
115
116
this._bannerChangeset = null;
117
this._redrawBanner();
118
},
119
120
wake: function() {
121
this._asleep = false;
122
123
this._redrawFocus();
124
this._redrawSelection();
125
126
this._bannerChangeset = null;
127
this._redrawBanner();
128
129
this._redrawFiletree();
130
131
if (this._initialized) {
132
return;
133
}
134
135
this._initialized = true;
136
var pht = this.getTranslations();
137
138
// We may be viewing the normal "/D123" view (with all the changesets)
139
// or the standalone view (with just one changeset). In the standalone
140
// view, some options (like jumping to next or previous file) do not
141
// make sense and do not function.
142
var standalone = this.getIsStandalone();
143
144
var label;
145
146
if (!standalone) {
147
label = pht('Jump to the table of contents.');
148
this._installKey('t', 'diff-nav', label, this._ontoc);
149
150
label = pht('Jump to the comment area.');
151
this._installKey('x', 'diff-nav', label, this._oncomments);
152
}
153
154
label = pht('Jump to next change.');
155
this._installJumpKey('j', label, 1);
156
157
label = pht('Jump to previous change.');
158
this._installJumpKey('k', label, -1);
159
160
if (!standalone) {
161
label = pht('Jump to next file.');
162
this._installJumpKey('J', label, 1, 'file');
163
164
label = pht('Jump to previous file.');
165
this._installJumpKey('K', label, -1, 'file');
166
}
167
168
label = pht('Jump to next inline comment.');
169
this._installJumpKey('n', label, 1, 'comment');
170
171
label = pht('Jump to previous inline comment.');
172
this._installJumpKey('p', label, -1, 'comment');
173
174
label = pht('Jump to next inline comment, including collapsed comments.');
175
this._installJumpKey('N', label, 1, 'comment', true);
176
177
label = pht(
178
'Jump to previous inline comment, including collapsed comments.');
179
this._installJumpKey('P', label, -1, 'comment', true);
180
181
var formation = this.getFormationView();
182
if (formation) {
183
var filetree = formation.getColumn(0);
184
var toggletree = JX.bind(filetree, filetree.toggleVisibility);
185
label = pht('Hide or show the paths panel.');
186
this._installKey('f', 'diff-vis', label, toggletree);
187
}
188
189
if (!standalone) {
190
label = pht('Hide or show the current changeset.');
191
this._installKey('h', 'diff-vis', label, this._onkeytogglefile);
192
}
193
194
label = pht('Reply to selected inline comment or change.');
195
this._installKey('r', 'inline', label,
196
JX.bind(this, this._onkeyreply, false));
197
198
label = pht('Reply and quote selected inline comment.');
199
this._installKey('R', 'inline', label,
200
JX.bind(this, this._onkeyreply, true));
201
202
label = pht('Add new inline comment on selected source text.');
203
this._installKey('c', 'inline', label,
204
JX.bind(this, this._onKeyCreate));
205
206
label = pht('Edit selected inline comment.');
207
this._installKey('e', 'inline', label, this._onkeyedit);
208
209
label = pht('Mark or unmark selected inline comment as done.');
210
this._installKey('w', 'inline', label, this._onkeydone);
211
212
label = pht('Collapse or expand inline comment.');
213
this._installKey('q', 'diff-vis', label, this._onkeycollapse);
214
215
label = pht('Hide or show all inline comments.');
216
this._installKey('A', 'diff-vis', label, this._onkeyhideall);
217
218
label = pht('Show path in repository.');
219
this._installKey('d', 'diff-nav', label, this._onkeyshowpath);
220
221
label = pht('Show directory in repository.');
222
this._installKey('D', 'diff-nav', label, this._onkeyshowdirectory);
223
224
label = pht('Open file in external editor.');
225
this._installKey('\\', 'diff-nav', label, this._onkeyopeneditor);
226
},
227
228
isAsleep: function() {
229
return this._asleep;
230
},
231
232
newChangesetForNode: function(node) {
233
var changeset = JX.DiffChangeset.getForNode(node);
234
235
this._changesets.push(changeset);
236
changeset.setChangesetList(this);
237
238
return changeset;
239
},
240
241
getChangesetForNode: function(node) {
242
return JX.DiffChangeset.getForNode(node);
243
},
244
245
getInlineByID: function(id) {
246
var inline = null;
247
248
for (var ii = 0; ii < this._changesets.length; ii++) {
249
inline = this._changesets[ii].getInlineByID(id);
250
if (inline) {
251
break;
252
}
253
}
254
255
return inline;
256
},
257
258
_ifawake: function(f) {
259
// This function takes another function and only calls it if the
260
// changeset list is awake, so we basically just ignore events when we
261
// are asleep. This may move up the stack at some point as we do more
262
// with Quicksand/Sheets.
263
264
if (this.isAsleep()) {
265
return;
266
}
267
268
return f.apply(this, [].slice.call(arguments, 1));
269
},
270
271
_onload: function(e) {
272
var data = e.getNodeData('differential-load');
273
274
// NOTE: We can trigger a load from either an explicit "Load" link on
275
// the changeset, or by clicking a link in the table of contents. If
276
// the event was a table of contents link, we let the anchor behavior
277
// run normally.
278
if (data.kill) {
279
e.kill();
280
}
281
282
var node = JX.$(data.id);
283
var changeset = this.getChangesetForNode(node);
284
285
changeset.load();
286
287
// TODO: Move this into Changeset.
288
var routable = changeset.getRoutable();
289
if (routable) {
290
routable.setPriority(2000);
291
}
292
},
293
294
_installKey: function(key, group, label, handler) {
295
handler = JX.bind(this, this._ifawake, handler);
296
297
return new JX.KeyboardShortcut(key, label)
298
.setHandler(handler)
299
.setGroup(group)
300
.register();
301
},
302
303
_installJumpKey: function(key, label, delta, filter, show_collapsed) {
304
filter = filter || null;
305
306
var options = {
307
filter: filter,
308
collapsed: show_collapsed
309
};
310
311
var handler = JX.bind(this, this._onjumpkey, delta, options);
312
return this._installKey(key, 'diff-nav', label, handler);
313
},
314
315
_ontoc: function(manager) {
316
var toc = JX.$('toc');
317
manager.scrollTo(toc);
318
},
319
320
_oncomments: function(manager) {
321
var reply = JX.$('reply');
322
manager.scrollTo(reply);
323
},
324
325
getSelectedInline: function() {
326
var cursor = this._cursorItem;
327
328
if (cursor) {
329
if (cursor.type == 'comment') {
330
return cursor.target;
331
}
332
}
333
334
return null;
335
},
336
337
_onkeyreply: function(is_quote) {
338
var cursor = this._cursorItem;
339
340
if (cursor) {
341
if (cursor.type == 'comment') {
342
var inline = cursor.target;
343
if (inline.canReply()) {
344
this.setFocus(null);
345
inline.reply(is_quote);
346
return;
347
}
348
}
349
350
// If the keyboard cursor is selecting a range of lines, we may have
351
// a mixture of old and new changes on the selected rows. It is not
352
// entirely unambiguous what the user means when they say they want
353
// to reply to this, but we use this logic: reply on the new file if
354
// there are any new lines. Otherwise (if there are only removed
355
// lines) reply on the old file.
356
357
if (cursor.type == 'change') {
358
var cells = this._getLineNumberCellsForChangeBlock(
359
cursor.nodes.begin,
360
cursor.nodes.end);
361
362
cursor.changeset.newInlineForRange(cells.src, cells.dst);
363
364
this.setFocus(null);
365
return;
366
}
367
}
368
369
var pht = this.getTranslations();
370
this._warnUser(pht('You must select a comment or change to reply to.'));
371
},
372
373
_getLineNumberCellsForChangeBlock: function(origin, target) {
374
// The "origin" and "target" are entire rows, but we need to find
375
// a range of cell nodes to actually create an inline, so go
376
// fishing.
377
378
var old_list = [];
379
var new_list = [];
380
381
var row = origin;
382
while (row) {
383
var header = row.firstChild;
384
while (header) {
385
if (this.getLineNumberFromHeader(header)) {
386
if (header.className.indexOf('old') !== -1) {
387
old_list.push(header);
388
} else if (header.className.indexOf('new') !== -1) {
389
new_list.push(header);
390
}
391
}
392
header = header.nextSibling;
393
}
394
395
if (row == target) {
396
break;
397
}
398
399
row = row.nextSibling;
400
}
401
402
var use_list;
403
if (new_list.length) {
404
use_list = new_list;
405
} else {
406
use_list = old_list;
407
}
408
409
var src = use_list[0];
410
var dst = use_list[use_list.length - 1];
411
412
return {
413
src: src,
414
dst: dst
415
};
416
},
417
418
_onkeyedit: function() {
419
var cursor = this._cursorItem;
420
421
if (cursor) {
422
if (cursor.type == 'comment') {
423
var inline = cursor.target;
424
if (inline.canEdit()) {
425
this.setFocus(null);
426
427
inline.edit();
428
return;
429
}
430
}
431
}
432
433
var pht = this.getTranslations();
434
this._warnUser(pht('You must select a comment to edit.'));
435
},
436
437
_onKeyCreate: function() {
438
var start = this._sourceSelectionStart;
439
var end = this._sourceSelectionEnd;
440
441
if (!this._sourceSelectionStart) {
442
var pht = this.getTranslations();
443
this._warnUser(
444
pht(
445
'You must select source text to create a new inline comment.'));
446
return;
447
}
448
449
this._setSourceSelection(null, null);
450
451
var changeset = start.changeset;
452
453
var config = {};
454
if (changeset.getResponseDocumentEngineKey() === null) {
455
// If the changeset is using a document renderer, we ignore the
456
// selection range and just treat this as a comment from the first
457
// block to the last block.
458
459
// If we don't discard the range, we later render a bogus highlight
460
// if the block content is complex (like a Jupyter notebook cell
461
// with images).
462
463
config.startOffset = start.offset;
464
config.endOffset = end.offset;
465
}
466
467
changeset.newInlineForRange(start.targetNode, end.targetNode, config);
468
},
469
470
_onkeydone: function() {
471
var cursor = this._cursorItem;
472
473
if (cursor) {
474
if (cursor.type == 'comment') {
475
var inline = cursor.target;
476
if (inline.canDone()) {
477
this.setFocus(null);
478
479
inline.toggleDone();
480
return;
481
}
482
}
483
}
484
485
var pht = this.getTranslations();
486
this._warnUser(pht('You must select a comment to mark done.'));
487
},
488
489
_onkeytogglefile: function() {
490
var pht = this.getTranslations();
491
var changeset = this._getChangesetForKeyCommand();
492
493
if (!changeset) {
494
this._warnUser(pht('You must select a file to hide or show.'));
495
return;
496
}
497
498
changeset.toggleVisibility();
499
},
500
501
_getChangesetForKeyCommand: function() {
502
var cursor = this._cursorItem;
503
504
var changeset;
505
if (cursor) {
506
changeset = cursor.changeset;
507
}
508
509
if (!changeset) {
510
changeset = this._getVisibleChangeset();
511
}
512
513
return changeset;
514
},
515
516
_onkeyopeneditor: function(e) {
517
var pht = this.getTranslations();
518
var changeset = this._getChangesetForKeyCommand();
519
520
if (!changeset) {
521
this._warnUser(pht('You must select a file to edit.'));
522
return;
523
}
524
525
this._openEditor(changeset);
526
},
527
528
_openEditor: function(changeset) {
529
var pht = this.getTranslations();
530
531
var editor_template = changeset.getEditorURITemplate();
532
if (editor_template === null) {
533
this._warnUser(pht('No external editor is configured.'));
534
return;
535
}
536
537
var line = null;
538
539
// See PHI1749. We aren't exactly sure what the user intends when they
540
// use the keyboard to select a change block and then activate the
541
// "Open in Editor" function: they might mean to open the old or new
542
// offset, and may have the old or new state (or some other state) in
543
// their working copy.
544
545
// For now, pick: the new state line number if one exists; or the old
546
// state line number if one does not. If nothing else, this behavior is
547
// simple.
548
549
// If there's a document engine, just open the file to the first line.
550
// We currently can not map display blocks to source lines.
551
552
// If there's an inline, open the file to that line.
553
554
if (changeset.getResponseDocumentEngineKey() === null) {
555
var cursor = this._cursorItem;
556
if (cursor && (cursor.changeset === changeset)) {
557
if (cursor.type == 'change') {
558
var cells = this._getLineNumberCellsForChangeBlock(
559
cursor.nodes.begin,
560
cursor.nodes.end);
561
line = this.getLineNumberFromHeader(cells.src);
562
}
563
564
if (cursor.type === 'comment') {
565
var inline = cursor.target;
566
line = inline.getLineNumber();
567
}
568
}
569
}
570
571
var variables = {
572
l: line || 1
573
};
574
575
var editor_uri = new JX.ExternalEditorLinkEngine()
576
.setTemplate(editor_template)
577
.setVariables(variables)
578
.newURI();
579
580
JX.$U(editor_uri).go();
581
},
582
583
_onkeyshowpath: function() {
584
this._onrepositorykey(false);
585
},
586
587
_onkeyshowdirectory: function() {
588
this._onrepositorykey(true);
589
},
590
591
_onrepositorykey: function(is_directory) {
592
var pht = this.getTranslations();
593
var changeset = this._getChangesetForKeyCommand();
594
595
if (!changeset) {
596
this._warnUser(pht('You must select a file to open.'));
597
return;
598
}
599
600
var show_uri;
601
if (is_directory) {
602
show_uri = changeset.getShowDirectoryURI();
603
} else {
604
show_uri = changeset.getShowPathURI();
605
}
606
607
if (show_uri === null) {
608
return;
609
}
610
611
window.open(show_uri);
612
},
613
614
_onkeycollapse: function() {
615
var cursor = this._cursorItem;
616
617
if (cursor) {
618
if (cursor.type == 'comment') {
619
var inline = cursor.target;
620
if (inline.canCollapse()) {
621
this.setFocus(null);
622
623
inline.setCollapsed(!inline.isCollapsed());
624
return;
625
}
626
}
627
}
628
629
var pht = this.getTranslations();
630
this._warnUser(pht('You must select a comment to hide.'));
631
},
632
633
_onkeyhideall: function() {
634
var inlines = this._getInlinesByType();
635
if (inlines.visible.length) {
636
this._toggleInlines('all');
637
} else {
638
this._toggleInlines('show');
639
}
640
},
641
642
_warnUser: function(message) {
643
new JX.Notification()
644
.setContent(message)
645
.alterClassName('jx-notification-alert', true)
646
.setDuration(3000)
647
.show();
648
},
649
650
_onjumpkey: function(delta, options) {
651
var state = this._getSelectionState();
652
653
var filter = options.filter || null;
654
var collapsed = options.collapsed || false;
655
var wrap = options.wrap || false;
656
var attribute = options.attribute || null;
657
var show = options.show || false;
658
659
var cursor = state.cursor;
660
var items = state.items;
661
662
// If there's currently no selection and the user tries to go back,
663
// don't do anything.
664
if ((cursor === null) && (delta < 0)) {
665
return;
666
}
667
668
var did_wrap = false;
669
while (true) {
670
if (cursor === null) {
671
cursor = 0;
672
} else {
673
cursor = cursor + delta;
674
}
675
676
// If we've gone backward past the first change, bail out.
677
if (cursor < 0) {
678
return;
679
}
680
681
// If we've gone forward off the end of the list, figure out where we
682
// should end up.
683
if (cursor >= items.length) {
684
if (!wrap) {
685
// If we aren't wrapping around, we're done.
686
return;
687
}
688
689
if (did_wrap) {
690
// If we're already wrapped around, we're done.
691
return;
692
}
693
694
// Otherwise, wrap the cursor back to the top.
695
cursor = 0;
696
did_wrap = true;
697
}
698
699
// If we're selecting things of a particular type (like only files)
700
// and the next item isn't of that type, move past it.
701
if (filter !== null) {
702
if (items[cursor].type !== filter) {
703
continue;
704
}
705
}
706
707
// If the item is collapsed, don't select it when iterating with jump
708
// keys. It can still potentially be selected in other ways.
709
if (!collapsed) {
710
if (items[cursor].collapsed) {
711
continue;
712
}
713
}
714
715
// If the item has been deleted, don't select it when iterating. The
716
// cursor may remain on it until it is removed.
717
if (items[cursor].deleted) {
718
continue;
719
}
720
721
// If we're selecting things with a particular attribute, like
722
// "unsaved", skip items without the attribute.
723
if (attribute !== null) {
724
if (!(items[cursor].attributes || {})[attribute]) {
725
continue;
726
}
727
}
728
729
// If this item is a hidden inline but we're clicking a button which
730
// selects inlines of a particular type, make it visible again.
731
if (items[cursor].hidden) {
732
if (!show) {
733
continue;
734
}
735
items[cursor].target.setHidden(false);
736
}
737
738
// Otherwise, we've found a valid item to select.
739
break;
740
}
741
742
this._setSelectionState(items[cursor], true);
743
},
744
745
_getSelectionState: function() {
746
var items = this._getSelectableItems();
747
748
var cursor = null;
749
if (this._cursorItem !== null) {
750
for (var ii = 0; ii < items.length; ii++) {
751
var item = items[ii];
752
if (this._cursorItem.target === item.target) {
753
cursor = ii;
754
break;
755
}
756
}
757
}
758
759
return {
760
cursor: cursor,
761
items: items
762
};
763
},
764
765
selectChangeset: function(changeset, scroll) {
766
var items = this._getSelectableItems();
767
768
var cursor = null;
769
for (var ii = 0; ii < items.length; ii++) {
770
var item = items[ii];
771
if (changeset === item.target) {
772
cursor = ii;
773
break;
774
}
775
}
776
777
if (cursor !== null) {
778
this._setSelectionState(items[cursor], scroll);
779
} else {
780
this._setSelectionState(null, false);
781
}
782
783
return this;
784
},
785
786
_setSelectionState: function(item, scroll) {
787
var old = this._cursorItem;
788
789
if (old) {
790
if (old.type === 'comment') {
791
old.target.setIsSelected(false);
792
}
793
}
794
795
this._cursorItem = item;
796
797
if (item) {
798
if (item.type === 'comment') {
799
item.target.setIsSelected(true);
800
}
801
}
802
803
this._redrawSelection(scroll);
804
805
return this;
806
},
807
808
_redrawSelection: function(scroll) {
809
var cursor = this._cursorItem;
810
if (!cursor) {
811
this.setFocus(null);
812
return;
813
}
814
815
// If this item has been removed from the document (for example: create
816
// a new empty comment, then use the "Unsaved" button to select it, then
817
// cancel it), we can still keep the cursor here but do not want to show
818
// a selection reticle over an invisible node.
819
if (cursor.deleted) {
820
this.setFocus(null);
821
return;
822
}
823
824
var changeset = cursor.changeset;
825
826
var tree = this._getTreeView();
827
if (changeset) {
828
tree.setSelectedPath(cursor.changeset.getPathView());
829
} else {
830
tree.setSelectedPath(null);
831
}
832
833
this._selectChangeset(changeset);
834
835
this.setFocus(cursor.nodes.begin, cursor.nodes.end);
836
837
if (scroll) {
838
var pos = JX.$V(cursor.nodes.begin);
839
JX.DOM.scrollToPosition(0, pos.y - 60);
840
}
841
842
return this;
843
},
844
845
redrawCursor: function() {
846
// NOTE: This is setting the cursor to the current cursor. Usually, this
847
// would have no effect.
848
849
// However, if the old cursor pointed at an inline and the inline has
850
// been edited so the rows have changed, this updates the cursor to point
851
// at the new inline with the proper rows for the current state, and
852
// redraws the reticle correctly.
853
854
var state = this._getSelectionState();
855
if (state.cursor !== null) {
856
this._setSelectionState(state.items[state.cursor], false);
857
}
858
},
859
860
_getSelectableItems: function() {
861
var result = [];
862
863
for (var ii = 0; ii < this._changesets.length; ii++) {
864
var items = this._changesets[ii].getSelectableItems();
865
for (var jj = 0; jj < items.length; jj++) {
866
result.push(items[jj]);
867
}
868
}
869
870
return result;
871
},
872
873
_onhover: function(e) {
874
if (e.getIsTouchEvent()) {
875
return;
876
}
877
878
var inline;
879
if (e.getType() == 'mouseout') {
880
inline = null;
881
} else {
882
inline = this._getInlineForEvent(e);
883
}
884
885
this._setHoverInline(inline);
886
},
887
888
_onmore: function(e) {
889
e.kill();
890
891
var node = e.getNode('differential-changeset');
892
var changeset = this.getChangesetForNode(node);
893
894
var data = e.getNodeData('show-more');
895
var target = e.getNode('context-target');
896
897
changeset.loadContext(data.range, target);
898
},
899
900
_onmenu: function(e) {
901
var button = e.getNode('differential-view-options');
902
903
var data = JX.Stratcom.getData(button);
904
if (data.menu) {
905
// We've already built this menu, so we can let the menu itself handle
906
// the event.
907
return;
908
}
909
910
e.prevent();
911
912
var pht = this.getTranslations();
913
914
var node = JX.DOM.findAbove(
915
button,
916
'div',
917
'differential-changeset');
918
919
var changeset_list = this;
920
var changeset = this.getChangesetForNode(node);
921
922
var menu = new JX.PHUIXDropdownMenu(button)
923
.setWidth(240);
924
var list = new JX.PHUIXActionListView();
925
926
var add_link = function(icon, name, href, local) {
927
var link = new JX.PHUIXActionView()
928
.setIcon(icon)
929
.setName(name)
930
.setHandler(function(e) {
931
if (local) {
932
window.location.assign(href);
933
} else {
934
window.open(href);
935
}
936
menu.close();
937
e.prevent();
938
});
939
940
if (href) {
941
link.setHref(href);
942
} else {
943
link
944
.setDisabled(true)
945
.setUnresponsive(true);
946
}
947
948
list.addItem(link);
949
return link;
950
};
951
952
var visible_item = new JX.PHUIXActionView()
953
.setKeyCommand('h')
954
.setHandler(function(e) {
955
e.prevent();
956
menu.close();
957
958
changeset.select(false);
959
changeset.toggleVisibility();
960
});
961
list.addItem(visible_item);
962
963
var reveal_item = new JX.PHUIXActionView()
964
.setIcon('fa-eye');
965
list.addItem(reveal_item);
966
967
list.addItem(
968
new JX.PHUIXActionView()
969
.setDivider(true));
970
971
var up_item = new JX.PHUIXActionView()
972
.setHandler(function(e) {
973
if (changeset.isLoaded()) {
974
975
// Don't let the user swap display modes if a comment is being
976
// edited, since they might lose their work. See PHI180.
977
var inlines = changeset.getInlines();
978
for (var ii = 0; ii < inlines.length; ii++) {
979
if (inlines[ii].isEditing()) {
980
changeset_list._warnUser(
981
pht(
982
'Finish editing inline comments before changing display ' +
983
'modes.'));
984
e.prevent();
985
menu.close();
986
return;
987
}
988
}
989
990
var renderer = changeset.getRendererKey();
991
if (renderer == '1up') {
992
renderer = '2up';
993
} else {
994
renderer = '1up';
995
}
996
changeset.reload({renderer: renderer});
997
} else {
998
changeset.reload();
999
}
1000
1001
e.prevent();
1002
menu.close();
1003
});
1004
list.addItem(up_item);
1005
1006
var encoding_item = new JX.PHUIXActionView()
1007
.setIcon('fa-font')
1008
.setName(pht('Change Text Encoding...'))
1009
.setHandler(function(e) {
1010
var params = {
1011
encoding: changeset.getCharacterEncoding()
1012
};
1013
1014
new JX.Workflow('/services/encoding/', params)
1015
.setHandler(function(r) {
1016
changeset.reload({encoding: r.encoding});
1017
})
1018
.start();
1019
1020
e.prevent();
1021
menu.close();
1022
});
1023
list.addItem(encoding_item);
1024
1025
var highlight_item = new JX.PHUIXActionView()
1026
.setIcon('fa-sun-o')
1027
.setName(pht('Highlight As...'))
1028
.setHandler(function(e) {
1029
var params = {
1030
highlight: changeset.getHighlight()
1031
};
1032
1033
new JX.Workflow('/services/highlight/', params)
1034
.setHandler(function(r) {
1035
changeset.reload({highlight: r.highlight});
1036
})
1037
.start();
1038
1039
e.prevent();
1040
menu.close();
1041
});
1042
list.addItem(highlight_item);
1043
1044
var engine_item = new JX.PHUIXActionView()
1045
.setIcon('fa-file-image-o')
1046
.setName(pht('View As Document Type...'))
1047
.setHandler(function(e) {
1048
var options = changeset.getAvailableDocumentEngineKeys() || [];
1049
options = options.join(',');
1050
1051
var params = {
1052
engine: changeset.getResponseDocumentEngineKey(),
1053
options: options
1054
};
1055
1056
new JX.Workflow('/services/viewas/', params)
1057
.setHandler(function(r) {
1058
changeset.reload({engine: r.engine});
1059
})
1060
.start();
1061
1062
e.prevent();
1063
menu.close();
1064
});
1065
list.addItem(engine_item);
1066
1067
list.addItem(
1068
new JX.PHUIXActionView()
1069
.setDivider(true));
1070
1071
add_link('fa-external-link', pht('View Standalone'), data.standaloneURI);
1072
1073
add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI);
1074
add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI);
1075
1076
add_link(
1077
'fa-folder-open-o',
1078
pht('Show Directory in Repository'),
1079
changeset.getShowDirectoryURI())
1080
.setKeyCommand('D');
1081
1082
add_link(
1083
'fa-file-text-o',
1084
pht('Show Path in Repository'),
1085
changeset.getShowPathURI())
1086
.setKeyCommand('d');
1087
1088
var editor_template = changeset.getEditorURITemplate();
1089
if (editor_template !== null) {
1090
var editor_item = new JX.PHUIXActionView()
1091
.setIcon('fa-i-cursor')
1092
.setName(pht('Open in Editor'))
1093
.setKeyCommand('\\')
1094
.setHandler(function(e) {
1095
1096
changeset_list._openEditor(changeset);
1097
1098
e.prevent();
1099
menu.close();
1100
});
1101
1102
list.addItem(editor_item);
1103
} else {
1104
var configure_uri = changeset.getEditorConfigureURI();
1105
if (configure_uri !== null) {
1106
add_link('fa-wrench', pht('Configure Editor'), configure_uri);
1107
}
1108
}
1109
1110
menu.setContent(list.getNode());
1111
1112
menu.listen('open', function() {
1113
// When the user opens the menu, check if there are any "Show More"
1114
// links in the changeset body. If there aren't, disable the "Show
1115
// Entire File" menu item since it won't change anything.
1116
1117
var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');
1118
if (nodes.length) {
1119
reveal_item
1120
.setDisabled(false)
1121
.setName(pht('Show All Context'))
1122
.setIcon('fa-arrows-v')
1123
.setHandler(function(e) {
1124
changeset.loadAllContext();
1125
e.prevent();
1126
menu.close();
1127
});
1128
} else {
1129
reveal_item
1130
.setDisabled(true)
1131
.setUnresponsive(true)
1132
.setIcon('fa-file')
1133
.setName(pht('All Context Shown'))
1134
.setHref(null);
1135
}
1136
1137
encoding_item.setDisabled(!changeset.isLoaded());
1138
highlight_item.setDisabled(!changeset.isLoaded());
1139
engine_item.setDisabled(!changeset.isLoaded());
1140
1141
if (changeset.isLoaded()) {
1142
if (changeset.getRendererKey() == '2up') {
1143
up_item
1144
.setIcon('fa-list-alt')
1145
.setName(pht('View Unified Diff'));
1146
} else {
1147
up_item
1148
.setIcon('fa-columns')
1149
.setName(pht('View Side-by-Side Diff'));
1150
}
1151
} else {
1152
up_item
1153
.setIcon('fa-refresh')
1154
.setName(pht('Load Changes'));
1155
}
1156
1157
visible_item
1158
.setDisabled(true)
1159
.setIcon('fa-eye-slash')
1160
.setName(pht('Hide Changeset'));
1161
1162
var diffs = JX.DOM.scry(
1163
JX.$(data.containerID),
1164
'table',
1165
'differential-diff');
1166
1167
if (diffs.length > 1) {
1168
JX.$E(
1169
'More than one node with sigil "differential-diff" was found in "'+
1170
data.containerID+'."');
1171
} else if (diffs.length == 1) {
1172
visible_item.setDisabled(false);
1173
} else {
1174
// Do nothing when there is no diff shown in the table. For example,
1175
// the file is binary.
1176
}
1177
1178
});
1179
1180
data.menu = menu;
1181
changeset.setViewMenu(menu);
1182
menu.open();
1183
},
1184
1185
_oncollapse: function(is_collapse, e) {
1186
e.kill();
1187
1188
var inline = this._getInlineForEvent(e);
1189
1190
inline.setCollapsed(is_collapse);
1191
},
1192
1193
_onresize: function() {
1194
this._redrawFocus();
1195
this._redrawSelection();
1196
1197
// Force a banner redraw after a resize event. Particularly, this makes
1198
// sure the inline state updates immediately after an inline edit
1199
// operation, even if the changeset itself has not changed.
1200
this._bannerChangeset = null;
1201
1202
this._redrawBanner();
1203
1204
var changesets = this._changesets;
1205
for (var ii = 0; ii < changesets.length; ii++) {
1206
changesets[ii].redrawFileTree();
1207
}
1208
},
1209
1210
_onscroll: function() {
1211
this._redrawBanner();
1212
},
1213
1214
_onselect: function(e) {
1215
// If the user clicked some element inside the header, like an action
1216
// icon, ignore the event. They have to click the header element itself.
1217
if (e.getTarget() !== e.getNode('differential-inline-header')) {
1218
return;
1219
}
1220
1221
// If the user has double-clicked or triple-clicked a header, we want to
1222
// toggle the inline selection mode, not select text. Kill select events
1223
// originating with this element as the target.
1224
if (e.getType() === 'selectstart') {
1225
e.kill();
1226
return;
1227
}
1228
1229
var inline = this._getInlineForEvent(e);
1230
if (!inline) {
1231
return;
1232
}
1233
1234
// NOTE: Don't kill or prevent the event. In particular, we want this
1235
// click to clear any text selection as it normally would.
1236
1237
this.selectInline(inline);
1238
},
1239
1240
selectInline: function(inline, force, scroll) {
1241
var selection = this._getSelectionState();
1242
var item;
1243
1244
if (!force) {
1245
// If the comment the user clicked is currently selected, deselect it.
1246
// This makes it easy to undo things if you clicked by mistake.
1247
if (selection.cursor !== null) {
1248
item = selection.items[selection.cursor];
1249
if (item.target === inline) {
1250
this._setSelectionState(null, false);
1251
return;
1252
}
1253
}
1254
}
1255
1256
// Otherwise, select the item that the user clicked. This makes it
1257
// easier to resume keyboard operations after using the mouse to do
1258
// something else.
1259
var items = selection.items;
1260
for (var ii = 0; ii < items.length; ii++) {
1261
item = items[ii];
1262
if (item.target === inline) {
1263
this._setSelectionState(item, scroll);
1264
}
1265
}
1266
1267
},
1268
1269
redrawPreview: function() {
1270
// TODO: This isn't the cleanest way to find the preview form, but
1271
// rendering no longer has direct access to it.
1272
var forms = JX.DOM.scry(document.body, 'form', 'transaction-append');
1273
if (forms.length) {
1274
JX.DOM.invoke(forms[0], 'shouldRefresh');
1275
}
1276
1277
// Clear the mouse hover reticle after a substantive edit: we don't get
1278
// a "mouseout" event if the row vanished because of row being removed
1279
// after an edit.
1280
this.resetHover();
1281
},
1282
1283
setFocus: function(node, extended_node) {
1284
if (!node) {
1285
var tree = this._getTreeView();
1286
tree.setSelectedPath(null);
1287
this._selectChangeset(null);
1288
}
1289
1290
this._focusStart = node;
1291
this._focusEnd = extended_node;
1292
this._redrawFocus();
1293
},
1294
1295
_selectChangeset: function(changeset) {
1296
if (this._selectedChangeset === changeset) {
1297
return;
1298
}
1299
1300
if (this._selectedChangeset !== null) {
1301
this._selectedChangeset.setIsSelected(false);
1302
this._selectedChangeset = null;
1303
}
1304
1305
this._selectedChangeset = changeset;
1306
if (this._selectedChangeset !== null) {
1307
this._selectedChangeset.setIsSelected(true);
1308
}
1309
},
1310
1311
_redrawFocus: function() {
1312
var node = this._focusStart;
1313
var extended_node = this._focusEnd || node;
1314
1315
var reticle = this._getFocusNode();
1316
if (!node || this.isAsleep()) {
1317
JX.DOM.remove(reticle);
1318
return;
1319
}
1320
1321
// Outset the reticle some pixels away from the element, so there's some
1322
// space between the focused element and the outline.
1323
var p = JX.Vector.getPos(node);
1324
var s = JX.Vector.getAggregateScrollForNode(node);
1325
var d = JX.Vector.getDim(node);
1326
1327
p.add(s).add(d.x + 1, 4).setPos(reticle);
1328
// Compute the size we need to extend to the full extent of the focused
1329
// nodes.
1330
JX.Vector.getPos(extended_node)
1331
.add(-p.x, -p.y)
1332
.add(0, JX.Vector.getDim(extended_node).y)
1333
.add(10, -4)
1334
.setDim(reticle);
1335
1336
JX.DOM.getContentFrame().appendChild(reticle);
1337
},
1338
1339
_getFocusNode: function() {
1340
if (!this._focusNode) {
1341
var node = JX.$N('div', {className : 'keyboard-focus-focus-reticle'});
1342
this._focusNode = node;
1343
}
1344
return this._focusNode;
1345
},
1346
1347
_setHoverInline: function(inline) {
1348
var origin = null;
1349
var target = null;
1350
1351
if (inline) {
1352
var changeset = inline.getChangeset();
1353
1354
var changeset_id;
1355
var side = inline.getDisplaySide();
1356
if (side == 'right') {
1357
changeset_id = changeset.getRightChangesetID();
1358
} else {
1359
changeset_id = changeset.getLeftChangesetID();
1360
}
1361
1362
var new_part;
1363
if (inline.isNewFile()) {
1364
new_part = 'N';
1365
} else {
1366
new_part = 'O';
1367
}
1368
1369
var prefix = 'C' + changeset_id + new_part + 'L';
1370
1371
var number = inline.getLineNumber();
1372
var length = inline.getLineLength();
1373
1374
try {
1375
origin = JX.$(prefix + number);
1376
target = JX.$(prefix + (number + length));
1377
} catch (error) {
1378
// There may not be any nodes present in the document. A case where
1379
// this occurs is when you reply to a ghost inline which was made
1380
// on lines near the bottom of "long.txt" in an earlier diff, and
1381
// the file was later shortened so those lines no longer exist. For
1382
// more details, see T11662.
1383
1384
origin = null;
1385
target = null;
1386
}
1387
}
1388
1389
this._setHoverRange(origin, target, inline);
1390
},
1391
1392
_setHoverRange: function(origin, target, inline) {
1393
inline = inline || null;
1394
1395
var origin_dirty = (origin !== this._hoverOrigin);
1396
var target_dirty = (target !== this._hoverTarget);
1397
var inline_dirty = (inline !== this._hoverInline);
1398
1399
var any_dirty = (origin_dirty || target_dirty || inline_dirty);
1400
if (any_dirty) {
1401
this._hoverOrigin = origin;
1402
this._hoverTarget = target;
1403
this._hoverInline = inline;
1404
this._redrawHover();
1405
}
1406
},
1407
1408
resetHover: function() {
1409
this._setHoverRange(null, null, null);
1410
},
1411
1412
_redrawHover: function() {
1413
var map = this._hoverMap;
1414
if (map) {
1415
this._hoverMap = null;
1416
this._applyHoverHighlight(map, false);
1417
}
1418
1419
var rows = this._hoverRows;
1420
if (rows) {
1421
this._hoverRows = null;
1422
this._applyHoverHighlight(rows, false);
1423
}
1424
1425
if (!this._hoverOrigin || this.isAsleep()) {
1426
return;
1427
}
1428
1429
var top = this._hoverOrigin;
1430
var bot = this._hoverTarget;
1431
if (JX.$V(top).y > JX.$V(bot).y) {
1432
var tmp = top;
1433
top = bot;
1434
bot = tmp;
1435
}
1436
1437
// Find the leftmost cell that we're going to highlight. This is the
1438
// next sibling with a "data-copy-mode" attribute, which is a marker
1439
// for the cell with actual content in it.
1440
var content_cell = top;
1441
while (content_cell && !this._isContentCell(content_cell)) {
1442
content_cell = content_cell.nextSibling;
1443
}
1444
1445
// If we didn't find a cell to highlight, don't highlight anything.
1446
if (!content_cell) {
1447
return;
1448
}
1449
1450
rows = this._findContentCells(top, bot, content_cell);
1451
1452
var inline = this._hoverInline;
1453
if (!inline) {
1454
this._hoverRows = rows;
1455
this._applyHoverHighlight(this._hoverRows, true);
1456
return;
1457
}
1458
1459
if (!inline.hoverMap) {
1460
inline.hoverMap = this._newHoverMap(rows, inline);
1461
}
1462
1463
this._hoverMap = inline.hoverMap;
1464
this._applyHoverHighlight(this._hoverMap, true);
1465
},
1466
1467
_applyHoverHighlight: function(items, on) {
1468
for (var ii = 0; ii < items.length; ii++) {
1469
var item = items[ii];
1470
1471
JX.DOM.alterClass(item.lineNode, 'inline-hover', on);
1472
JX.DOM.alterClass(item.cellNode, 'inline-hover', on);
1473
1474
if (item.bright) {
1475
JX.DOM.alterClass(item.cellNode, 'inline-hover-bright', on);
1476
}
1477
1478
if (item.hoverNode) {
1479
if (on) {
1480
item.cellNode.insertBefore(
1481
item.hoverNode,
1482
item.cellNode.firstChild);
1483
} else {
1484
JX.DOM.remove(item.hoverNode);
1485
}
1486
}
1487
}
1488
},
1489
1490
_findContentCells: function(top, bot, content_cell) {
1491
var head_row = JX.DOM.findAbove(top, 'tr');
1492
var last_row = JX.DOM.findAbove(bot, 'tr');
1493
1494
var cursor = head_row;
1495
var rows = [];
1496
var idx = null;
1497
var ii;
1498
var line_cell = null;
1499
do {
1500
line_cell = null;
1501
for (ii = 0; ii < cursor.childNodes.length; ii++) {
1502
var child = cursor.childNodes[ii];
1503
if (!JX.DOM.isType(child, 'td')) {
1504
continue;
1505
}
1506
1507
if (child.getAttribute('data-n')) {
1508
line_cell = child;
1509
}
1510
1511
if (child === content_cell) {
1512
idx = ii;
1513
}
1514
1515
if (ii !== idx) {
1516
continue;
1517
}
1518
1519
if (this._isContentCell(child)) {
1520
rows.push({
1521
lineNode: line_cell,
1522
cellNode: child
1523
});
1524
}
1525
1526
break;
1527
}
1528
1529
if (cursor === last_row) {
1530
break;
1531
}
1532
1533
cursor = cursor.nextSibling;
1534
} while (cursor);
1535
1536
return rows;
1537
},
1538
1539
_newHoverMap: function(rows, inline) {
1540
var start = inline.getStartOffset();
1541
var end = inline.getEndOffset();
1542
1543
var info;
1544
var content;
1545
for (ii = 0; ii < rows.length; ii++) {
1546
info = this._getSelectionOffset(rows[ii].cellNode, null);
1547
1548
content = info.content;
1549
content = content.replace(/\n+$/, '');
1550
1551
rows[ii].content = content;
1552
}
1553
1554
var attr_dull = {
1555
className: 'inline-hover-text'
1556
};
1557
1558
var attr_bright = {
1559
className: 'inline-hover-text inline-hover-text-bright'
1560
};
1561
1562
var attr_container = {
1563
className: 'inline-hover-container'
1564
};
1565
1566
var min = 0;
1567
var max = rows.length - 1;
1568
var offset_min;
1569
var offset_max;
1570
var len;
1571
var node;
1572
var text;
1573
var any_highlight = false;
1574
for (ii = 0; ii < rows.length; ii++) {
1575
content = rows[ii].content;
1576
len = content.length;
1577
1578
if (ii === min && (start !== null)) {
1579
offset_min = start;
1580
} else {
1581
offset_min = 0;
1582
}
1583
1584
if (ii === max && (end !== null)) {
1585
offset_max = Math.min(end, len);
1586
} else {
1587
offset_max = len;
1588
}
1589
1590
var has_min = (offset_min > 0);
1591
var has_max = (offset_max < len);
1592
1593
if (has_min || has_max) {
1594
any_highlight = true;
1595
}
1596
1597
rows[ii].min = offset_min;
1598
rows[ii].max = offset_max;
1599
rows[ii].hasMin = has_min;
1600
rows[ii].hasMax = has_max;
1601
}
1602
1603
for (ii = 0; ii < rows.length; ii++) {
1604
content = rows[ii].content;
1605
offset_min = rows[ii].min;
1606
offset_max = rows[ii].max;
1607
1608
var has_highlight = (rows[ii].hasMin || rows[ii].hasMax);
1609
1610
if (any_highlight) {
1611
var parts = [];
1612
1613
if (offset_min > 0) {
1614
text = content.substring(0, offset_min);
1615
node = JX.$N('span', attr_dull, text);
1616
parts.push(node);
1617
}
1618
1619
if (len) {
1620
text = content.substring(offset_min, offset_max);
1621
node = JX.$N('span', attr_bright, text);
1622
parts.push(node);
1623
}
1624
1625
if (offset_max < len) {
1626
text = content.substring(offset_max, len);
1627
node = JX.$N('span', attr_dull, text);
1628
parts.push(node);
1629
}
1630
1631
rows[ii].hoverNode = JX.$N('div', attr_container, parts);
1632
} else {
1633
rows[ii].hoverNode = null;
1634
}
1635
1636
rows[ii].bright = (any_highlight && !has_highlight);
1637
}
1638
1639
return rows;
1640
},
1641
1642
_deleteInlineByID: function(id) {
1643
var uri = this.getInlineURI();
1644
var data = {
1645
op: 'refdelete',
1646
id: id
1647
};
1648
1649
var handler = JX.bind(this, this.redrawPreview);
1650
1651
new JX.Workflow(uri, data)
1652
.setHandler(handler)
1653
.start();
1654
},
1655
1656
_getInlineForEvent: function(e) {
1657
var node = e.getNode('differential-changeset');
1658
if (!node) {
1659
return null;
1660
}
1661
1662
var changeset = this.getChangesetForNode(node);
1663
1664
var inline_row = e.getNode('inline-row');
1665
return changeset.getInlineForRow(inline_row);
1666
},
1667
1668
getLineNumberFromHeader: function(node) {
1669
var n = parseInt(node.getAttribute('data-n'));
1670
1671
if (!n) {
1672
return null;
1673
}
1674
1675
// If this is a line number that's part of a row showing more context,
1676
// we don't want to let users leave inlines here.
1677
1678
try {
1679
JX.DOM.findAbove(node, 'tr', 'context-target');
1680
return null;
1681
} catch (ex) {
1682
// Ignore.
1683
}
1684
1685
return n;
1686
},
1687
1688
getDisplaySideFromHeader: function(th) {
1689
return (th.parentNode.firstChild != th) ? 'right' : 'left';
1690
},
1691
1692
_onrangedown: function(e) {
1693
// NOTE: We're allowing "mousedown" from a touch event through so users
1694
// can leave inlines on a single line.
1695
1696
// See PHI985. We want to exclude both right-mouse and middle-mouse
1697
// clicks from continuing.
1698
if (!e.isLeftButton()) {
1699
return;
1700
}
1701
1702
if (this._rangeActive) {
1703
return;
1704
}
1705
1706
var target = e.getTarget();
1707
var number = this.getLineNumberFromHeader(target);
1708
if (!number) {
1709
return;
1710
}
1711
1712
e.kill();
1713
this._rangeActive = true;
1714
1715
this._rangeOrigin = target;
1716
this._rangeTarget = target;
1717
1718
this._setHoverRange(this._rangeOrigin, this._rangeTarget);
1719
},
1720
1721
_onrangemove: function(e) {
1722
if (e.getIsTouchEvent()) {
1723
return;
1724
}
1725
1726
var is_out = (e.getType() == 'mouseout');
1727
var target = e.getTarget();
1728
1729
this._updateRange(target, is_out);
1730
},
1731
1732
_updateRange: function(target, is_out) {
1733
// Don't update the range if this target doesn't correspond to a line
1734
// number. For instance, this may be a dead line number, like the empty
1735
// line numbers on the left hand side of a newly added file.
1736
var number = this.getLineNumberFromHeader(target);
1737
if (!number) {
1738
return;
1739
}
1740
1741
if (this._rangeActive) {
1742
var origin = this._hoverOrigin;
1743
1744
// Don't update the reticle if we're selecting a line range and the
1745
// "<th />" under the cursor is on the wrong side of the file. You can
1746
// only leave inline comments on the left or right side of a file, not
1747
// across lines on both sides.
1748
var origin_side = this.getDisplaySideFromHeader(origin);
1749
var target_side = this.getDisplaySideFromHeader(target);
1750
if (origin_side != target_side) {
1751
return;
1752
}
1753
1754
// Don't update the reticle if we're selecting a line range and the
1755
// "<th />" under the cursor corresponds to a different file. You can
1756
// only leave inline comments on lines in a single file, not across
1757
// multiple files.
1758
var origin_table = JX.DOM.findAbove(origin, 'table');
1759
var target_table = JX.DOM.findAbove(target, 'table');
1760
if (origin_table != target_table) {
1761
return;
1762
}
1763
}
1764
1765
if (is_out) {
1766
if (this._rangeActive) {
1767
// If we're dragging a range, just leave the state as it is. This
1768
// allows you to drag over something invalid while selecting a
1769
// range without the range flickering or getting lost.
1770
} else {
1771
// Otherwise, clear the current range.
1772
this.resetHover();
1773
}
1774
return;
1775
}
1776
1777
if (this._rangeActive) {
1778
this._rangeTarget = target;
1779
} else {
1780
this._rangeOrigin = target;
1781
this._rangeTarget = target;
1782
}
1783
1784
this._setHoverRange(this._rangeOrigin, this._rangeTarget);
1785
},
1786
1787
_onrangeup: function(e) {
1788
if (!this._rangeActive) {
1789
return;
1790
}
1791
1792
e.kill();
1793
1794
var origin = this._rangeOrigin;
1795
var target = this._rangeTarget;
1796
1797
// If the user dragged a range from the bottom to the top, swap the node
1798
// order around.
1799
if (JX.$V(origin).y > JX.$V(target).y) {
1800
var tmp = target;
1801
target = origin;
1802
origin = tmp;
1803
}
1804
1805
var node = JX.DOM.findAbove(origin, null, 'differential-changeset');
1806
var changeset = this.getChangesetForNode(node);
1807
1808
changeset.newInlineForRange(origin, target);
1809
1810
this._rangeActive = false;
1811
this._rangeOrigin = null;
1812
this._rangeTarget = null;
1813
1814
this.resetHover();
1815
},
1816
1817
_redrawBanner: function() {
1818
// If the inline comment menu is open and we've done a redraw, close it.
1819
// In particular, this makes it close when you scroll the document:
1820
// otherwise, it stays open but the banner moves underneath it.
1821
if (this._dropdownMenu) {
1822
this._dropdownMenu.close();
1823
}
1824
1825
var node = this._getBannerNode();
1826
var changeset = this._getVisibleChangeset();
1827
var tree = this._getTreeView();
1828
var formation = this.getFormationView();
1829
1830
if (!changeset) {
1831
this._bannerChangeset = null;
1832
JX.DOM.remove(node);
1833
tree.setFocusedPath(null);
1834
1835
if (formation) {
1836
formation.repaint();
1837
}
1838
1839
return;
1840
}
1841
1842
// Don't do anything if nothing has changed. This seems to avoid some
1843
// flickering issues in Safari, at least.
1844
if (this._bannerChangeset === changeset) {
1845
return;
1846
}
1847
this._bannerChangeset = changeset;
1848
1849
var paths = tree.getPaths();
1850
for (var ii = 0; ii < paths.length; ii++) {
1851
var path = paths[ii];
1852
if (path.getChangeset() === changeset) {
1853
tree.setFocusedPath(path);
1854
}
1855
}
1856
1857
var inlines = this._getInlinesByType();
1858
1859
var unsaved = inlines.unsaved;
1860
var unsubmitted = inlines.unsubmitted;
1861
var undone = inlines.undone;
1862
var done = inlines.done;
1863
var draft_done = inlines.draftDone;
1864
1865
JX.DOM.alterClass(
1866
node,
1867
'diff-banner-has-unsaved',
1868
!!unsaved.length);
1869
1870
JX.DOM.alterClass(
1871
node,
1872
'diff-banner-has-unsubmitted',
1873
!!unsubmitted.length);
1874
1875
JX.DOM.alterClass(
1876
node,
1877
'diff-banner-has-draft-done',
1878
!!draft_done.length);
1879
1880
var pht = this.getTranslations();
1881
var unsaved_button = this._getUnsavedButton();
1882
var unsubmitted_button = this._getUnsubmittedButton();
1883
var done_button = this._getDoneButton();
1884
var menu_button = this._getMenuButton();
1885
1886
if (unsaved.length) {
1887
unsaved_button.setText(unsaved.length + ' ' + pht('Unsaved'));
1888
JX.DOM.show(unsaved_button.getNode());
1889
} else {
1890
JX.DOM.hide(unsaved_button.getNode());
1891
}
1892
1893
if (unsubmitted.length || draft_done.length) {
1894
var any_draft_count = unsubmitted.length + draft_done.length;
1895
1896
unsubmitted_button.setText(any_draft_count + ' ' + pht('Unsubmitted'));
1897
JX.DOM.show(unsubmitted_button.getNode());
1898
} else {
1899
JX.DOM.hide(unsubmitted_button.getNode());
1900
}
1901
1902
if (done.length || undone.length) {
1903
// If you haven't marked any comments as "Done", we just show text
1904
// like "3 Comments". If you've marked at least one done, we show
1905
// "1 / 3 Comments".
1906
1907
var done_text;
1908
if (done.length) {
1909
done_text = [
1910
done.length,
1911
' / ',
1912
(done.length + undone.length),
1913
' ',
1914
pht('Comments')
1915
];
1916
} else {
1917
done_text = [
1918
undone.length,
1919
' ',
1920
pht('Comments')
1921
];
1922
}
1923
1924
done_button.setText(done_text);
1925
1926
JX.DOM.show(done_button.getNode());
1927
1928
// If any comments are not marked "Done", this cycles through the
1929
// missing comments. Otherwise, it cycles through all the saved
1930
// comments.
1931
if (undone.length) {
1932
this._doneMode = 'undone';
1933
} else {
1934
this._doneMode = 'done';
1935
}
1936
1937
} else {
1938
JX.DOM.hide(done_button.getNode());
1939
}
1940
1941
var path_view = [icon, ' ', changeset.getDisplayPath()];
1942
1943
var buttons_attrs = {
1944
className: 'diff-banner-buttons'
1945
};
1946
1947
var buttons_list = [
1948
unsaved_button.getNode(),
1949
unsubmitted_button.getNode(),
1950
done_button.getNode(),
1951
menu_button.getNode()
1952
];
1953
1954
var buttons_view = JX.$N('div', buttons_attrs, buttons_list);
1955
1956
var icon = new JX.PHUIXIconView()
1957
.setIcon(changeset.getIcon())
1958
.getNode();
1959
JX.DOM.setContent(node, [buttons_view, path_view]);
1960
1961
document.body.appendChild(node);
1962
1963
if (formation) {
1964
formation.repaint();
1965
}
1966
},
1967
1968
_getInlinesByType: function() {
1969
var changesets = this._changesets;
1970
var unsaved = [];
1971
var unsubmitted = [];
1972
var undone = [];
1973
var done = [];
1974
var draft_done = [];
1975
1976
var visible_done = [];
1977
var visible_collapsed = [];
1978
var visible_ghosts = [];
1979
var visible = [];
1980
var hidden = [];
1981
1982
for (var ii = 0; ii < changesets.length; ii++) {
1983
var inlines = changesets[ii].getInlines();
1984
var inline;
1985
var jj;
1986
for (jj = 0; jj < inlines.length; jj++) {
1987
inline = inlines[jj];
1988
1989
if (inline.isDeleted()) {
1990
continue;
1991
}
1992
1993
if (inline.isSynthetic()) {
1994
continue;
1995
}
1996
1997
if (inline.isEditing()) {
1998
unsaved.push(inline);
1999
} else if (!inline.getID()) {
2000
// These are new comments which have been cancelled, and do not
2001
// count as anything.
2002
continue;
2003
} else if (inline.isDraft()) {
2004
unsubmitted.push(inline);
2005
} else {
2006
// NOTE: Unlike other states, an inline may be marked with a
2007
// draft checkmark and still be a "done" or "undone" comment.
2008
if (inline.isDraftDone()) {
2009
draft_done.push(inline);
2010
}
2011
2012
if (!inline.isDone()) {
2013
undone.push(inline);
2014
} else {
2015
done.push(inline);
2016
}
2017
}
2018
}
2019
2020
for (jj = 0; jj < inlines.length; jj++) {
2021
inline = inlines[jj];
2022
if (inline.isDeleted()) {
2023
continue;
2024
}
2025
2026
if (inline.isEditing()) {
2027
continue;
2028
}
2029
2030
if (inline.isHidden()) {
2031
hidden.push(inline);
2032
continue;
2033
}
2034
2035
visible.push(inline);
2036
2037
if (inline.isDone()) {
2038
visible_done.push(inline);
2039
}
2040
2041
if (inline.isCollapsed()) {
2042
visible_collapsed.push(inline);
2043
}
2044
2045
if (inline.isGhost()) {
2046
visible_ghosts.push(inline);
2047
}
2048
}
2049
}
2050
2051
return {
2052
unsaved: unsaved,
2053
unsubmitted: unsubmitted,
2054
undone: undone,
2055
done: done,
2056
draftDone: draft_done,
2057
visibleDone: visible_done,
2058
visibleGhosts: visible_ghosts,
2059
visibleCollapsed: visible_collapsed,
2060
visible: visible,
2061
hidden: hidden
2062
};
2063
2064
},
2065
2066
_getUnsavedButton: function() {
2067
if (!this._unsavedButton) {
2068
var button = new JX.PHUIXButtonView()
2069
.setIcon('fa-commenting-o')
2070
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
2071
2072
var node = button.getNode();
2073
2074
var onunsaved = JX.bind(this, this._onunsavedclick);
2075
JX.DOM.listen(node, 'click', null, onunsaved);
2076
2077
this._unsavedButton = button;
2078
}
2079
2080
return this._unsavedButton;
2081
},
2082
2083
_getUnsubmittedButton: function() {
2084
if (!this._unsubmittedButton) {
2085
var button = new JX.PHUIXButtonView()
2086
.setIcon('fa-comment-o')
2087
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
2088
2089
var node = button.getNode();
2090
2091
var onunsubmitted = JX.bind(this, this._onunsubmittedclick);
2092
JX.DOM.listen(node, 'click', null, onunsubmitted);
2093
2094
this._unsubmittedButton = button;
2095
}
2096
2097
return this._unsubmittedButton;
2098
},
2099
2100
_getDoneButton: function() {
2101
if (!this._doneButton) {
2102
var button = new JX.PHUIXButtonView()
2103
.setIcon('fa-comment')
2104
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
2105
2106
var node = button.getNode();
2107
2108
var ondone = JX.bind(this, this._ondoneclick);
2109
JX.DOM.listen(node, 'click', null, ondone);
2110
2111
this._doneButton = button;
2112
}
2113
2114
return this._doneButton;
2115
},
2116
2117
_getMenuButton: function() {
2118
if (!this._menuButton) {
2119
var pht = this.getTranslations();
2120
2121
var button = new JX.PHUIXButtonView()
2122
.setIcon('fa-bars')
2123
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE)
2124
.setAuralLabel(pht('Display Options'));
2125
2126
var dropdown = new JX.PHUIXDropdownMenu(button.getNode());
2127
this._menuItems = {};
2128
2129
var list = new JX.PHUIXActionListView();
2130
dropdown.setContent(list.getNode());
2131
2132
var map = {
2133
hideDone: {
2134
type: 'done'
2135
},
2136
hideCollapsed: {
2137
type: 'collapsed'
2138
},
2139
hideGhosts: {
2140
type: 'ghosts'
2141
},
2142
hideAll: {
2143
type: 'all'
2144
},
2145
showAll: {
2146
type: 'show'
2147
}
2148
};
2149
2150
for (var k in map) {
2151
var spec = map[k];
2152
2153
var handler = JX.bind(this, this._onhideinlines, spec.type);
2154
var item = new JX.PHUIXActionView()
2155
.setHandler(handler);
2156
2157
list.addItem(item);
2158
this._menuItems[k] = item;
2159
}
2160
2161
dropdown.listen('open', JX.bind(this, this._ondropdown));
2162
2163
if (this.getInlineListURI()) {
2164
list.addItem(
2165
new JX.PHUIXActionView()
2166
.setDivider(true));
2167
2168
list.addItem(
2169
new JX.PHUIXActionView()
2170
.setIcon('fa-external-link')
2171
.setName(pht('List Inline Comments'))
2172
.setHref(this.getInlineListURI()));
2173
}
2174
2175
this._menuButton = button;
2176
this._dropdownMenu = dropdown;
2177
}
2178
2179
return this._menuButton;
2180
},
2181
2182
_ondropdown: function() {
2183
var inlines = this._getInlinesByType();
2184
var items = this._menuItems;
2185
var pht = this.getTranslations();
2186
2187
items.hideDone
2188
.setName(pht('Hide "Done" Inlines'))
2189
.setDisabled(!inlines.visibleDone.length);
2190
2191
items.hideCollapsed
2192
.setName(pht('Hide Collapsed Inlines'))
2193
.setDisabled(!inlines.visibleCollapsed.length);
2194
2195
items.hideGhosts
2196
.setName(pht('Hide Older Inlines'))
2197
.setDisabled(!inlines.visibleGhosts.length);
2198
2199
items.hideAll
2200
.setName(pht('Hide All Inlines'))
2201
.setDisabled(!inlines.visible.length);
2202
2203
items.showAll
2204
.setName(pht('Show All Inlines'))
2205
.setDisabled(!inlines.hidden.length);
2206
},
2207
2208
_onhideinlines: function(type, e) {
2209
this._dropdownMenu.close();
2210
e.prevent();
2211
2212
this._toggleInlines(type);
2213
},
2214
2215
_toggleInlines: function(type) {
2216
var inlines = this._getInlinesByType();
2217
2218
// Clear the selection state since we end up in a weird place if the
2219
// user hides the selected inline.
2220
this._setSelectionState(null);
2221
2222
var targets;
2223
var mode = true;
2224
switch (type) {
2225
case 'done':
2226
targets = inlines.visibleDone;
2227
break;
2228
case 'collapsed':
2229
targets = inlines.visibleCollapsed;
2230
break;
2231
case 'ghosts':
2232
targets = inlines.visibleGhosts;
2233
break;
2234
case 'all':
2235
targets = inlines.visible;
2236
break;
2237
case 'show':
2238
targets = inlines.hidden;
2239
mode = false;
2240
break;
2241
}
2242
2243
for (var ii = 0; ii < targets.length; ii++) {
2244
targets[ii].setHidden(mode);
2245
}
2246
},
2247
2248
_onunsavedclick: function(e) {
2249
e.kill();
2250
2251
var options = {
2252
filter: 'comment',
2253
wrap: true,
2254
show: true,
2255
attribute: 'unsaved'
2256
};
2257
2258
this._onjumpkey(1, options);
2259
},
2260
2261
_onunsubmittedclick: function(e) {
2262
e.kill();
2263
2264
var options = {
2265
filter: 'comment',
2266
wrap: true,
2267
show: true,
2268
attribute: 'anyDraft'
2269
};
2270
2271
this._onjumpkey(1, options);
2272
},
2273
2274
_ondoneclick: function(e) {
2275
e.kill();
2276
2277
var options = {
2278
filter: 'comment',
2279
wrap: true,
2280
show: true,
2281
attribute: this._doneMode
2282
};
2283
2284
this._onjumpkey(1, options);
2285
},
2286
2287
_getBannerNode: function() {
2288
if (!this._bannerNode) {
2289
var attributes = {
2290
className: 'diff-banner',
2291
id: 'diff-banner'
2292
};
2293
2294
this._bannerNode = JX.$N('div', attributes);
2295
}
2296
2297
return this._bannerNode;
2298
},
2299
2300
_getVisibleChangeset: function() {
2301
if (this.isAsleep()) {
2302
return null;
2303
}
2304
2305
if (JX.Device.getDevice() != 'desktop') {
2306
return null;
2307
}
2308
2309
// Never show the banner if we're very near the top of the page.
2310
var margin = 480;
2311
var s = JX.Vector.getScroll();
2312
if (s.y < margin) {
2313
return null;
2314
}
2315
2316
// We're going to find the changeset which spans an invisible line a
2317
// little underneath the bottom of the banner. This makes the header
2318
// tick over from "A.txt" to "B.txt" just as "A.txt" scrolls completely
2319
// offscreen.
2320
var detect_height = 64;
2321
2322
for (var ii = 0; ii < this._changesets.length; ii++) {
2323
var changeset = this._changesets[ii];
2324
var c = changeset.getVectors();
2325
2326
// If the changeset starts above the line...
2327
if (c.pos.y <= (s.y + detect_height)) {
2328
// ...and ends below the line, this is the current visible changeset.
2329
if ((c.pos.y + c.dim.y) >= (s.y + detect_height)) {
2330
return changeset;
2331
}
2332
}
2333
}
2334
2335
return null;
2336
},
2337
2338
_getTreeView: function() {
2339
if (!this._treeView) {
2340
var tree = new JX.DiffTreeView();
2341
2342
for (var ii = 0; ii < this._changesets.length; ii++) {
2343
var changeset = this._changesets[ii];
2344
tree.addPath(changeset.getPathView());
2345
}
2346
2347
this._treeView = tree;
2348
}
2349
return this._treeView;
2350
},
2351
2352
_redrawFiletree : function() {
2353
var formation = this.getFormationView();
2354
2355
if (!formation) {
2356
return;
2357
}
2358
2359
var filetree = formation.getColumn(0);
2360
var flank = filetree.getFlank();
2361
2362
var flank_body = flank.getBodyNode();
2363
2364
var tree = this._getTreeView();
2365
JX.DOM.setContent(flank_body, tree.getNode());
2366
},
2367
2368
_setupInlineCommentListeners: function() {
2369
var onsave = JX.bind(this, this._onInlineEvent, 'save');
2370
JX.Stratcom.listen(
2371
['submit', 'didSyntheticSubmit'],
2372
'inline-edit-form',
2373
onsave);
2374
2375
var oncancel = JX.bind(this, this._onInlineEvent, 'cancel');
2376
JX.Stratcom.listen(
2377
'click',
2378
'inline-edit-cancel',
2379
oncancel);
2380
2381
var onundo = JX.bind(this, this._onInlineEvent, 'undo');
2382
JX.Stratcom.listen(
2383
'click',
2384
'differential-inline-comment-undo',
2385
onundo);
2386
2387
var ondone = JX.bind(this, this._onInlineEvent, 'done');
2388
JX.Stratcom.listen(
2389
'click',
2390
['differential-inline-comment', 'differential-inline-done'],
2391
ondone);
2392
2393
var ondelete = JX.bind(this, this._onInlineEvent, 'delete');
2394
JX.Stratcom.listen(
2395
'click',
2396
['differential-inline-comment', 'differential-inline-delete'],
2397
ondelete);
2398
2399
var onmenu = JX.bind(this, this._onInlineEvent, 'menu');
2400
JX.Stratcom.listen(
2401
'click',
2402
['differential-inline-comment', 'inline-action-dropdown'],
2403
onmenu);
2404
2405
var ondraft = JX.bind(this, this._onInlineEvent, 'draft');
2406
JX.Stratcom.listen(
2407
'keydown',
2408
['differential-inline-comment', 'tag:textarea'],
2409
ondraft);
2410
2411
var on_preview_view = JX.bind(this, this._onPreviewEvent, 'view');
2412
JX.Stratcom.listen(
2413
'click',
2414
'differential-inline-preview-jump',
2415
on_preview_view);
2416
},
2417
2418
_onPreviewEvent: function(action, e) {
2419
if (this.isAsleep()) {
2420
return;
2421
}
2422
2423
var data = e.getNodeData('differential-inline-preview-jump');
2424
var inline = this.getInlineByID(data.inlineCommentID);
2425
if (!inline) {
2426
return;
2427
}
2428
2429
e.kill();
2430
2431
switch (action) {
2432
case 'view':
2433
this.selectInline(inline, true, true);
2434
break;
2435
}
2436
},
2437
2438
_onInlineEvent: function(action, e) {
2439
if (this.isAsleep()) {
2440
return;
2441
}
2442
2443
if (action !== 'draft' && action !== 'menu') {
2444
e.kill();
2445
}
2446
2447
var inline = this._getInlineForEvent(e);
2448
var is_ref = false;
2449
2450
// If we don't have a natural inline object, the user may have clicked
2451
// an action (like "Delete") inside a preview element at the bottom of
2452
// the page.
2453
2454
// If they did, try to find an associated normal inline to act on, and
2455
// pretend they clicked that instead. This makes the overall state of
2456
// the page more consistent.
2457
2458
// However, there may be no normal inline (for example, because it is
2459
// on a version of the diff which is not visible). In this case, we
2460
// act by reference.
2461
2462
if (inline === null) {
2463
var data = e.getNodeData('differential-inline-comment');
2464
inline = this.getInlineByID(data.id);
2465
if (inline) {
2466
is_ref = true;
2467
} else {
2468
switch (action) {
2469
case 'delete':
2470
this._deleteInlineByID(data.id);
2471
return;
2472
}
2473
}
2474
}
2475
2476
// TODO: For normal operations, highlight the inline range here.
2477
2478
switch (action) {
2479
case 'save':
2480
inline.save();
2481
break;
2482
case 'cancel':
2483
inline.cancel();
2484
break;
2485
case 'undo':
2486
inline.undo();
2487
break;
2488
case 'done':
2489
inline.toggleDone();
2490
break;
2491
case 'delete':
2492
inline.delete(is_ref);
2493
break;
2494
case 'draft':
2495
inline.triggerDraft();
2496
break;
2497
case 'menu':
2498
var node = e.getNode('inline-action-dropdown');
2499
inline.activateMenu(node, e);
2500
break;
2501
}
2502
},
2503
2504
_onSelectRange: function(e) {
2505
this._updateSourceSelection();
2506
},
2507
2508
_updateSourceSelection: function() {
2509
var ranges = this._getSelectedRanges();
2510
2511
// In Firefox, selecting multiple rows gives us multiple ranges. In
2512
// Safari and Chrome, we get a single range.
2513
if (!ranges.length) {
2514
this._setSourceSelection(null, null);
2515
return;
2516
}
2517
2518
var min = 0;
2519
var max = ranges.length - 1;
2520
2521
var head = ranges[min].startContainer;
2522
var last = ranges[max].endContainer;
2523
2524
var head_loc = this._getFragmentLocation(head);
2525
var last_loc = this._getFragmentLocation(last);
2526
2527
if (head_loc === null || last_loc === null) {
2528
this._setSourceSelection(null, null);
2529
return;
2530
}
2531
2532
if (head_loc.changesetID !== last_loc.changesetID) {
2533
this._setSourceSelection(null, null);
2534
return;
2535
}
2536
2537
head_loc.offset += ranges[min].startOffset;
2538
last_loc.offset += ranges[max].endOffset;
2539
2540
this._setSourceSelection(head_loc, last_loc);
2541
},
2542
2543
_setSourceSelection: function(start, end) {
2544
var start_updated =
2545
!this._isSameSourceSelection(this._sourceSelectionStart, start);
2546
2547
var end_updated =
2548
!this._isSameSourceSelection(this._sourceSelectionEnd, end);
2549
2550
if (!start_updated && !end_updated) {
2551
return;
2552
}
2553
2554
this._sourceSelectionStart = start;
2555
this._sourceSelectionEnd = end;
2556
2557
if (!start) {
2558
this._closeSourceSelectionMenu();
2559
return;
2560
}
2561
2562
var menu;
2563
if (this._sourceSelectionMenu) {
2564
menu = this._sourceSelectionMenu;
2565
} else {
2566
menu = this._newSourceSelectionMenu();
2567
this._sourceSelectionMenu = menu;
2568
}
2569
2570
var pos = JX.$V(start.node)
2571
.add(0, -menu.getMenuNodeDimensions().y)
2572
.add(0, -24);
2573
2574
menu.setPosition(pos);
2575
menu.open();
2576
},
2577
2578
_newSourceSelectionMenu: function() {
2579
var pht = this.getTranslations();
2580
2581
var menu = new JX.PHUIXDropdownMenu(null)
2582
.setWidth(240);
2583
2584
// We need to disable autofocus for this menu, since it operates on the
2585
// text selection in the document. If we leave this enabled, opening the
2586
// menu immediately discards the selection.
2587
menu.setDisableAutofocus(true);
2588
2589
var list = new JX.PHUIXActionListView();
2590
menu.setContent(list.getNode());
2591
2592
var oncreate = JX.bind(this, this._onSourceSelectionMenuAction, 'create');
2593
2594
var comment_item = new JX.PHUIXActionView()
2595
.setIcon('fa-comment-o')
2596
.setName(pht('New Inline Comment'))
2597
.setKeyCommand('c')
2598
.setHandler(oncreate);
2599
2600
list.addItem(comment_item);
2601
2602
return menu;
2603
},
2604
2605
_onSourceSelectionMenuAction: function(action, e) {
2606
e.kill();
2607
this._closeSourceSelectionMenu();
2608
2609
switch (action) {
2610
case 'create':
2611
this._onKeyCreate();
2612
break;
2613
}
2614
},
2615
2616
_closeSourceSelectionMenu: function() {
2617
if (this._sourceSelectionMenu) {
2618
this._sourceSelectionMenu.close();
2619
}
2620
},
2621
2622
_isSameSourceSelection: function(u, v) {
2623
if (u === null && v === null) {
2624
return true;
2625
}
2626
2627
if (u === null && v !== null) {
2628
return false;
2629
}
2630
2631
if (u !== null && v === null) {
2632
return false;
2633
}
2634
2635
return (
2636
(u.changesetID === v.changesetID) &&
2637
(u.line === v.line) &&
2638
(u.displayColumn === v.displayColumn) &&
2639
(u.offset === v.offset)
2640
);
2641
},
2642
2643
_getFragmentLocation: function(fragment) {
2644
// Find the changeset containing the fragment.
2645
var changeset = null;
2646
try {
2647
var node = JX.DOM.findAbove(
2648
fragment,
2649
'div',
2650
'differential-changeset');
2651
2652
changeset = this.getChangesetForNode(node);
2653
if (!changeset) {
2654
return null;
2655
}
2656
} catch (ex) {
2657
return null;
2658
}
2659
2660
// Find the line number and display column for the fragment.
2661
var line = null;
2662
var column_count = -1;
2663
var has_new = false;
2664
var has_old = false;
2665
var offset = null;
2666
var target_node = null;
2667
var td;
2668
try {
2669
2670
// NOTE: In Safari, you can carefully select an entire line and then
2671
// move your mouse down slightly, causing selection of an empty
2672
// document fragment which is an immediate child of the next "<tr />".
2673
2674
// If the fragment is a direct child of a "<tr />" parent, assume the
2675
// user has done this and select the last child of the previous row
2676
// instead. It's possible there are other ways to do this, so this may
2677
// not always be the right rule.
2678
2679
// Otherwise, select the containing "<td />".
2680
2681
var is_end;
2682
if (JX.DOM.isType(fragment.parentNode, 'tr')) {
2683
// Assume this is Safari, and that the user has carefully selected a
2684
// row and then moved their mouse down a few pixels to select the
2685
// invisible fragment at the beginning of the next row.
2686
var cells = fragment.parentNode.previousSibling.childNodes;
2687
td = cells[cells.length - 1];
2688
is_end = true;
2689
} else {
2690
td = this._findContentCell(fragment);
2691
is_end = false;
2692
}
2693
2694
var cursor = td;
2695
while (cursor) {
2696
if (cursor.getAttribute('data-copy-mode')) {
2697
column_count++;
2698
} else {
2699
// In unified mode, the content column isn't currently marked
2700
// with an attribute, and we can't count content columns anyway.
2701
// Keep track of whether or not we see a "NL" (New Line) column
2702
// and/or an "OL" (Old Line) column to try to puzzle out which
2703
// side of the display change we're on.
2704
2705
if (cursor.id.match(/NL/)) {
2706
has_new = true;
2707
} else if (cursor.id.match(/OL/)) {
2708
has_old = true;
2709
}
2710
}
2711
2712
var n = parseInt(cursor.getAttribute('data-n'));
2713
2714
if (n) {
2715
if (line === null) {
2716
target_node = cursor;
2717
line = n;
2718
}
2719
}
2720
2721
cursor = cursor.previousSibling;
2722
}
2723
2724
if (!line) {
2725
return null;
2726
}
2727
2728
if (column_count < 0) {
2729
if (has_new || has_old) {
2730
if (has_new) {
2731
column_count = 1;
2732
} else {
2733
column_count = 0;
2734
}
2735
} else {
2736
return null;
2737
}
2738
}
2739
2740
var info = this._getSelectionOffset(td, fragment);
2741
2742
if (info.found) {
2743
offset = info.offset;
2744
} else {
2745
if (is_end) {
2746
offset = info.offset;
2747
} else {
2748
offset = 0;
2749
}
2750
}
2751
} catch (ex) {
2752
return null;
2753
}
2754
2755
var changeset_id;
2756
if (column_count > 0) {
2757
changeset_id = changeset.getRightChangesetID();
2758
} else {
2759
changeset_id = changeset.getLeftChangesetID();
2760
}
2761
2762
return {
2763
node: td,
2764
changeset: changeset,
2765
changesetID: changeset_id,
2766
line: line,
2767
displayColumn: column_count,
2768
offset: offset,
2769
targetNode: target_node
2770
};
2771
},
2772
2773
_getSelectionOffset: function(node, target) {
2774
// If this is an aural hint node in a unified diff, ignore it when
2775
// calculating the selection offset.
2776
if (node.getAttribute && node.getAttribute('data-aural')) {
2777
return {
2778
offset: 0,
2779
content: '',
2780
found: false
2781
};
2782
}
2783
2784
if (!node.childNodes || !node.childNodes.length) {
2785
return {
2786
offset: node.textContent.length,
2787
content: node.textContent,
2788
found: false
2789
};
2790
}
2791
2792
var found = false;
2793
var offset = 0;
2794
var content = '';
2795
for (var ii = 0; ii < node.childNodes.length; ii++) {
2796
var child = node.childNodes[ii];
2797
2798
if (child === target) {
2799
found = true;
2800
}
2801
2802
var spec = this._getSelectionOffset(child, target);
2803
2804
content += spec.content;
2805
if (!found) {
2806
offset += spec.offset;
2807
}
2808
2809
found = found || spec.found;
2810
}
2811
2812
return {
2813
offset: offset,
2814
content: content,
2815
found: found
2816
};
2817
},
2818
2819
_getSelectedRanges: function() {
2820
var ranges = [];
2821
2822
if (!window.getSelection) {
2823
return ranges;
2824
}
2825
2826
var selection = window.getSelection();
2827
for (var ii = 0; ii < selection.rangeCount; ii++) {
2828
var range = selection.getRangeAt(ii);
2829
if (range.collapsed) {
2830
continue;
2831
}
2832
2833
ranges.push(range);
2834
}
2835
2836
return ranges;
2837
},
2838
2839
_isContentCell: function(node) {
2840
return !!node.getAttribute('data-copy-mode');
2841
},
2842
2843
_findContentCell: function(node) {
2844
var cursor = node;
2845
while (true) {
2846
cursor = JX.DOM.findAbove(cursor, 'td');
2847
if (this._isContentCell(cursor)) {
2848
return cursor;
2849
}
2850
}
2851
}
2852
2853
}
2854
2855
});
2856
2857