@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1/**
2 * @requires javelin-dom
3 * javelin-util
4 * javelin-stratcom
5 * javelin-install
6 * javelin-workflow
7 * javelin-router
8 * javelin-behavior-device
9 * javelin-vector
10 * phabricator-diff-inline
11 * phabricator-diff-path-view
12 * phuix-button-view
13 * javelin-magical-init
14 * @provides phabricator-diff-changeset
15 *
16 * @javelin-installs JX.DiffChangeset
17 *
18 * @javelin
19 */
20
21JX.install('DiffChangeset', {
22
23 construct : function(node) {
24 this._node = node;
25
26 var data = this._getNodeData();
27
28 this._renderURI = data.renderURI;
29 this._ref = data.ref;
30 this._loaded = data.loaded;
31 this._treeNodeID = data.treeNodeID;
32
33 this._leftID = data.left;
34 this._rightID = data.right;
35
36 this._displayPath = JX.$H(data.displayPath);
37 this._pathParts = data.pathParts;
38 this._icon = data.icon;
39
40 this._editorURITemplate = data.editorURITemplate;
41 this._editorConfigureURI = data.editorConfigureURI;
42 this._showPathURI = data.showPathURI;
43 this._showDirectoryURI = data.showDirectoryURI;
44
45 this._pathIconIcon = data.pathIconIcon;
46 this._pathIconColor = data.pathIconColor;
47 this._isLowImportance = data.isLowImportance;
48 this._isOwned = data.isOwned;
49 this._isLoading = true;
50
51 this._inlines = null;
52
53 if (data.changesetState) {
54 this._loadChangesetState(data.changesetState);
55 }
56
57 JX.enableDispatch(window, 'selectstart');
58
59 var onselect = JX.bind(this, this._onClickHeader);
60 JX.DOM.listen(
61 this._node,
62 ['mousedown', 'selectstart'],
63 'changeset-header',
64 onselect);
65 },
66
67 members: {
68 _node: null,
69 _loaded: false,
70 _sequence: 0,
71 _stabilize: false,
72
73 _renderURI: null,
74 _ref: null,
75 _rendererKey: null,
76 _highlight: null,
77 _requestDocumentEngineKey: null,
78 _responseDocumentEngineKey: null,
79 _availableDocumentEngineKeys: null,
80 _characterEncoding: null,
81 _undoTemplates: null,
82
83 _leftID: null,
84 _rightID: null,
85
86 _inlines: null,
87 _visible: true,
88
89 _displayPath: null,
90
91 _changesetList: null,
92 _icon: null,
93
94 _editorURITemplate: null,
95 _editorConfigureURI: null,
96 _showPathURI: null,
97 _showDirectoryURI: null,
98
99 _pathView: null,
100
101 _pathIconIcon: null,
102 _pathIconColor: null,
103 _isLowImportance: null,
104 _isOwned: null,
105 _isHidden: null,
106 _isSelected: false,
107 _viewMenu: null,
108
109 getEditorURITemplate: function() {
110 return this._editorURITemplate;
111 },
112
113 getEditorConfigureURI: function() {
114 return this._editorConfigureURI;
115 },
116
117 getShowPathURI: function() {
118 return this._showPathURI;
119 },
120
121 getShowDirectoryURI: function() {
122 return this._showDirectoryURI;
123 },
124
125 getLeftChangesetID: function() {
126 return this._leftID;
127 },
128
129 getRightChangesetID: function() {
130 return this._rightID;
131 },
132
133 setChangesetList: function(list) {
134 this._changesetList = list;
135 return this;
136 },
137
138 setViewMenu: function(menu) {
139 this._viewMenu = menu;
140 return this;
141 },
142
143 getIcon: function() {
144 if (!this._visible) {
145 return 'fa-file-o';
146 }
147
148 return this._icon;
149 },
150
151 getColor: function() {
152 if (!this._visible) {
153 return 'grey';
154 }
155
156 return 'blue';
157 },
158
159 getChangesetList: function() {
160 return this._changesetList;
161 },
162
163 /**
164 * Has the content of this changeset been loaded?
165 *
166 * This method returns `true` if a request has been fired, even if the
167 * response has not returned yet.
168 *
169 * @return bool True if the content has been loaded.
170 */
171 isLoaded: function() {
172 return this._loaded;
173 },
174
175
176 /**
177 * Configure stabilization of the document position on content load.
178 *
179 * When we dump the changeset into the document, we can try to stabilize
180 * the document scroll position so that the user doesn't feel like they
181 * are jumping around as things load in. This is generally useful when
182 * populating initial changes.
183 *
184 * However, if a user explicitly requests a content load by clicking a
185 * "Load" link or using the dropdown menu, this stabilization generally
186 * feels unnatural, so we don't use it in response to explicit user action.
187 *
188 * @param bool True to stabilize the next content fill.
189 * @return this
190 */
191 setStabilize: function(stabilize) {
192 this._stabilize = stabilize;
193 return this;
194 },
195
196
197 /**
198 * Should this changeset load immediately when the page loads?
199 *
200 * Normally, changes load immediately, but if a diff or commit is very
201 * large we stop doing this and have the user load files explicitly, or
202 * choose to load everything.
203 *
204 * @return bool True if the changeset should load automatically when the
205 * page loads.
206 */
207 shouldAutoload: function() {
208 return this._getNodeData().autoload;
209 },
210
211
212 /**
213 * Load this changeset, if it isn't already loading.
214 *
215 * This fires a request to fill the content of this changeset, provided
216 * there isn't already a request in flight. To force a reload, use
217 * @{method:reload}.
218 *
219 * @return this
220 */
221 load: function() {
222 if (this._loaded) {
223 return this;
224 }
225
226 return this.reload();
227 },
228
229
230 /**
231 * Reload the changeset content.
232 *
233 * This method always issues a request, even if the content is already
234 * loading. To load conditionally, use @{method:load}.
235 *
236 * @return this
237 */
238 reload: function(state) {
239 this._loaded = true;
240 this._sequence++;
241
242 var workflow = this._newReloadWorkflow(state)
243 .setHandler(JX.bind(this, this._onresponse, this._sequence));
244
245 this._startContentWorkflow(workflow);
246
247 var pht = this.getChangesetList().getTranslations();
248
249 JX.DOM.setContent(
250 this._getContentFrame(),
251 JX.$N(
252 'div',
253 {className: 'differential-loading'},
254 pht('Loading...')));
255
256 return this;
257 },
258
259 _newReloadWorkflow: function(state) {
260 var params = this._getViewParameters(state);
261 return new JX.Workflow(this._renderURI, params);
262 },
263
264 /**
265 * Load missing context in a changeset.
266 *
267 * We do this when the user clicks "Show X Lines". We also expand all of
268 * the missing context when they "Show All Context".
269 *
270 * @param string Line range specification, like "0-40/0-20".
271 * @param node Row where the context should be rendered after loading.
272 * @param bool True if this is a bulk load of multiple context blocks.
273 * @return this
274 */
275 loadContext: function(range, target, bulk) {
276 var params = this._getViewParameters();
277 params.range = range;
278
279 var pht = this.getChangesetList().getTranslations();
280
281 var container = JX.DOM.scry(target, 'td')[0];
282 JX.DOM.setContent(container, pht('Loading...'));
283 JX.DOM.alterClass(target, 'differential-show-more-loading', true);
284
285 var workflow = new JX.Workflow(this._renderURI, params)
286 .setHandler(JX.bind(this, this._oncontext, target));
287
288 if (bulk) {
289 // If we're loading a bunch of these because the viewer clicked
290 // "Show All Context" or similar, use lower-priority requests
291 // and draw a progress bar.
292 this._startContentWorkflow(workflow);
293 } else {
294 // If this is a single click on a context link, use a higher priority
295 // load without a chrome change.
296 workflow.start();
297 }
298
299 return this;
300 },
301
302 loadAllContext: function() {
303 var nodes = JX.DOM.scry(this._node, 'tr', 'context-target');
304 for (var ii = 0; ii < nodes.length; ii++) {
305 var show = JX.DOM.scry(nodes[ii], 'a', 'show-more');
306 for (var jj = 0; jj < show.length; jj++) {
307 var data = JX.Stratcom.getData(show[jj]);
308 if (data.type != 'all') {
309 continue;
310 }
311 this.loadContext(data.range, nodes[ii], true);
312 }
313 }
314 },
315
316 _startContentWorkflow: function(workflow) {
317 var routable = workflow.getRoutable();
318
319 routable
320 .setPriority(500)
321 .setType('content')
322 .setKey(this._getRoutableKey());
323
324 JX.Router.getInstance().queue(routable);
325 },
326
327 getDisplayPath: function() {
328 return this._displayPath;
329 },
330
331 /**
332 * Receive a response to a context request.
333 */
334 _oncontext: function(target, response) {
335 // TODO: This should be better structured.
336 // If the response comes back with several top-level nodes, the last one
337 // is the actual context; the others are headers. Add any headers first,
338 // then copy the new rows into the document.
339 var markup = JX.$H(response.changeset).getFragment();
340 var len = markup.childNodes.length;
341 var diff = JX.DOM.findAbove(target, 'table', 'differential-diff');
342
343 for (var ii = 0; ii < len - 1; ii++) {
344 diff.parentNode.insertBefore(markup.firstChild, diff);
345 }
346
347 var table = markup.firstChild;
348 var root = target.parentNode;
349 this._moveRows(table, root, target);
350 root.removeChild(target);
351
352 this._onchangesetresponse(response);
353 },
354
355 _moveRows: function(src, dst, before) {
356 var rows = JX.DOM.scry(src, 'tr');
357 for (var ii = 0; ii < rows.length; ii++) {
358
359 // Find the table this <tr /> belongs to. If it's a sub-table, like a
360 // table in an inline comment, don't copy it.
361 if (JX.DOM.findAbove(rows[ii], 'table') !== src) {
362 continue;
363 }
364
365 if (before) {
366 dst.insertBefore(rows[ii], before);
367 } else {
368 dst.appendChild(rows[ii]);
369 }
370 }
371 },
372
373 /**
374 * Get parameters which define the current rendering options.
375 */
376 _getViewParameters: function(state) {
377 var parameters = {
378 ref: this._ref,
379 device: this._getDefaultDeviceRenderer()
380 };
381
382 if (state) {
383 JX.copy(parameters, state);
384 }
385
386 return parameters;
387 },
388
389 /**
390 * Get the active @{class:JX.Routable} for this changeset.
391 *
392 * After issuing a request with @{method:load} or @{method:reload}, you
393 * can adjust routable settings (like priority) by querying the routable
394 * with this method. Note that there may not be a current routable.
395 *
396 * @return JX.Routable|null Active routable, if one exists.
397 */
398 getRoutable: function() {
399 return JX.Router.getInstance().getRoutableByKey(this._getRoutableKey());
400 },
401
402 getRendererKey: function() {
403 return this._rendererKey;
404 },
405
406 _getDefaultDeviceRenderer: function() {
407 // NOTE: If you load the page at one device resolution and then resize to
408 // a different one we don't re-render the diffs, because it's a
409 // complicated mess and you could lose inline comments, cursor positions,
410 // etc.
411 return (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
412 },
413
414 getUndoTemplates: function() {
415 return this._undoTemplates;
416 },
417
418 getCharacterEncoding: function() {
419 return this._characterEncoding;
420 },
421
422 getHighlight: function() {
423 return this._highlight;
424 },
425
426 getRequestDocumentEngineKey: function() {
427 return this._requestDocumentEngineKey;
428 },
429
430 getResponseDocumentEngineKey: function() {
431 return this._responseDocumentEngineKey;
432 },
433
434 getAvailableDocumentEngineKeys: function() {
435 return this._availableDocumentEngineKeys;
436 },
437
438 getSelectableItems: function() {
439 var items = [];
440
441 items.push({
442 type: 'file',
443 changeset: this,
444 target: this,
445 nodes: {
446 begin: this._node,
447 end: null
448 }
449 });
450
451 if (!this._visible) {
452 return items;
453 }
454
455 var rows = JX.DOM.scry(this._node, 'tr');
456
457 var blocks = [];
458 var block;
459 var ii;
460 var parent_node = null;
461 for (ii = 0; ii < rows.length; ii++) {
462 var type = this._getRowType(rows[ii]);
463
464 // This row might be part of a diff inside an inline comment, showing
465 // an inline edit suggestion. Before we accept it as a possible target
466 // for selection, make sure it's a child of the right parent.
467
468 if (parent_node === null) {
469 parent_node = rows[ii].parentNode;
470 }
471
472 if (type !== null) {
473 if (rows[ii].parentNode !== parent_node) {
474 type = null;
475 }
476 }
477
478 if (!block || (block.type !== type)) {
479 block = {
480 type: type,
481 items: []
482 };
483 blocks.push(block);
484 }
485
486 block.items.push(rows[ii]);
487 }
488
489 var last_inline = null;
490 var last_inline_item = null;
491 for (ii = 0; ii < blocks.length; ii++) {
492 block = blocks[ii];
493
494 if (block.type == 'change') {
495 items.push({
496 type: block.type,
497 changeset: this,
498 target: block.items[0],
499 nodes: {
500 begin: block.items[0],
501 end: block.items[block.items.length - 1]
502 }
503 });
504 }
505
506 if (block.type == 'comment') {
507 for (var jj = 0; jj < block.items.length; jj++) {
508 var inline = this.getInlineForRow(block.items[jj]);
509
510 // When comments are being edited, they have a hidden row with
511 // the actual comment and then a visible row with the editor.
512
513 // In this case, we only want to generate one item, but it should
514 // use the editor as a scroll target. To accomplish this, check if
515 // this row has the same inline as the previous row. If so, update
516 // the last item to use this row's nodes.
517
518 if (inline === last_inline) {
519 last_inline_item.nodes.begin = block.items[jj];
520 last_inline_item.nodes.end = block.items[jj];
521 continue;
522 } else {
523 last_inline = inline;
524 }
525
526 var is_saved = (!inline.isDraft() && !inline.isEditing());
527
528 last_inline_item = {
529 type: block.type,
530 changeset: this,
531 target: inline,
532 hidden: inline.isHidden(),
533 collapsed: inline.isCollapsed(),
534 deleted: !inline.getID() && !inline.isEditing(),
535 nodes: {
536 begin: block.items[jj],
537 end: block.items[jj]
538 },
539 attributes: {
540 unsaved: inline.isEditing(),
541 anyDraft: inline.isDraft() || inline.isDraftDone(),
542 undone: (is_saved && !inline.isDone()),
543 done: (is_saved && inline.isDone())
544 }
545 };
546
547 items.push(last_inline_item);
548 }
549 }
550 }
551
552 return items;
553 },
554
555 _getRowType: function(row) {
556 // NOTE: Don't do "className.indexOf()" elsewhere. This is evil legacy
557 // magic.
558
559 if (row.className.indexOf('inline') !== -1) {
560 return 'comment';
561 }
562
563 var cells = JX.DOM.scry(row, 'td');
564 for (var ii = 0; ii < cells.length; ii++) {
565 if (cells[ii].className.indexOf('old') !== -1 ||
566 cells[ii].className.indexOf('new') !== -1) {
567 return 'change';
568 }
569 }
570 },
571
572 _getNodeData: function() {
573 return JX.Stratcom.getData(this._node);
574 },
575
576 getVectors: function() {
577 return {
578 pos: JX.$V(this._node),
579 dim: JX.Vector.getDim(this._node)
580 };
581 },
582
583 _onresponse: function(sequence, response) {
584 if (sequence != this._sequence) {
585 // If this isn't the most recent request, ignore it. This normally
586 // means the user changed view settings between the time the page loaded
587 // and the content filled.
588 return;
589 }
590
591 // As we populate the changeset list, we try to hold the document scroll
592 // position steady, so that, e.g., users who want to leave a comment on a
593 // diff with a large number of changes don't constantly have the text
594 // area scrolled off the bottom of the screen until the entire diff loads.
595 //
596 // There are several major cases here:
597 //
598 // - If we're near the top of the document, never scroll.
599 // - If we're near the bottom of the document, always scroll, unless
600 // we have an anchor.
601 // - Otherwise, scroll if the changes were above (or, at least,
602 // almost entirely above) the viewport.
603 //
604 // We don't scroll if the changes were just near the top of the viewport
605 // because this makes us scroll incorrectly when an anchored change is
606 // visible. See T12779.
607
608 var target = this._node;
609
610 var old_pos = JX.Vector.getScroll();
611 var old_view = JX.Vector.getViewport();
612 var old_dim = JX.Vector.getDocument();
613
614 // Number of pixels away from the top or bottom of the document which
615 // count as "nearby".
616 var sticky = 480;
617
618 var near_top = (old_pos.y <= sticky);
619 var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
620
621 // If we have an anchor in the URL, never stick to the bottom of the
622 // page. See T11784 for discussion.
623 if (window.location.hash) {
624 near_bot = false;
625 }
626
627 var target_pos = JX.Vector.getPos(target);
628 var target_dim = JX.Vector.getDim(target);
629 var target_bot = (target_pos.y + target_dim.y);
630
631 // Detect if the changeset is entirely (or, at least, almost entirely)
632 // above us. The height here is roughly the height of the persistent
633 // banner.
634 var above_screen = (target_bot < old_pos.y + 64);
635
636 // If we have a URL anchor and are currently nearby, stick to it
637 // no matter what.
638 var on_target = null;
639 if (window.location.hash) {
640 try {
641 var anchor = JX.$(window.location.hash.replace('#', ''));
642 if (anchor) {
643 var anchor_pos = JX.$V(anchor);
644 if ((anchor_pos.y > old_pos.y) &&
645 (anchor_pos.y < old_pos.y + 96)) {
646 on_target = anchor;
647 }
648 }
649 } catch (ignored) {
650 // If we have a bogus anchor, just ignore it.
651 }
652 }
653
654 var frame = this._getContentFrame();
655 JX.DOM.setContent(frame, JX.$H(response.changeset));
656
657 if (this._stabilize) {
658 if (on_target) {
659 JX.DOM.scrollToPosition(old_pos.x, JX.$V(on_target).y - 60);
660 } else if (!near_top) {
661 if (near_bot || above_screen) {
662 // Figure out how much taller the document got.
663 var delta = (JX.Vector.getDocument().y - old_dim.y);
664 JX.DOM.scrollToPosition(old_pos.x, old_pos.y + delta);
665 }
666 }
667 this._stabilize = false;
668 }
669
670 this._onchangesetresponse(response);
671 },
672
673 _onchangesetresponse: function(response) {
674 // Code shared by autoload and context responses.
675
676 this._loadChangesetState(response);
677 this._rebuildAllInlines();
678
679 JX.Stratcom.invoke('resize');
680 },
681
682 _loadChangesetState: function(state) {
683 if (state.coverage) {
684 for (var k in state.coverage) {
685 try {
686 JX.DOM.replace(JX.$(k), JX.$H(state.coverage[k]));
687 } catch (ignored) {
688 // Not terribly important.
689 }
690 }
691 }
692
693 if (state.undoTemplates) {
694 this._undoTemplates = state.undoTemplates;
695 }
696
697 this._rendererKey = state.rendererKey;
698 this._highlight = state.highlight;
699 this._characterEncoding = state.characterEncoding;
700 this._requestDocumentEngineKey = state.requestDocumentEngineKey;
701 this._responseDocumentEngineKey = state.responseDocumentEngineKey;
702 this._availableDocumentEngineKeys = state.availableDocumentEngineKeys;
703 this._isHidden = state.isHidden;
704
705 var is_hidden = !this.isVisible();
706 if (this._isHidden != is_hidden) {
707 this.setVisible(!this._isHidden);
708 }
709
710 this._isLoading = false;
711 this.getPathView().setIsLoading(this._isLoading);
712 },
713
714 _getContentFrame: function() {
715 return JX.DOM.find(this._node, 'div', 'changeset-view-content');
716 },
717
718 _getRoutableKey: function() {
719 return 'changeset-view.' + this._ref + '.' + this._sequence;
720 },
721
722 getInlineForRow: function(node) {
723 var data = JX.Stratcom.getData(node);
724
725 if (!data.inline) {
726 var inline = this._newInlineForRow(node);
727 this.getInlines().push(inline);
728 }
729
730 return data.inline;
731 },
732
733 _newInlineForRow: function(node) {
734 return new JX.DiffInline()
735 .setChangeset(this)
736 .bindToRow(node);
737 },
738
739 newInlineForRange: function(origin, target, options) {
740 var list = this.getChangesetList();
741
742 var src = list.getLineNumberFromHeader(origin);
743 var dst = list.getLineNumberFromHeader(target);
744
745 var changeset_id = null;
746 var side = list.getDisplaySideFromHeader(origin);
747 if (side == 'right') {
748 changeset_id = this.getRightChangesetID();
749 } else {
750 changeset_id = this.getLeftChangesetID();
751 }
752
753 var is_new = false;
754 if (side == 'right') {
755 is_new = true;
756 } else if (this.getRightChangesetID() != this.getLeftChangesetID()) {
757 is_new = true;
758 }
759
760 var data = {
761 origin: origin,
762 target: target,
763 number: src,
764 length: dst - src,
765 changesetID: changeset_id,
766 displaySide: side,
767 isNewFile: is_new
768 };
769
770 JX.copy(data, options || {});
771
772 var inline = new JX.DiffInline()
773 .setChangeset(this)
774 .bindToRange(data);
775
776 this.getInlines().push(inline);
777
778 inline.create();
779
780 return inline;
781 },
782
783 newInlineReply: function(original, state) {
784 var inline = new JX.DiffInline()
785 .setChangeset(this)
786 .bindToReply(original);
787
788 this._inlines.push(inline);
789
790 inline.create(state);
791
792 return inline;
793 },
794
795 getInlineByID: function(id) {
796 return this._queryInline('id', id);
797 },
798
799 getInlineByPHID: function(phid) {
800 return this._queryInline('phid', phid);
801 },
802
803 _queryInline: function(field, value) {
804 // First, look for the inline in the objects we've already built.
805 var inline = this._findInline(field, value);
806 if (inline) {
807 return inline;
808 }
809
810 // If we haven't found a matching inline yet, rebuild all the inlines
811 // present in the document, then look again.
812 this._rebuildAllInlines();
813 return this._findInline(field, value);
814 },
815
816 _findInline: function(field, value) {
817 var inlines = this.getInlines();
818
819 for (var ii = 0; ii < inlines.length; ii++) {
820 var inline = inlines[ii];
821
822 var target;
823 switch (field) {
824 case 'id':
825 target = inline.getID();
826 break;
827 case 'phid':
828 target = inline.getPHID();
829 break;
830 }
831
832 if (target == value) {
833 return inline;
834 }
835 }
836
837 return null;
838 },
839
840 getInlines: function() {
841 if (this._inlines === null) {
842 this._rebuildAllInlines();
843 }
844
845 return this._inlines;
846 },
847
848 _rebuildAllInlines: function() {
849 this._inlines = [];
850
851 var rows = JX.DOM.scry(this._node, 'tr');
852 var ii;
853 for (ii = 0; ii < rows.length; ii++) {
854 var row = rows[ii];
855 if (this._getRowType(row) != 'comment') {
856 continue;
857 }
858
859 this._inlines.push(this._newInlineForRow(row));
860 }
861 },
862
863 redrawFileTree: function() {
864 var inlines = this.getInlines();
865 var done = [];
866 var undone = [];
867 var inline;
868
869 for (var ii = 0; ii < inlines.length; ii++) {
870 inline = inlines[ii];
871
872 if (inline.isDeleted()) {
873 continue;
874 }
875
876 if (inline.isUndo()) {
877 continue;
878 }
879
880 if (inline.isSynthetic()) {
881 continue;
882 }
883
884 if (inline.isEditing()) {
885 continue;
886 }
887
888 if (!inline.getID()) {
889 // These are new comments which have been cancelled, and do not
890 // count as anything.
891 continue;
892 }
893
894 if (inline.isDraft()) {
895 continue;
896 }
897
898 if (!inline.isDone()) {
899 undone.push(inline);
900 } else {
901 done.push(inline);
902 }
903 }
904
905 var total = done.length + undone.length;
906
907 var hint;
908 var is_visible;
909 var is_completed;
910 if (total) {
911 if (done.length) {
912 hint = [done.length, '/', total];
913 } else {
914 hint = total;
915 }
916 is_visible = true;
917 is_completed = (done.length == total);
918 } else {
919 hint = '-';
920 is_visible = false;
921 is_completed = false;
922 }
923
924 var node = this.getPathView().getInlineNode();
925
926 JX.DOM.setContent(node, hint);
927
928 JX.DOM.alterClass(node, 'diff-tree-path-inlines-visible', is_visible);
929 JX.DOM.alterClass(node, 'diff-tree-path-inlines-completed', is_completed);
930 },
931
932 _onClickHeader: function(e) {
933 // If the user clicks the actual path name text, don't count this as
934 // a selection action: we want to let them select the path.
935 var path_name = e.getNode('changeset-header-path-name');
936 if (path_name) {
937 return;
938 }
939
940 // Don't allow repeatedly clicking a header to begin a "select word" or
941 // "select line" operation.
942 if (e.getType() === 'selectstart') {
943 e.kill();
944 return;
945 }
946
947 // NOTE: Don't prevent or kill the event. If the user has text selected,
948 // clicking a header should clear the selection (and dismiss any inline
949 // context menu, if one exists) as clicking elsewhere in the document
950 // normally would.
951
952 if (this._isSelected) {
953 this.getChangesetList().selectChangeset(null);
954 } else {
955 this.select(false);
956 }
957 },
958
959 toggleVisibility: function() {
960 this.setVisible(!this._visible);
961
962 var attrs = {
963 hidden: this.isVisible() ? 0 : 1,
964 discard: 1
965 };
966
967 var workflow = this._newReloadWorkflow(attrs)
968 .setHandler(JX.bag);
969
970 this._startContentWorkflow(workflow);
971 },
972
973 setVisible: function(visible) {
974 this._visible = visible;
975
976 var diff = this._getDiffNode();
977 var options = this._getViewButtonNode();
978 var show = this._getShowButtonNode();
979
980 if (this._visible) {
981 JX.DOM.show(diff);
982 JX.DOM.show(options);
983 JX.DOM.hide(show);
984 } else {
985 JX.DOM.hide(diff);
986 JX.DOM.hide(options);
987 JX.DOM.show(show);
988
989 if (this._viewMenu) {
990 this._viewMenu.close();
991 }
992 }
993
994 JX.Stratcom.invoke('resize');
995
996 var node = this._node;
997 JX.DOM.alterClass(node, 'changeset-content-hidden', !this._visible);
998
999 this.getPathView().setIsHidden(!this._visible);
1000 },
1001
1002 setIsSelected: function(is_selected) {
1003 this._isSelected = !!is_selected;
1004
1005 var node = this._node;
1006 JX.DOM.alterClass(node, 'changeset-selected', this._isSelected);
1007
1008 return this;
1009 },
1010
1011 _getDiffNode: function() {
1012 if (!this._diffNode) {
1013 this._diffNode = JX.DOM.find(this._node, 'table', 'differential-diff');
1014 }
1015 return this._diffNode;
1016 },
1017
1018 _getViewButtonNode: function() {
1019 if (!this._viewButtonNode) {
1020 this._viewButtonNode = JX.DOM.find(
1021 this._node,
1022 'a',
1023 'differential-view-options');
1024 }
1025 return this._viewButtonNode;
1026 },
1027
1028 _getShowButtonNode: function() {
1029 if (!this._showButtonNode) {
1030 var pht = this.getChangesetList().getTranslations();
1031
1032 var show_button = new JX.PHUIXButtonView()
1033 .setIcon('fa-angle-double-down')
1034 .setText(pht('Show Changeset'))
1035 .setColor('grey');
1036
1037 var button_node = show_button.getNode();
1038 this._getViewButtonNode().parentNode.appendChild(button_node);
1039
1040 var onshow = JX.bind(this, this._onClickShowButton);
1041 JX.DOM.listen(button_node, 'click', null, onshow);
1042
1043 this._showButtonNode = button_node;
1044 }
1045 return this._showButtonNode;
1046 },
1047
1048 _onClickShowButton: function(e) {
1049 e.prevent();
1050
1051 // We're always showing the changeset, but want to make sure the state
1052 // change is persisted on the server.
1053 this.toggleVisibility();
1054 },
1055
1056 isVisible: function() {
1057 return this._visible;
1058 },
1059
1060 getPathView: function() {
1061 if (!this._pathView) {
1062 var view = new JX.DiffPathView()
1063 .setChangeset(this)
1064 .setPath(this._pathParts)
1065 .setIsLowImportance(this._isLowImportance)
1066 .setIsOwned(this._isOwned)
1067 .setIsLoading(this._isLoading);
1068
1069 view.getIcon()
1070 .setIcon(this._pathIconIcon)
1071 .setColor(this._pathIconColor);
1072
1073 this._pathView = view;
1074 }
1075
1076 return this._pathView;
1077 },
1078
1079 select: function(scroll) {
1080 this.getChangesetList().selectChangeset(this, scroll);
1081 return this;
1082 }
1083 },
1084
1085 statics: {
1086 getForNode: function(node) {
1087 var data = JX.Stratcom.getData(node);
1088 if (!data.changesetViewManager) {
1089 data.changesetViewManager = new JX.DiffChangeset(node);
1090 }
1091 return data.changesetViewManager;
1092 }
1093 }
1094});