@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 1094 lines 30 kB view raw
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});