Path: blob/master/webroot/rsrc/js/application/diff/DiffChangesetList.js
12242 views
/**1* @provides phabricator-diff-changeset-list2* @requires javelin-install3* phuix-button-view4* phabricator-diff-tree-view5* @javelin6*/78JX.install('DiffChangesetList', {910construct: function() {11this._changesets = [];1213var onload = JX.bind(this, this._ifawake, this._onload);14JX.Stratcom.listen('click', 'differential-load', onload);1516var onmore = JX.bind(this, this._ifawake, this._onmore);17JX.Stratcom.listen('click', 'show-more', onmore);1819var onmenu = JX.bind(this, this._ifawake, this._onmenu);20JX.Stratcom.listen('click', 'differential-view-options', onmenu);2122var onexpand = JX.bind(this, this._ifawake, this._oncollapse, false);23JX.Stratcom.listen('click', 'reveal-inline', onexpand);2425var onresize = JX.bind(this, this._ifawake, this._onresize);26JX.Stratcom.listen('resize', null, onresize);2728var onscroll = JX.bind(this, this._ifawake, this._onscroll);29JX.Stratcom.listen('scroll', null, onscroll);3031JX.enableDispatch(window, 'selectstart');3233var onselect = JX.bind(this, this._ifawake, this._onselect);34JX.Stratcom.listen(35['mousedown', 'selectstart'],36['differential-inline-comment', 'differential-inline-header'],37onselect);3839var onhover = JX.bind(this, this._ifawake, this._onhover);40JX.Stratcom.listen(41['mouseover', 'mouseout'],42'differential-inline-comment',43onhover);4445var onrangedown = JX.bind(this, this._ifawake, this._onrangedown);46JX.Stratcom.listen(47'mousedown',48['differential-changeset', 'tag:td'],49onrangedown);5051var onrangemove = JX.bind(this, this._ifawake, this._onrangemove);52JX.Stratcom.listen(53['mouseover', 'mouseout'],54['differential-changeset', 'tag:td'],55onrangemove);5657var onrangeup = JX.bind(this, this._ifawake, this._onrangeup);58JX.Stratcom.listen(59'mouseup',60null,61onrangeup);6263var onrange = JX.bind(this, this._ifawake, this._onSelectRange);64JX.enableDispatch(window, 'selectionchange');65JX.Stratcom.listen('selectionchange', null, onrange);6667this._setupInlineCommentListeners();68},6970properties: {71translations: null,72inlineURI: null,73inlineListURI: null,74isStandalone: false,75formationView: null76},7778members: {79_initialized: false,80_asleep: true,81_changesets: null,8283_cursorItem: null,8485_focusNode: null,86_focusStart: null,87_focusEnd: null,8889_hoverInline: null,90_hoverOrigin: null,91_hoverTarget: null,9293_rangeActive: false,94_rangeOrigin: null,95_rangeTarget: null,9697_bannerNode: null,98_unsavedButton: null,99_unsubmittedButton: null,100_doneButton: null,101_doneMode: null,102103_dropdownMenu: null,104_menuButton: null,105_menuItems: null,106_selectedChangeset: null,107108sleep: function() {109this._asleep = true;110111this._redrawFocus();112this._redrawSelection();113this.resetHover();114115this._bannerChangeset = null;116this._redrawBanner();117},118119wake: function() {120this._asleep = false;121122this._redrawFocus();123this._redrawSelection();124125this._bannerChangeset = null;126this._redrawBanner();127128this._redrawFiletree();129130if (this._initialized) {131return;132}133134this._initialized = true;135var pht = this.getTranslations();136137// We may be viewing the normal "/D123" view (with all the changesets)138// or the standalone view (with just one changeset). In the standalone139// view, some options (like jumping to next or previous file) do not140// make sense and do not function.141var standalone = this.getIsStandalone();142143var label;144145if (!standalone) {146label = pht('Jump to the table of contents.');147this._installKey('t', 'diff-nav', label, this._ontoc);148149label = pht('Jump to the comment area.');150this._installKey('x', 'diff-nav', label, this._oncomments);151}152153label = pht('Jump to next change.');154this._installJumpKey('j', label, 1);155156label = pht('Jump to previous change.');157this._installJumpKey('k', label, -1);158159if (!standalone) {160label = pht('Jump to next file.');161this._installJumpKey('J', label, 1, 'file');162163label = pht('Jump to previous file.');164this._installJumpKey('K', label, -1, 'file');165}166167label = pht('Jump to next inline comment.');168this._installJumpKey('n', label, 1, 'comment');169170label = pht('Jump to previous inline comment.');171this._installJumpKey('p', label, -1, 'comment');172173label = pht('Jump to next inline comment, including collapsed comments.');174this._installJumpKey('N', label, 1, 'comment', true);175176label = pht(177'Jump to previous inline comment, including collapsed comments.');178this._installJumpKey('P', label, -1, 'comment', true);179180var formation = this.getFormationView();181if (formation) {182var filetree = formation.getColumn(0);183var toggletree = JX.bind(filetree, filetree.toggleVisibility);184label = pht('Hide or show the paths panel.');185this._installKey('f', 'diff-vis', label, toggletree);186}187188if (!standalone) {189label = pht('Hide or show the current changeset.');190this._installKey('h', 'diff-vis', label, this._onkeytogglefile);191}192193label = pht('Reply to selected inline comment or change.');194this._installKey('r', 'inline', label,195JX.bind(this, this._onkeyreply, false));196197label = pht('Reply and quote selected inline comment.');198this._installKey('R', 'inline', label,199JX.bind(this, this._onkeyreply, true));200201label = pht('Add new inline comment on selected source text.');202this._installKey('c', 'inline', label,203JX.bind(this, this._onKeyCreate));204205label = pht('Edit selected inline comment.');206this._installKey('e', 'inline', label, this._onkeyedit);207208label = pht('Mark or unmark selected inline comment as done.');209this._installKey('w', 'inline', label, this._onkeydone);210211label = pht('Collapse or expand inline comment.');212this._installKey('q', 'diff-vis', label, this._onkeycollapse);213214label = pht('Hide or show all inline comments.');215this._installKey('A', 'diff-vis', label, this._onkeyhideall);216217label = pht('Show path in repository.');218this._installKey('d', 'diff-nav', label, this._onkeyshowpath);219220label = pht('Show directory in repository.');221this._installKey('D', 'diff-nav', label, this._onkeyshowdirectory);222223label = pht('Open file in external editor.');224this._installKey('\\', 'diff-nav', label, this._onkeyopeneditor);225},226227isAsleep: function() {228return this._asleep;229},230231newChangesetForNode: function(node) {232var changeset = JX.DiffChangeset.getForNode(node);233234this._changesets.push(changeset);235changeset.setChangesetList(this);236237return changeset;238},239240getChangesetForNode: function(node) {241return JX.DiffChangeset.getForNode(node);242},243244getInlineByID: function(id) {245var inline = null;246247for (var ii = 0; ii < this._changesets.length; ii++) {248inline = this._changesets[ii].getInlineByID(id);249if (inline) {250break;251}252}253254return inline;255},256257_ifawake: function(f) {258// This function takes another function and only calls it if the259// changeset list is awake, so we basically just ignore events when we260// are asleep. This may move up the stack at some point as we do more261// with Quicksand/Sheets.262263if (this.isAsleep()) {264return;265}266267return f.apply(this, [].slice.call(arguments, 1));268},269270_onload: function(e) {271var data = e.getNodeData('differential-load');272273// NOTE: We can trigger a load from either an explicit "Load" link on274// the changeset, or by clicking a link in the table of contents. If275// the event was a table of contents link, we let the anchor behavior276// run normally.277if (data.kill) {278e.kill();279}280281var node = JX.$(data.id);282var changeset = this.getChangesetForNode(node);283284changeset.load();285286// TODO: Move this into Changeset.287var routable = changeset.getRoutable();288if (routable) {289routable.setPriority(2000);290}291},292293_installKey: function(key, group, label, handler) {294handler = JX.bind(this, this._ifawake, handler);295296return new JX.KeyboardShortcut(key, label)297.setHandler(handler)298.setGroup(group)299.register();300},301302_installJumpKey: function(key, label, delta, filter, show_collapsed) {303filter = filter || null;304305var options = {306filter: filter,307collapsed: show_collapsed308};309310var handler = JX.bind(this, this._onjumpkey, delta, options);311return this._installKey(key, 'diff-nav', label, handler);312},313314_ontoc: function(manager) {315var toc = JX.$('toc');316manager.scrollTo(toc);317},318319_oncomments: function(manager) {320var reply = JX.$('reply');321manager.scrollTo(reply);322},323324getSelectedInline: function() {325var cursor = this._cursorItem;326327if (cursor) {328if (cursor.type == 'comment') {329return cursor.target;330}331}332333return null;334},335336_onkeyreply: function(is_quote) {337var cursor = this._cursorItem;338339if (cursor) {340if (cursor.type == 'comment') {341var inline = cursor.target;342if (inline.canReply()) {343this.setFocus(null);344inline.reply(is_quote);345return;346}347}348349// If the keyboard cursor is selecting a range of lines, we may have350// a mixture of old and new changes on the selected rows. It is not351// entirely unambiguous what the user means when they say they want352// to reply to this, but we use this logic: reply on the new file if353// there are any new lines. Otherwise (if there are only removed354// lines) reply on the old file.355356if (cursor.type == 'change') {357var cells = this._getLineNumberCellsForChangeBlock(358cursor.nodes.begin,359cursor.nodes.end);360361cursor.changeset.newInlineForRange(cells.src, cells.dst);362363this.setFocus(null);364return;365}366}367368var pht = this.getTranslations();369this._warnUser(pht('You must select a comment or change to reply to.'));370},371372_getLineNumberCellsForChangeBlock: function(origin, target) {373// The "origin" and "target" are entire rows, but we need to find374// a range of cell nodes to actually create an inline, so go375// fishing.376377var old_list = [];378var new_list = [];379380var row = origin;381while (row) {382var header = row.firstChild;383while (header) {384if (this.getLineNumberFromHeader(header)) {385if (header.className.indexOf('old') !== -1) {386old_list.push(header);387} else if (header.className.indexOf('new') !== -1) {388new_list.push(header);389}390}391header = header.nextSibling;392}393394if (row == target) {395break;396}397398row = row.nextSibling;399}400401var use_list;402if (new_list.length) {403use_list = new_list;404} else {405use_list = old_list;406}407408var src = use_list[0];409var dst = use_list[use_list.length - 1];410411return {412src: src,413dst: dst414};415},416417_onkeyedit: function() {418var cursor = this._cursorItem;419420if (cursor) {421if (cursor.type == 'comment') {422var inline = cursor.target;423if (inline.canEdit()) {424this.setFocus(null);425426inline.edit();427return;428}429}430}431432var pht = this.getTranslations();433this._warnUser(pht('You must select a comment to edit.'));434},435436_onKeyCreate: function() {437var start = this._sourceSelectionStart;438var end = this._sourceSelectionEnd;439440if (!this._sourceSelectionStart) {441var pht = this.getTranslations();442this._warnUser(443pht(444'You must select source text to create a new inline comment.'));445return;446}447448this._setSourceSelection(null, null);449450var changeset = start.changeset;451452var config = {};453if (changeset.getResponseDocumentEngineKey() === null) {454// If the changeset is using a document renderer, we ignore the455// selection range and just treat this as a comment from the first456// block to the last block.457458// If we don't discard the range, we later render a bogus highlight459// if the block content is complex (like a Jupyter notebook cell460// with images).461462config.startOffset = start.offset;463config.endOffset = end.offset;464}465466changeset.newInlineForRange(start.targetNode, end.targetNode, config);467},468469_onkeydone: function() {470var cursor = this._cursorItem;471472if (cursor) {473if (cursor.type == 'comment') {474var inline = cursor.target;475if (inline.canDone()) {476this.setFocus(null);477478inline.toggleDone();479return;480}481}482}483484var pht = this.getTranslations();485this._warnUser(pht('You must select a comment to mark done.'));486},487488_onkeytogglefile: function() {489var pht = this.getTranslations();490var changeset = this._getChangesetForKeyCommand();491492if (!changeset) {493this._warnUser(pht('You must select a file to hide or show.'));494return;495}496497changeset.toggleVisibility();498},499500_getChangesetForKeyCommand: function() {501var cursor = this._cursorItem;502503var changeset;504if (cursor) {505changeset = cursor.changeset;506}507508if (!changeset) {509changeset = this._getVisibleChangeset();510}511512return changeset;513},514515_onkeyopeneditor: function(e) {516var pht = this.getTranslations();517var changeset = this._getChangesetForKeyCommand();518519if (!changeset) {520this._warnUser(pht('You must select a file to edit.'));521return;522}523524this._openEditor(changeset);525},526527_openEditor: function(changeset) {528var pht = this.getTranslations();529530var editor_template = changeset.getEditorURITemplate();531if (editor_template === null) {532this._warnUser(pht('No external editor is configured.'));533return;534}535536var line = null;537538// See PHI1749. We aren't exactly sure what the user intends when they539// use the keyboard to select a change block and then activate the540// "Open in Editor" function: they might mean to open the old or new541// offset, and may have the old or new state (or some other state) in542// their working copy.543544// For now, pick: the new state line number if one exists; or the old545// state line number if one does not. If nothing else, this behavior is546// simple.547548// If there's a document engine, just open the file to the first line.549// We currently can not map display blocks to source lines.550551// If there's an inline, open the file to that line.552553if (changeset.getResponseDocumentEngineKey() === null) {554var cursor = this._cursorItem;555if (cursor && (cursor.changeset === changeset)) {556if (cursor.type == 'change') {557var cells = this._getLineNumberCellsForChangeBlock(558cursor.nodes.begin,559cursor.nodes.end);560line = this.getLineNumberFromHeader(cells.src);561}562563if (cursor.type === 'comment') {564var inline = cursor.target;565line = inline.getLineNumber();566}567}568}569570var variables = {571l: line || 1572};573574var editor_uri = new JX.ExternalEditorLinkEngine()575.setTemplate(editor_template)576.setVariables(variables)577.newURI();578579JX.$U(editor_uri).go();580},581582_onkeyshowpath: function() {583this._onrepositorykey(false);584},585586_onkeyshowdirectory: function() {587this._onrepositorykey(true);588},589590_onrepositorykey: function(is_directory) {591var pht = this.getTranslations();592var changeset = this._getChangesetForKeyCommand();593594if (!changeset) {595this._warnUser(pht('You must select a file to open.'));596return;597}598599var show_uri;600if (is_directory) {601show_uri = changeset.getShowDirectoryURI();602} else {603show_uri = changeset.getShowPathURI();604}605606if (show_uri === null) {607return;608}609610window.open(show_uri);611},612613_onkeycollapse: function() {614var cursor = this._cursorItem;615616if (cursor) {617if (cursor.type == 'comment') {618var inline = cursor.target;619if (inline.canCollapse()) {620this.setFocus(null);621622inline.setCollapsed(!inline.isCollapsed());623return;624}625}626}627628var pht = this.getTranslations();629this._warnUser(pht('You must select a comment to hide.'));630},631632_onkeyhideall: function() {633var inlines = this._getInlinesByType();634if (inlines.visible.length) {635this._toggleInlines('all');636} else {637this._toggleInlines('show');638}639},640641_warnUser: function(message) {642new JX.Notification()643.setContent(message)644.alterClassName('jx-notification-alert', true)645.setDuration(3000)646.show();647},648649_onjumpkey: function(delta, options) {650var state = this._getSelectionState();651652var filter = options.filter || null;653var collapsed = options.collapsed || false;654var wrap = options.wrap || false;655var attribute = options.attribute || null;656var show = options.show || false;657658var cursor = state.cursor;659var items = state.items;660661// If there's currently no selection and the user tries to go back,662// don't do anything.663if ((cursor === null) && (delta < 0)) {664return;665}666667var did_wrap = false;668while (true) {669if (cursor === null) {670cursor = 0;671} else {672cursor = cursor + delta;673}674675// If we've gone backward past the first change, bail out.676if (cursor < 0) {677return;678}679680// If we've gone forward off the end of the list, figure out where we681// should end up.682if (cursor >= items.length) {683if (!wrap) {684// If we aren't wrapping around, we're done.685return;686}687688if (did_wrap) {689// If we're already wrapped around, we're done.690return;691}692693// Otherwise, wrap the cursor back to the top.694cursor = 0;695did_wrap = true;696}697698// If we're selecting things of a particular type (like only files)699// and the next item isn't of that type, move past it.700if (filter !== null) {701if (items[cursor].type !== filter) {702continue;703}704}705706// If the item is collapsed, don't select it when iterating with jump707// keys. It can still potentially be selected in other ways.708if (!collapsed) {709if (items[cursor].collapsed) {710continue;711}712}713714// If the item has been deleted, don't select it when iterating. The715// cursor may remain on it until it is removed.716if (items[cursor].deleted) {717continue;718}719720// If we're selecting things with a particular attribute, like721// "unsaved", skip items without the attribute.722if (attribute !== null) {723if (!(items[cursor].attributes || {})[attribute]) {724continue;725}726}727728// If this item is a hidden inline but we're clicking a button which729// selects inlines of a particular type, make it visible again.730if (items[cursor].hidden) {731if (!show) {732continue;733}734items[cursor].target.setHidden(false);735}736737// Otherwise, we've found a valid item to select.738break;739}740741this._setSelectionState(items[cursor], true);742},743744_getSelectionState: function() {745var items = this._getSelectableItems();746747var cursor = null;748if (this._cursorItem !== null) {749for (var ii = 0; ii < items.length; ii++) {750var item = items[ii];751if (this._cursorItem.target === item.target) {752cursor = ii;753break;754}755}756}757758return {759cursor: cursor,760items: items761};762},763764selectChangeset: function(changeset, scroll) {765var items = this._getSelectableItems();766767var cursor = null;768for (var ii = 0; ii < items.length; ii++) {769var item = items[ii];770if (changeset === item.target) {771cursor = ii;772break;773}774}775776if (cursor !== null) {777this._setSelectionState(items[cursor], scroll);778} else {779this._setSelectionState(null, false);780}781782return this;783},784785_setSelectionState: function(item, scroll) {786var old = this._cursorItem;787788if (old) {789if (old.type === 'comment') {790old.target.setIsSelected(false);791}792}793794this._cursorItem = item;795796if (item) {797if (item.type === 'comment') {798item.target.setIsSelected(true);799}800}801802this._redrawSelection(scroll);803804return this;805},806807_redrawSelection: function(scroll) {808var cursor = this._cursorItem;809if (!cursor) {810this.setFocus(null);811return;812}813814// If this item has been removed from the document (for example: create815// a new empty comment, then use the "Unsaved" button to select it, then816// cancel it), we can still keep the cursor here but do not want to show817// a selection reticle over an invisible node.818if (cursor.deleted) {819this.setFocus(null);820return;821}822823var changeset = cursor.changeset;824825var tree = this._getTreeView();826if (changeset) {827tree.setSelectedPath(cursor.changeset.getPathView());828} else {829tree.setSelectedPath(null);830}831832this._selectChangeset(changeset);833834this.setFocus(cursor.nodes.begin, cursor.nodes.end);835836if (scroll) {837var pos = JX.$V(cursor.nodes.begin);838JX.DOM.scrollToPosition(0, pos.y - 60);839}840841return this;842},843844redrawCursor: function() {845// NOTE: This is setting the cursor to the current cursor. Usually, this846// would have no effect.847848// However, if the old cursor pointed at an inline and the inline has849// been edited so the rows have changed, this updates the cursor to point850// at the new inline with the proper rows for the current state, and851// redraws the reticle correctly.852853var state = this._getSelectionState();854if (state.cursor !== null) {855this._setSelectionState(state.items[state.cursor], false);856}857},858859_getSelectableItems: function() {860var result = [];861862for (var ii = 0; ii < this._changesets.length; ii++) {863var items = this._changesets[ii].getSelectableItems();864for (var jj = 0; jj < items.length; jj++) {865result.push(items[jj]);866}867}868869return result;870},871872_onhover: function(e) {873if (e.getIsTouchEvent()) {874return;875}876877var inline;878if (e.getType() == 'mouseout') {879inline = null;880} else {881inline = this._getInlineForEvent(e);882}883884this._setHoverInline(inline);885},886887_onmore: function(e) {888e.kill();889890var node = e.getNode('differential-changeset');891var changeset = this.getChangesetForNode(node);892893var data = e.getNodeData('show-more');894var target = e.getNode('context-target');895896changeset.loadContext(data.range, target);897},898899_onmenu: function(e) {900var button = e.getNode('differential-view-options');901902var data = JX.Stratcom.getData(button);903if (data.menu) {904// We've already built this menu, so we can let the menu itself handle905// the event.906return;907}908909e.prevent();910911var pht = this.getTranslations();912913var node = JX.DOM.findAbove(914button,915'div',916'differential-changeset');917918var changeset_list = this;919var changeset = this.getChangesetForNode(node);920921var menu = new JX.PHUIXDropdownMenu(button)922.setWidth(240);923var list = new JX.PHUIXActionListView();924925var add_link = function(icon, name, href, local) {926var link = new JX.PHUIXActionView()927.setIcon(icon)928.setName(name)929.setHandler(function(e) {930if (local) {931window.location.assign(href);932} else {933window.open(href);934}935menu.close();936e.prevent();937});938939if (href) {940link.setHref(href);941} else {942link943.setDisabled(true)944.setUnresponsive(true);945}946947list.addItem(link);948return link;949};950951var visible_item = new JX.PHUIXActionView()952.setKeyCommand('h')953.setHandler(function(e) {954e.prevent();955menu.close();956957changeset.select(false);958changeset.toggleVisibility();959});960list.addItem(visible_item);961962var reveal_item = new JX.PHUIXActionView()963.setIcon('fa-eye');964list.addItem(reveal_item);965966list.addItem(967new JX.PHUIXActionView()968.setDivider(true));969970var up_item = new JX.PHUIXActionView()971.setHandler(function(e) {972if (changeset.isLoaded()) {973974// Don't let the user swap display modes if a comment is being975// edited, since they might lose their work. See PHI180.976var inlines = changeset.getInlines();977for (var ii = 0; ii < inlines.length; ii++) {978if (inlines[ii].isEditing()) {979changeset_list._warnUser(980pht(981'Finish editing inline comments before changing display ' +982'modes.'));983e.prevent();984menu.close();985return;986}987}988989var renderer = changeset.getRendererKey();990if (renderer == '1up') {991renderer = '2up';992} else {993renderer = '1up';994}995changeset.reload({renderer: renderer});996} else {997changeset.reload();998}9991000e.prevent();1001menu.close();1002});1003list.addItem(up_item);10041005var encoding_item = new JX.PHUIXActionView()1006.setIcon('fa-font')1007.setName(pht('Change Text Encoding...'))1008.setHandler(function(e) {1009var params = {1010encoding: changeset.getCharacterEncoding()1011};10121013new JX.Workflow('/services/encoding/', params)1014.setHandler(function(r) {1015changeset.reload({encoding: r.encoding});1016})1017.start();10181019e.prevent();1020menu.close();1021});1022list.addItem(encoding_item);10231024var highlight_item = new JX.PHUIXActionView()1025.setIcon('fa-sun-o')1026.setName(pht('Highlight As...'))1027.setHandler(function(e) {1028var params = {1029highlight: changeset.getHighlight()1030};10311032new JX.Workflow('/services/highlight/', params)1033.setHandler(function(r) {1034changeset.reload({highlight: r.highlight});1035})1036.start();10371038e.prevent();1039menu.close();1040});1041list.addItem(highlight_item);10421043var engine_item = new JX.PHUIXActionView()1044.setIcon('fa-file-image-o')1045.setName(pht('View As Document Type...'))1046.setHandler(function(e) {1047var options = changeset.getAvailableDocumentEngineKeys() || [];1048options = options.join(',');10491050var params = {1051engine: changeset.getResponseDocumentEngineKey(),1052options: options1053};10541055new JX.Workflow('/services/viewas/', params)1056.setHandler(function(r) {1057changeset.reload({engine: r.engine});1058})1059.start();10601061e.prevent();1062menu.close();1063});1064list.addItem(engine_item);10651066list.addItem(1067new JX.PHUIXActionView()1068.setDivider(true));10691070add_link('fa-external-link', pht('View Standalone'), data.standaloneURI);10711072add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI);1073add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI);10741075add_link(1076'fa-folder-open-o',1077pht('Show Directory in Repository'),1078changeset.getShowDirectoryURI())1079.setKeyCommand('D');10801081add_link(1082'fa-file-text-o',1083pht('Show Path in Repository'),1084changeset.getShowPathURI())1085.setKeyCommand('d');10861087var editor_template = changeset.getEditorURITemplate();1088if (editor_template !== null) {1089var editor_item = new JX.PHUIXActionView()1090.setIcon('fa-i-cursor')1091.setName(pht('Open in Editor'))1092.setKeyCommand('\\')1093.setHandler(function(e) {10941095changeset_list._openEditor(changeset);10961097e.prevent();1098menu.close();1099});11001101list.addItem(editor_item);1102} else {1103var configure_uri = changeset.getEditorConfigureURI();1104if (configure_uri !== null) {1105add_link('fa-wrench', pht('Configure Editor'), configure_uri);1106}1107}11081109menu.setContent(list.getNode());11101111menu.listen('open', function() {1112// When the user opens the menu, check if there are any "Show More"1113// links in the changeset body. If there aren't, disable the "Show1114// Entire File" menu item since it won't change anything.11151116var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');1117if (nodes.length) {1118reveal_item1119.setDisabled(false)1120.setName(pht('Show All Context'))1121.setIcon('fa-arrows-v')1122.setHandler(function(e) {1123changeset.loadAllContext();1124e.prevent();1125menu.close();1126});1127} else {1128reveal_item1129.setDisabled(true)1130.setUnresponsive(true)1131.setIcon('fa-file')1132.setName(pht('All Context Shown'))1133.setHref(null);1134}11351136encoding_item.setDisabled(!changeset.isLoaded());1137highlight_item.setDisabled(!changeset.isLoaded());1138engine_item.setDisabled(!changeset.isLoaded());11391140if (changeset.isLoaded()) {1141if (changeset.getRendererKey() == '2up') {1142up_item1143.setIcon('fa-list-alt')1144.setName(pht('View Unified Diff'));1145} else {1146up_item1147.setIcon('fa-columns')1148.setName(pht('View Side-by-Side Diff'));1149}1150} else {1151up_item1152.setIcon('fa-refresh')1153.setName(pht('Load Changes'));1154}11551156visible_item1157.setDisabled(true)1158.setIcon('fa-eye-slash')1159.setName(pht('Hide Changeset'));11601161var diffs = JX.DOM.scry(1162JX.$(data.containerID),1163'table',1164'differential-diff');11651166if (diffs.length > 1) {1167JX.$E(1168'More than one node with sigil "differential-diff" was found in "'+1169data.containerID+'."');1170} else if (diffs.length == 1) {1171visible_item.setDisabled(false);1172} else {1173// Do nothing when there is no diff shown in the table. For example,1174// the file is binary.1175}11761177});11781179data.menu = menu;1180changeset.setViewMenu(menu);1181menu.open();1182},11831184_oncollapse: function(is_collapse, e) {1185e.kill();11861187var inline = this._getInlineForEvent(e);11881189inline.setCollapsed(is_collapse);1190},11911192_onresize: function() {1193this._redrawFocus();1194this._redrawSelection();11951196// Force a banner redraw after a resize event. Particularly, this makes1197// sure the inline state updates immediately after an inline edit1198// operation, even if the changeset itself has not changed.1199this._bannerChangeset = null;12001201this._redrawBanner();12021203var changesets = this._changesets;1204for (var ii = 0; ii < changesets.length; ii++) {1205changesets[ii].redrawFileTree();1206}1207},12081209_onscroll: function() {1210this._redrawBanner();1211},12121213_onselect: function(e) {1214// If the user clicked some element inside the header, like an action1215// icon, ignore the event. They have to click the header element itself.1216if (e.getTarget() !== e.getNode('differential-inline-header')) {1217return;1218}12191220// If the user has double-clicked or triple-clicked a header, we want to1221// toggle the inline selection mode, not select text. Kill select events1222// originating with this element as the target.1223if (e.getType() === 'selectstart') {1224e.kill();1225return;1226}12271228var inline = this._getInlineForEvent(e);1229if (!inline) {1230return;1231}12321233// NOTE: Don't kill or prevent the event. In particular, we want this1234// click to clear any text selection as it normally would.12351236this.selectInline(inline);1237},12381239selectInline: function(inline, force, scroll) {1240var selection = this._getSelectionState();1241var item;12421243if (!force) {1244// If the comment the user clicked is currently selected, deselect it.1245// This makes it easy to undo things if you clicked by mistake.1246if (selection.cursor !== null) {1247item = selection.items[selection.cursor];1248if (item.target === inline) {1249this._setSelectionState(null, false);1250return;1251}1252}1253}12541255// Otherwise, select the item that the user clicked. This makes it1256// easier to resume keyboard operations after using the mouse to do1257// something else.1258var items = selection.items;1259for (var ii = 0; ii < items.length; ii++) {1260item = items[ii];1261if (item.target === inline) {1262this._setSelectionState(item, scroll);1263}1264}12651266},12671268redrawPreview: function() {1269// TODO: This isn't the cleanest way to find the preview form, but1270// rendering no longer has direct access to it.1271var forms = JX.DOM.scry(document.body, 'form', 'transaction-append');1272if (forms.length) {1273JX.DOM.invoke(forms[0], 'shouldRefresh');1274}12751276// Clear the mouse hover reticle after a substantive edit: we don't get1277// a "mouseout" event if the row vanished because of row being removed1278// after an edit.1279this.resetHover();1280},12811282setFocus: function(node, extended_node) {1283if (!node) {1284var tree = this._getTreeView();1285tree.setSelectedPath(null);1286this._selectChangeset(null);1287}12881289this._focusStart = node;1290this._focusEnd = extended_node;1291this._redrawFocus();1292},12931294_selectChangeset: function(changeset) {1295if (this._selectedChangeset === changeset) {1296return;1297}12981299if (this._selectedChangeset !== null) {1300this._selectedChangeset.setIsSelected(false);1301this._selectedChangeset = null;1302}13031304this._selectedChangeset = changeset;1305if (this._selectedChangeset !== null) {1306this._selectedChangeset.setIsSelected(true);1307}1308},13091310_redrawFocus: function() {1311var node = this._focusStart;1312var extended_node = this._focusEnd || node;13131314var reticle = this._getFocusNode();1315if (!node || this.isAsleep()) {1316JX.DOM.remove(reticle);1317return;1318}13191320// Outset the reticle some pixels away from the element, so there's some1321// space between the focused element and the outline.1322var p = JX.Vector.getPos(node);1323var s = JX.Vector.getAggregateScrollForNode(node);1324var d = JX.Vector.getDim(node);13251326p.add(s).add(d.x + 1, 4).setPos(reticle);1327// Compute the size we need to extend to the full extent of the focused1328// nodes.1329JX.Vector.getPos(extended_node)1330.add(-p.x, -p.y)1331.add(0, JX.Vector.getDim(extended_node).y)1332.add(10, -4)1333.setDim(reticle);13341335JX.DOM.getContentFrame().appendChild(reticle);1336},13371338_getFocusNode: function() {1339if (!this._focusNode) {1340var node = JX.$N('div', {className : 'keyboard-focus-focus-reticle'});1341this._focusNode = node;1342}1343return this._focusNode;1344},13451346_setHoverInline: function(inline) {1347var origin = null;1348var target = null;13491350if (inline) {1351var changeset = inline.getChangeset();13521353var changeset_id;1354var side = inline.getDisplaySide();1355if (side == 'right') {1356changeset_id = changeset.getRightChangesetID();1357} else {1358changeset_id = changeset.getLeftChangesetID();1359}13601361var new_part;1362if (inline.isNewFile()) {1363new_part = 'N';1364} else {1365new_part = 'O';1366}13671368var prefix = 'C' + changeset_id + new_part + 'L';13691370var number = inline.getLineNumber();1371var length = inline.getLineLength();13721373try {1374origin = JX.$(prefix + number);1375target = JX.$(prefix + (number + length));1376} catch (error) {1377// There may not be any nodes present in the document. A case where1378// this occurs is when you reply to a ghost inline which was made1379// on lines near the bottom of "long.txt" in an earlier diff, and1380// the file was later shortened so those lines no longer exist. For1381// more details, see T11662.13821383origin = null;1384target = null;1385}1386}13871388this._setHoverRange(origin, target, inline);1389},13901391_setHoverRange: function(origin, target, inline) {1392inline = inline || null;13931394var origin_dirty = (origin !== this._hoverOrigin);1395var target_dirty = (target !== this._hoverTarget);1396var inline_dirty = (inline !== this._hoverInline);13971398var any_dirty = (origin_dirty || target_dirty || inline_dirty);1399if (any_dirty) {1400this._hoverOrigin = origin;1401this._hoverTarget = target;1402this._hoverInline = inline;1403this._redrawHover();1404}1405},14061407resetHover: function() {1408this._setHoverRange(null, null, null);1409},14101411_redrawHover: function() {1412var map = this._hoverMap;1413if (map) {1414this._hoverMap = null;1415this._applyHoverHighlight(map, false);1416}14171418var rows = this._hoverRows;1419if (rows) {1420this._hoverRows = null;1421this._applyHoverHighlight(rows, false);1422}14231424if (!this._hoverOrigin || this.isAsleep()) {1425return;1426}14271428var top = this._hoverOrigin;1429var bot = this._hoverTarget;1430if (JX.$V(top).y > JX.$V(bot).y) {1431var tmp = top;1432top = bot;1433bot = tmp;1434}14351436// Find the leftmost cell that we're going to highlight. This is the1437// next sibling with a "data-copy-mode" attribute, which is a marker1438// for the cell with actual content in it.1439var content_cell = top;1440while (content_cell && !this._isContentCell(content_cell)) {1441content_cell = content_cell.nextSibling;1442}14431444// If we didn't find a cell to highlight, don't highlight anything.1445if (!content_cell) {1446return;1447}14481449rows = this._findContentCells(top, bot, content_cell);14501451var inline = this._hoverInline;1452if (!inline) {1453this._hoverRows = rows;1454this._applyHoverHighlight(this._hoverRows, true);1455return;1456}14571458if (!inline.hoverMap) {1459inline.hoverMap = this._newHoverMap(rows, inline);1460}14611462this._hoverMap = inline.hoverMap;1463this._applyHoverHighlight(this._hoverMap, true);1464},14651466_applyHoverHighlight: function(items, on) {1467for (var ii = 0; ii < items.length; ii++) {1468var item = items[ii];14691470JX.DOM.alterClass(item.lineNode, 'inline-hover', on);1471JX.DOM.alterClass(item.cellNode, 'inline-hover', on);14721473if (item.bright) {1474JX.DOM.alterClass(item.cellNode, 'inline-hover-bright', on);1475}14761477if (item.hoverNode) {1478if (on) {1479item.cellNode.insertBefore(1480item.hoverNode,1481item.cellNode.firstChild);1482} else {1483JX.DOM.remove(item.hoverNode);1484}1485}1486}1487},14881489_findContentCells: function(top, bot, content_cell) {1490var head_row = JX.DOM.findAbove(top, 'tr');1491var last_row = JX.DOM.findAbove(bot, 'tr');14921493var cursor = head_row;1494var rows = [];1495var idx = null;1496var ii;1497var line_cell = null;1498do {1499line_cell = null;1500for (ii = 0; ii < cursor.childNodes.length; ii++) {1501var child = cursor.childNodes[ii];1502if (!JX.DOM.isType(child, 'td')) {1503continue;1504}15051506if (child.getAttribute('data-n')) {1507line_cell = child;1508}15091510if (child === content_cell) {1511idx = ii;1512}15131514if (ii !== idx) {1515continue;1516}15171518if (this._isContentCell(child)) {1519rows.push({1520lineNode: line_cell,1521cellNode: child1522});1523}15241525break;1526}15271528if (cursor === last_row) {1529break;1530}15311532cursor = cursor.nextSibling;1533} while (cursor);15341535return rows;1536},15371538_newHoverMap: function(rows, inline) {1539var start = inline.getStartOffset();1540var end = inline.getEndOffset();15411542var info;1543var content;1544for (ii = 0; ii < rows.length; ii++) {1545info = this._getSelectionOffset(rows[ii].cellNode, null);15461547content = info.content;1548content = content.replace(/\n+$/, '');15491550rows[ii].content = content;1551}15521553var attr_dull = {1554className: 'inline-hover-text'1555};15561557var attr_bright = {1558className: 'inline-hover-text inline-hover-text-bright'1559};15601561var attr_container = {1562className: 'inline-hover-container'1563};15641565var min = 0;1566var max = rows.length - 1;1567var offset_min;1568var offset_max;1569var len;1570var node;1571var text;1572var any_highlight = false;1573for (ii = 0; ii < rows.length; ii++) {1574content = rows[ii].content;1575len = content.length;15761577if (ii === min && (start !== null)) {1578offset_min = start;1579} else {1580offset_min = 0;1581}15821583if (ii === max && (end !== null)) {1584offset_max = Math.min(end, len);1585} else {1586offset_max = len;1587}15881589var has_min = (offset_min > 0);1590var has_max = (offset_max < len);15911592if (has_min || has_max) {1593any_highlight = true;1594}15951596rows[ii].min = offset_min;1597rows[ii].max = offset_max;1598rows[ii].hasMin = has_min;1599rows[ii].hasMax = has_max;1600}16011602for (ii = 0; ii < rows.length; ii++) {1603content = rows[ii].content;1604offset_min = rows[ii].min;1605offset_max = rows[ii].max;16061607var has_highlight = (rows[ii].hasMin || rows[ii].hasMax);16081609if (any_highlight) {1610var parts = [];16111612if (offset_min > 0) {1613text = content.substring(0, offset_min);1614node = JX.$N('span', attr_dull, text);1615parts.push(node);1616}16171618if (len) {1619text = content.substring(offset_min, offset_max);1620node = JX.$N('span', attr_bright, text);1621parts.push(node);1622}16231624if (offset_max < len) {1625text = content.substring(offset_max, len);1626node = JX.$N('span', attr_dull, text);1627parts.push(node);1628}16291630rows[ii].hoverNode = JX.$N('div', attr_container, parts);1631} else {1632rows[ii].hoverNode = null;1633}16341635rows[ii].bright = (any_highlight && !has_highlight);1636}16371638return rows;1639},16401641_deleteInlineByID: function(id) {1642var uri = this.getInlineURI();1643var data = {1644op: 'refdelete',1645id: id1646};16471648var handler = JX.bind(this, this.redrawPreview);16491650new JX.Workflow(uri, data)1651.setHandler(handler)1652.start();1653},16541655_getInlineForEvent: function(e) {1656var node = e.getNode('differential-changeset');1657if (!node) {1658return null;1659}16601661var changeset = this.getChangesetForNode(node);16621663var inline_row = e.getNode('inline-row');1664return changeset.getInlineForRow(inline_row);1665},16661667getLineNumberFromHeader: function(node) {1668var n = parseInt(node.getAttribute('data-n'));16691670if (!n) {1671return null;1672}16731674// If this is a line number that's part of a row showing more context,1675// we don't want to let users leave inlines here.16761677try {1678JX.DOM.findAbove(node, 'tr', 'context-target');1679return null;1680} catch (ex) {1681// Ignore.1682}16831684return n;1685},16861687getDisplaySideFromHeader: function(th) {1688return (th.parentNode.firstChild != th) ? 'right' : 'left';1689},16901691_onrangedown: function(e) {1692// NOTE: We're allowing "mousedown" from a touch event through so users1693// can leave inlines on a single line.16941695// See PHI985. We want to exclude both right-mouse and middle-mouse1696// clicks from continuing.1697if (!e.isLeftButton()) {1698return;1699}17001701if (this._rangeActive) {1702return;1703}17041705var target = e.getTarget();1706var number = this.getLineNumberFromHeader(target);1707if (!number) {1708return;1709}17101711e.kill();1712this._rangeActive = true;17131714this._rangeOrigin = target;1715this._rangeTarget = target;17161717this._setHoverRange(this._rangeOrigin, this._rangeTarget);1718},17191720_onrangemove: function(e) {1721if (e.getIsTouchEvent()) {1722return;1723}17241725var is_out = (e.getType() == 'mouseout');1726var target = e.getTarget();17271728this._updateRange(target, is_out);1729},17301731_updateRange: function(target, is_out) {1732// Don't update the range if this target doesn't correspond to a line1733// number. For instance, this may be a dead line number, like the empty1734// line numbers on the left hand side of a newly added file.1735var number = this.getLineNumberFromHeader(target);1736if (!number) {1737return;1738}17391740if (this._rangeActive) {1741var origin = this._hoverOrigin;17421743// Don't update the reticle if we're selecting a line range and the1744// "<th />" under the cursor is on the wrong side of the file. You can1745// only leave inline comments on the left or right side of a file, not1746// across lines on both sides.1747var origin_side = this.getDisplaySideFromHeader(origin);1748var target_side = this.getDisplaySideFromHeader(target);1749if (origin_side != target_side) {1750return;1751}17521753// Don't update the reticle if we're selecting a line range and the1754// "<th />" under the cursor corresponds to a different file. You can1755// only leave inline comments on lines in a single file, not across1756// multiple files.1757var origin_table = JX.DOM.findAbove(origin, 'table');1758var target_table = JX.DOM.findAbove(target, 'table');1759if (origin_table != target_table) {1760return;1761}1762}17631764if (is_out) {1765if (this._rangeActive) {1766// If we're dragging a range, just leave the state as it is. This1767// allows you to drag over something invalid while selecting a1768// range without the range flickering or getting lost.1769} else {1770// Otherwise, clear the current range.1771this.resetHover();1772}1773return;1774}17751776if (this._rangeActive) {1777this._rangeTarget = target;1778} else {1779this._rangeOrigin = target;1780this._rangeTarget = target;1781}17821783this._setHoverRange(this._rangeOrigin, this._rangeTarget);1784},17851786_onrangeup: function(e) {1787if (!this._rangeActive) {1788return;1789}17901791e.kill();17921793var origin = this._rangeOrigin;1794var target = this._rangeTarget;17951796// If the user dragged a range from the bottom to the top, swap the node1797// order around.1798if (JX.$V(origin).y > JX.$V(target).y) {1799var tmp = target;1800target = origin;1801origin = tmp;1802}18031804var node = JX.DOM.findAbove(origin, null, 'differential-changeset');1805var changeset = this.getChangesetForNode(node);18061807changeset.newInlineForRange(origin, target);18081809this._rangeActive = false;1810this._rangeOrigin = null;1811this._rangeTarget = null;18121813this.resetHover();1814},18151816_redrawBanner: function() {1817// If the inline comment menu is open and we've done a redraw, close it.1818// In particular, this makes it close when you scroll the document:1819// otherwise, it stays open but the banner moves underneath it.1820if (this._dropdownMenu) {1821this._dropdownMenu.close();1822}18231824var node = this._getBannerNode();1825var changeset = this._getVisibleChangeset();1826var tree = this._getTreeView();1827var formation = this.getFormationView();18281829if (!changeset) {1830this._bannerChangeset = null;1831JX.DOM.remove(node);1832tree.setFocusedPath(null);18331834if (formation) {1835formation.repaint();1836}18371838return;1839}18401841// Don't do anything if nothing has changed. This seems to avoid some1842// flickering issues in Safari, at least.1843if (this._bannerChangeset === changeset) {1844return;1845}1846this._bannerChangeset = changeset;18471848var paths = tree.getPaths();1849for (var ii = 0; ii < paths.length; ii++) {1850var path = paths[ii];1851if (path.getChangeset() === changeset) {1852tree.setFocusedPath(path);1853}1854}18551856var inlines = this._getInlinesByType();18571858var unsaved = inlines.unsaved;1859var unsubmitted = inlines.unsubmitted;1860var undone = inlines.undone;1861var done = inlines.done;1862var draft_done = inlines.draftDone;18631864JX.DOM.alterClass(1865node,1866'diff-banner-has-unsaved',1867!!unsaved.length);18681869JX.DOM.alterClass(1870node,1871'diff-banner-has-unsubmitted',1872!!unsubmitted.length);18731874JX.DOM.alterClass(1875node,1876'diff-banner-has-draft-done',1877!!draft_done.length);18781879var pht = this.getTranslations();1880var unsaved_button = this._getUnsavedButton();1881var unsubmitted_button = this._getUnsubmittedButton();1882var done_button = this._getDoneButton();1883var menu_button = this._getMenuButton();18841885if (unsaved.length) {1886unsaved_button.setText(unsaved.length + ' ' + pht('Unsaved'));1887JX.DOM.show(unsaved_button.getNode());1888} else {1889JX.DOM.hide(unsaved_button.getNode());1890}18911892if (unsubmitted.length || draft_done.length) {1893var any_draft_count = unsubmitted.length + draft_done.length;18941895unsubmitted_button.setText(any_draft_count + ' ' + pht('Unsubmitted'));1896JX.DOM.show(unsubmitted_button.getNode());1897} else {1898JX.DOM.hide(unsubmitted_button.getNode());1899}19001901if (done.length || undone.length) {1902// If you haven't marked any comments as "Done", we just show text1903// like "3 Comments". If you've marked at least one done, we show1904// "1 / 3 Comments".19051906var done_text;1907if (done.length) {1908done_text = [1909done.length,1910' / ',1911(done.length + undone.length),1912' ',1913pht('Comments')1914];1915} else {1916done_text = [1917undone.length,1918' ',1919pht('Comments')1920];1921}19221923done_button.setText(done_text);19241925JX.DOM.show(done_button.getNode());19261927// If any comments are not marked "Done", this cycles through the1928// missing comments. Otherwise, it cycles through all the saved1929// comments.1930if (undone.length) {1931this._doneMode = 'undone';1932} else {1933this._doneMode = 'done';1934}19351936} else {1937JX.DOM.hide(done_button.getNode());1938}19391940var path_view = [icon, ' ', changeset.getDisplayPath()];19411942var buttons_attrs = {1943className: 'diff-banner-buttons'1944};19451946var buttons_list = [1947unsaved_button.getNode(),1948unsubmitted_button.getNode(),1949done_button.getNode(),1950menu_button.getNode()1951];19521953var buttons_view = JX.$N('div', buttons_attrs, buttons_list);19541955var icon = new JX.PHUIXIconView()1956.setIcon(changeset.getIcon())1957.getNode();1958JX.DOM.setContent(node, [buttons_view, path_view]);19591960document.body.appendChild(node);19611962if (formation) {1963formation.repaint();1964}1965},19661967_getInlinesByType: function() {1968var changesets = this._changesets;1969var unsaved = [];1970var unsubmitted = [];1971var undone = [];1972var done = [];1973var draft_done = [];19741975var visible_done = [];1976var visible_collapsed = [];1977var visible_ghosts = [];1978var visible = [];1979var hidden = [];19801981for (var ii = 0; ii < changesets.length; ii++) {1982var inlines = changesets[ii].getInlines();1983var inline;1984var jj;1985for (jj = 0; jj < inlines.length; jj++) {1986inline = inlines[jj];19871988if (inline.isDeleted()) {1989continue;1990}19911992if (inline.isSynthetic()) {1993continue;1994}19951996if (inline.isEditing()) {1997unsaved.push(inline);1998} else if (!inline.getID()) {1999// These are new comments which have been cancelled, and do not2000// count as anything.2001continue;2002} else if (inline.isDraft()) {2003unsubmitted.push(inline);2004} else {2005// NOTE: Unlike other states, an inline may be marked with a2006// draft checkmark and still be a "done" or "undone" comment.2007if (inline.isDraftDone()) {2008draft_done.push(inline);2009}20102011if (!inline.isDone()) {2012undone.push(inline);2013} else {2014done.push(inline);2015}2016}2017}20182019for (jj = 0; jj < inlines.length; jj++) {2020inline = inlines[jj];2021if (inline.isDeleted()) {2022continue;2023}20242025if (inline.isEditing()) {2026continue;2027}20282029if (inline.isHidden()) {2030hidden.push(inline);2031continue;2032}20332034visible.push(inline);20352036if (inline.isDone()) {2037visible_done.push(inline);2038}20392040if (inline.isCollapsed()) {2041visible_collapsed.push(inline);2042}20432044if (inline.isGhost()) {2045visible_ghosts.push(inline);2046}2047}2048}20492050return {2051unsaved: unsaved,2052unsubmitted: unsubmitted,2053undone: undone,2054done: done,2055draftDone: draft_done,2056visibleDone: visible_done,2057visibleGhosts: visible_ghosts,2058visibleCollapsed: visible_collapsed,2059visible: visible,2060hidden: hidden2061};20622063},20642065_getUnsavedButton: function() {2066if (!this._unsavedButton) {2067var button = new JX.PHUIXButtonView()2068.setIcon('fa-commenting-o')2069.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);20702071var node = button.getNode();20722073var onunsaved = JX.bind(this, this._onunsavedclick);2074JX.DOM.listen(node, 'click', null, onunsaved);20752076this._unsavedButton = button;2077}20782079return this._unsavedButton;2080},20812082_getUnsubmittedButton: function() {2083if (!this._unsubmittedButton) {2084var button = new JX.PHUIXButtonView()2085.setIcon('fa-comment-o')2086.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);20872088var node = button.getNode();20892090var onunsubmitted = JX.bind(this, this._onunsubmittedclick);2091JX.DOM.listen(node, 'click', null, onunsubmitted);20922093this._unsubmittedButton = button;2094}20952096return this._unsubmittedButton;2097},20982099_getDoneButton: function() {2100if (!this._doneButton) {2101var button = new JX.PHUIXButtonView()2102.setIcon('fa-comment')2103.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);21042105var node = button.getNode();21062107var ondone = JX.bind(this, this._ondoneclick);2108JX.DOM.listen(node, 'click', null, ondone);21092110this._doneButton = button;2111}21122113return this._doneButton;2114},21152116_getMenuButton: function() {2117if (!this._menuButton) {2118var pht = this.getTranslations();21192120var button = new JX.PHUIXButtonView()2121.setIcon('fa-bars')2122.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE)2123.setAuralLabel(pht('Display Options'));21242125var dropdown = new JX.PHUIXDropdownMenu(button.getNode());2126this._menuItems = {};21272128var list = new JX.PHUIXActionListView();2129dropdown.setContent(list.getNode());21302131var map = {2132hideDone: {2133type: 'done'2134},2135hideCollapsed: {2136type: 'collapsed'2137},2138hideGhosts: {2139type: 'ghosts'2140},2141hideAll: {2142type: 'all'2143},2144showAll: {2145type: 'show'2146}2147};21482149for (var k in map) {2150var spec = map[k];21512152var handler = JX.bind(this, this._onhideinlines, spec.type);2153var item = new JX.PHUIXActionView()2154.setHandler(handler);21552156list.addItem(item);2157this._menuItems[k] = item;2158}21592160dropdown.listen('open', JX.bind(this, this._ondropdown));21612162if (this.getInlineListURI()) {2163list.addItem(2164new JX.PHUIXActionView()2165.setDivider(true));21662167list.addItem(2168new JX.PHUIXActionView()2169.setIcon('fa-external-link')2170.setName(pht('List Inline Comments'))2171.setHref(this.getInlineListURI()));2172}21732174this._menuButton = button;2175this._dropdownMenu = dropdown;2176}21772178return this._menuButton;2179},21802181_ondropdown: function() {2182var inlines = this._getInlinesByType();2183var items = this._menuItems;2184var pht = this.getTranslations();21852186items.hideDone2187.setName(pht('Hide "Done" Inlines'))2188.setDisabled(!inlines.visibleDone.length);21892190items.hideCollapsed2191.setName(pht('Hide Collapsed Inlines'))2192.setDisabled(!inlines.visibleCollapsed.length);21932194items.hideGhosts2195.setName(pht('Hide Older Inlines'))2196.setDisabled(!inlines.visibleGhosts.length);21972198items.hideAll2199.setName(pht('Hide All Inlines'))2200.setDisabled(!inlines.visible.length);22012202items.showAll2203.setName(pht('Show All Inlines'))2204.setDisabled(!inlines.hidden.length);2205},22062207_onhideinlines: function(type, e) {2208this._dropdownMenu.close();2209e.prevent();22102211this._toggleInlines(type);2212},22132214_toggleInlines: function(type) {2215var inlines = this._getInlinesByType();22162217// Clear the selection state since we end up in a weird place if the2218// user hides the selected inline.2219this._setSelectionState(null);22202221var targets;2222var mode = true;2223switch (type) {2224case 'done':2225targets = inlines.visibleDone;2226break;2227case 'collapsed':2228targets = inlines.visibleCollapsed;2229break;2230case 'ghosts':2231targets = inlines.visibleGhosts;2232break;2233case 'all':2234targets = inlines.visible;2235break;2236case 'show':2237targets = inlines.hidden;2238mode = false;2239break;2240}22412242for (var ii = 0; ii < targets.length; ii++) {2243targets[ii].setHidden(mode);2244}2245},22462247_onunsavedclick: function(e) {2248e.kill();22492250var options = {2251filter: 'comment',2252wrap: true,2253show: true,2254attribute: 'unsaved'2255};22562257this._onjumpkey(1, options);2258},22592260_onunsubmittedclick: function(e) {2261e.kill();22622263var options = {2264filter: 'comment',2265wrap: true,2266show: true,2267attribute: 'anyDraft'2268};22692270this._onjumpkey(1, options);2271},22722273_ondoneclick: function(e) {2274e.kill();22752276var options = {2277filter: 'comment',2278wrap: true,2279show: true,2280attribute: this._doneMode2281};22822283this._onjumpkey(1, options);2284},22852286_getBannerNode: function() {2287if (!this._bannerNode) {2288var attributes = {2289className: 'diff-banner',2290id: 'diff-banner'2291};22922293this._bannerNode = JX.$N('div', attributes);2294}22952296return this._bannerNode;2297},22982299_getVisibleChangeset: function() {2300if (this.isAsleep()) {2301return null;2302}23032304if (JX.Device.getDevice() != 'desktop') {2305return null;2306}23072308// Never show the banner if we're very near the top of the page.2309var margin = 480;2310var s = JX.Vector.getScroll();2311if (s.y < margin) {2312return null;2313}23142315// We're going to find the changeset which spans an invisible line a2316// little underneath the bottom of the banner. This makes the header2317// tick over from "A.txt" to "B.txt" just as "A.txt" scrolls completely2318// offscreen.2319var detect_height = 64;23202321for (var ii = 0; ii < this._changesets.length; ii++) {2322var changeset = this._changesets[ii];2323var c = changeset.getVectors();23242325// If the changeset starts above the line...2326if (c.pos.y <= (s.y + detect_height)) {2327// ...and ends below the line, this is the current visible changeset.2328if ((c.pos.y + c.dim.y) >= (s.y + detect_height)) {2329return changeset;2330}2331}2332}23332334return null;2335},23362337_getTreeView: function() {2338if (!this._treeView) {2339var tree = new JX.DiffTreeView();23402341for (var ii = 0; ii < this._changesets.length; ii++) {2342var changeset = this._changesets[ii];2343tree.addPath(changeset.getPathView());2344}23452346this._treeView = tree;2347}2348return this._treeView;2349},23502351_redrawFiletree : function() {2352var formation = this.getFormationView();23532354if (!formation) {2355return;2356}23572358var filetree = formation.getColumn(0);2359var flank = filetree.getFlank();23602361var flank_body = flank.getBodyNode();23622363var tree = this._getTreeView();2364JX.DOM.setContent(flank_body, tree.getNode());2365},23662367_setupInlineCommentListeners: function() {2368var onsave = JX.bind(this, this._onInlineEvent, 'save');2369JX.Stratcom.listen(2370['submit', 'didSyntheticSubmit'],2371'inline-edit-form',2372onsave);23732374var oncancel = JX.bind(this, this._onInlineEvent, 'cancel');2375JX.Stratcom.listen(2376'click',2377'inline-edit-cancel',2378oncancel);23792380var onundo = JX.bind(this, this._onInlineEvent, 'undo');2381JX.Stratcom.listen(2382'click',2383'differential-inline-comment-undo',2384onundo);23852386var ondone = JX.bind(this, this._onInlineEvent, 'done');2387JX.Stratcom.listen(2388'click',2389['differential-inline-comment', 'differential-inline-done'],2390ondone);23912392var ondelete = JX.bind(this, this._onInlineEvent, 'delete');2393JX.Stratcom.listen(2394'click',2395['differential-inline-comment', 'differential-inline-delete'],2396ondelete);23972398var onmenu = JX.bind(this, this._onInlineEvent, 'menu');2399JX.Stratcom.listen(2400'click',2401['differential-inline-comment', 'inline-action-dropdown'],2402onmenu);24032404var ondraft = JX.bind(this, this._onInlineEvent, 'draft');2405JX.Stratcom.listen(2406'keydown',2407['differential-inline-comment', 'tag:textarea'],2408ondraft);24092410var on_preview_view = JX.bind(this, this._onPreviewEvent, 'view');2411JX.Stratcom.listen(2412'click',2413'differential-inline-preview-jump',2414on_preview_view);2415},24162417_onPreviewEvent: function(action, e) {2418if (this.isAsleep()) {2419return;2420}24212422var data = e.getNodeData('differential-inline-preview-jump');2423var inline = this.getInlineByID(data.inlineCommentID);2424if (!inline) {2425return;2426}24272428e.kill();24292430switch (action) {2431case 'view':2432this.selectInline(inline, true, true);2433break;2434}2435},24362437_onInlineEvent: function(action, e) {2438if (this.isAsleep()) {2439return;2440}24412442if (action !== 'draft' && action !== 'menu') {2443e.kill();2444}24452446var inline = this._getInlineForEvent(e);2447var is_ref = false;24482449// If we don't have a natural inline object, the user may have clicked2450// an action (like "Delete") inside a preview element at the bottom of2451// the page.24522453// If they did, try to find an associated normal inline to act on, and2454// pretend they clicked that instead. This makes the overall state of2455// the page more consistent.24562457// However, there may be no normal inline (for example, because it is2458// on a version of the diff which is not visible). In this case, we2459// act by reference.24602461if (inline === null) {2462var data = e.getNodeData('differential-inline-comment');2463inline = this.getInlineByID(data.id);2464if (inline) {2465is_ref = true;2466} else {2467switch (action) {2468case 'delete':2469this._deleteInlineByID(data.id);2470return;2471}2472}2473}24742475// TODO: For normal operations, highlight the inline range here.24762477switch (action) {2478case 'save':2479inline.save();2480break;2481case 'cancel':2482inline.cancel();2483break;2484case 'undo':2485inline.undo();2486break;2487case 'done':2488inline.toggleDone();2489break;2490case 'delete':2491inline.delete(is_ref);2492break;2493case 'draft':2494inline.triggerDraft();2495break;2496case 'menu':2497var node = e.getNode('inline-action-dropdown');2498inline.activateMenu(node, e);2499break;2500}2501},25022503_onSelectRange: function(e) {2504this._updateSourceSelection();2505},25062507_updateSourceSelection: function() {2508var ranges = this._getSelectedRanges();25092510// In Firefox, selecting multiple rows gives us multiple ranges. In2511// Safari and Chrome, we get a single range.2512if (!ranges.length) {2513this._setSourceSelection(null, null);2514return;2515}25162517var min = 0;2518var max = ranges.length - 1;25192520var head = ranges[min].startContainer;2521var last = ranges[max].endContainer;25222523var head_loc = this._getFragmentLocation(head);2524var last_loc = this._getFragmentLocation(last);25252526if (head_loc === null || last_loc === null) {2527this._setSourceSelection(null, null);2528return;2529}25302531if (head_loc.changesetID !== last_loc.changesetID) {2532this._setSourceSelection(null, null);2533return;2534}25352536head_loc.offset += ranges[min].startOffset;2537last_loc.offset += ranges[max].endOffset;25382539this._setSourceSelection(head_loc, last_loc);2540},25412542_setSourceSelection: function(start, end) {2543var start_updated =2544!this._isSameSourceSelection(this._sourceSelectionStart, start);25452546var end_updated =2547!this._isSameSourceSelection(this._sourceSelectionEnd, end);25482549if (!start_updated && !end_updated) {2550return;2551}25522553this._sourceSelectionStart = start;2554this._sourceSelectionEnd = end;25552556if (!start) {2557this._closeSourceSelectionMenu();2558return;2559}25602561var menu;2562if (this._sourceSelectionMenu) {2563menu = this._sourceSelectionMenu;2564} else {2565menu = this._newSourceSelectionMenu();2566this._sourceSelectionMenu = menu;2567}25682569var pos = JX.$V(start.node)2570.add(0, -menu.getMenuNodeDimensions().y)2571.add(0, -24);25722573menu.setPosition(pos);2574menu.open();2575},25762577_newSourceSelectionMenu: function() {2578var pht = this.getTranslations();25792580var menu = new JX.PHUIXDropdownMenu(null)2581.setWidth(240);25822583// We need to disable autofocus for this menu, since it operates on the2584// text selection in the document. If we leave this enabled, opening the2585// menu immediately discards the selection.2586menu.setDisableAutofocus(true);25872588var list = new JX.PHUIXActionListView();2589menu.setContent(list.getNode());25902591var oncreate = JX.bind(this, this._onSourceSelectionMenuAction, 'create');25922593var comment_item = new JX.PHUIXActionView()2594.setIcon('fa-comment-o')2595.setName(pht('New Inline Comment'))2596.setKeyCommand('c')2597.setHandler(oncreate);25982599list.addItem(comment_item);26002601return menu;2602},26032604_onSourceSelectionMenuAction: function(action, e) {2605e.kill();2606this._closeSourceSelectionMenu();26072608switch (action) {2609case 'create':2610this._onKeyCreate();2611break;2612}2613},26142615_closeSourceSelectionMenu: function() {2616if (this._sourceSelectionMenu) {2617this._sourceSelectionMenu.close();2618}2619},26202621_isSameSourceSelection: function(u, v) {2622if (u === null && v === null) {2623return true;2624}26252626if (u === null && v !== null) {2627return false;2628}26292630if (u !== null && v === null) {2631return false;2632}26332634return (2635(u.changesetID === v.changesetID) &&2636(u.line === v.line) &&2637(u.displayColumn === v.displayColumn) &&2638(u.offset === v.offset)2639);2640},26412642_getFragmentLocation: function(fragment) {2643// Find the changeset containing the fragment.2644var changeset = null;2645try {2646var node = JX.DOM.findAbove(2647fragment,2648'div',2649'differential-changeset');26502651changeset = this.getChangesetForNode(node);2652if (!changeset) {2653return null;2654}2655} catch (ex) {2656return null;2657}26582659// Find the line number and display column for the fragment.2660var line = null;2661var column_count = -1;2662var has_new = false;2663var has_old = false;2664var offset = null;2665var target_node = null;2666var td;2667try {26682669// NOTE: In Safari, you can carefully select an entire line and then2670// move your mouse down slightly, causing selection of an empty2671// document fragment which is an immediate child of the next "<tr />".26722673// If the fragment is a direct child of a "<tr />" parent, assume the2674// user has done this and select the last child of the previous row2675// instead. It's possible there are other ways to do this, so this may2676// not always be the right rule.26772678// Otherwise, select the containing "<td />".26792680var is_end;2681if (JX.DOM.isType(fragment.parentNode, 'tr')) {2682// Assume this is Safari, and that the user has carefully selected a2683// row and then moved their mouse down a few pixels to select the2684// invisible fragment at the beginning of the next row.2685var cells = fragment.parentNode.previousSibling.childNodes;2686td = cells[cells.length - 1];2687is_end = true;2688} else {2689td = this._findContentCell(fragment);2690is_end = false;2691}26922693var cursor = td;2694while (cursor) {2695if (cursor.getAttribute('data-copy-mode')) {2696column_count++;2697} else {2698// In unified mode, the content column isn't currently marked2699// with an attribute, and we can't count content columns anyway.2700// Keep track of whether or not we see a "NL" (New Line) column2701// and/or an "OL" (Old Line) column to try to puzzle out which2702// side of the display change we're on.27032704if (cursor.id.match(/NL/)) {2705has_new = true;2706} else if (cursor.id.match(/OL/)) {2707has_old = true;2708}2709}27102711var n = parseInt(cursor.getAttribute('data-n'));27122713if (n) {2714if (line === null) {2715target_node = cursor;2716line = n;2717}2718}27192720cursor = cursor.previousSibling;2721}27222723if (!line) {2724return null;2725}27262727if (column_count < 0) {2728if (has_new || has_old) {2729if (has_new) {2730column_count = 1;2731} else {2732column_count = 0;2733}2734} else {2735return null;2736}2737}27382739var info = this._getSelectionOffset(td, fragment);27402741if (info.found) {2742offset = info.offset;2743} else {2744if (is_end) {2745offset = info.offset;2746} else {2747offset = 0;2748}2749}2750} catch (ex) {2751return null;2752}27532754var changeset_id;2755if (column_count > 0) {2756changeset_id = changeset.getRightChangesetID();2757} else {2758changeset_id = changeset.getLeftChangesetID();2759}27602761return {2762node: td,2763changeset: changeset,2764changesetID: changeset_id,2765line: line,2766displayColumn: column_count,2767offset: offset,2768targetNode: target_node2769};2770},27712772_getSelectionOffset: function(node, target) {2773// If this is an aural hint node in a unified diff, ignore it when2774// calculating the selection offset.2775if (node.getAttribute && node.getAttribute('data-aural')) {2776return {2777offset: 0,2778content: '',2779found: false2780};2781}27822783if (!node.childNodes || !node.childNodes.length) {2784return {2785offset: node.textContent.length,2786content: node.textContent,2787found: false2788};2789}27902791var found = false;2792var offset = 0;2793var content = '';2794for (var ii = 0; ii < node.childNodes.length; ii++) {2795var child = node.childNodes[ii];27962797if (child === target) {2798found = true;2799}28002801var spec = this._getSelectionOffset(child, target);28022803content += spec.content;2804if (!found) {2805offset += spec.offset;2806}28072808found = found || spec.found;2809}28102811return {2812offset: offset,2813content: content,2814found: found2815};2816},28172818_getSelectedRanges: function() {2819var ranges = [];28202821if (!window.getSelection) {2822return ranges;2823}28242825var selection = window.getSelection();2826for (var ii = 0; ii < selection.rangeCount; ii++) {2827var range = selection.getRangeAt(ii);2828if (range.collapsed) {2829continue;2830}28312832ranges.push(range);2833}28342835return ranges;2836},28372838_isContentCell: function(node) {2839return !!node.getAttribute('data-copy-mode');2840},28412842_findContentCell: function(node) {2843var cursor = node;2844while (true) {2845cursor = JX.DOM.findAbove(cursor, 'td');2846if (this._isContentCell(cursor)) {2847return cursor;2848}2849}2850}28512852}28532854});285528562857