Path: blob/master/webroot/rsrc/js/application/diff/DiffInline.js
12242 views
/**1* @provides phabricator-diff-inline2* @requires javelin-dom3* phabricator-diff-inline-content-state4* @javelin5*/67JX.install('DiffInline', {89construct : function() {10this._state = {};11},1213members: {14_id: null,15_phid: null,16_changesetID: null,17_row: null,18_number: null,19_length: null,20_displaySide: null,21_isNewFile: null,22_replyToCommentPHID: null,23_snippet: null,24_menuItems: null,25_documentEngineKey: null,2627_isDeleted: false,28_isInvisible: false,29_isLoading: false,3031_changeset: null,3233_isCollapsed: false,34_isDraft: null,35_isDraftDone: null,36_isFixed: null,37_isEditing: false,38_isNew: false,39_isSynthetic: false,40_isHidden: false,4142_editRow: null,43_undoRow: null,44_undoType: null,45_undoState: null,4647_draftRequest: null,48_skipFocus: false,49_menu: null,5051_startOffset: null,52_endOffset: null,53_isSelected: false,54_canSuggestEdit: false,5556_state: null,5758bindToRow: function(row) {59this._row = row;6061var row_data = JX.Stratcom.getData(row);62row_data.inline = this;63this._isCollapsed = row_data.hidden || false;6465// TODO: Get smarter about this once we do more editing, this is pretty66// hacky.67var comment = JX.DOM.find(row, 'div', 'differential-inline-comment');68var data = JX.Stratcom.getData(comment);6970this._readInlineState(data);71this._phid = data.phid;7273if (data.on_right) {74this._displaySide = 'right';75} else {76this._displaySide = 'left';77}7879this._number = parseInt(data.number, 10);80this._length = parseInt(data.length, 10);8182this._isNewFile = data.isNewFile;8384this._replyToCommentPHID = data.replyToCommentPHID;8586this._isDraft = data.isDraft;87this._isFixed = data.isFixed;88this._isGhost = data.isGhost;89this._isSynthetic = data.isSynthetic;90this._isDraftDone = data.isDraftDone;9192this._changesetID = data.changesetID;93this._isNew = false;94this._snippet = data.snippet;95this._menuItems = data.menuItems;96this._documentEngineKey = data.documentEngineKey;9798this._startOffset = data.startOffset;99this._endOffset = data.endOffset;100101this._isEditing = data.isEditing;102103if (this._isEditing) {104// NOTE: The "original" shipped down in the DOM may reflect a draft105// which we're currently editing. This flow is a little clumsy, but106// reasonable until some future change moves away from "send down107// the inline, then immediately click edit".108this.edit(null, true);109} else {110this.setInvisible(false);111}112113this._startDrafts();114115return this;116},117118isDraft: function() {119return this._isDraft;120},121122isDone: function() {123return this._isFixed;124},125126isEditing: function() {127return this._isEditing;128},129130isUndo: function() {131return !!this._undoRow;132},133134isDeleted: function() {135return this._isDeleted;136},137138isSynthetic: function() {139return this._isSynthetic;140},141142isDraftDone: function() {143return this._isDraftDone;144},145146isHidden: function() {147return this._isHidden;148},149150isGhost: function() {151return this._isGhost;152},153154getStartOffset: function() {155return this._startOffset;156},157158getEndOffset: function() {159return this._endOffset;160},161162setIsSelected: function(is_selected) {163this._isSelected = is_selected;164165if (this._row) {166JX.DOM.alterClass(167this._row,168'inline-comment-selected',169this._isSelected);170}171172return this;173},174175bindToRange: function(data) {176this._displaySide = data.displaySide;177this._number = parseInt(data.number, 10);178this._length = parseInt(data.length, 10);179this._isNewFile = data.isNewFile;180this._changesetID = data.changesetID;181this._isNew = true;182183if (data.hasOwnProperty('startOffset')) {184this._startOffset = data.startOffset;185} else {186this._startOffset = null;187}188189if (data.hasOwnProperty('endOffset')) {190this._endOffset = data.endOffset;191} else {192this._endOffset = null;193}194195// Insert the comment after any other comments which already appear on196// the same row.197var parent_row = JX.DOM.findAbove(data.target, 'tr');198var target_row = parent_row.nextSibling;199while (target_row && JX.Stratcom.hasSigil(target_row, 'inline-row')) {200target_row = target_row.nextSibling;201}202203var row = this._newRow();204parent_row.parentNode.insertBefore(row, target_row);205206this.setInvisible(true);207this._startDrafts();208209return this;210},211212bindToReply: function(inline) {213this._displaySide = inline._displaySide;214this._number = inline._number;215this._length = inline._length;216this._isNewFile = inline._isNewFile;217this._changesetID = inline._changesetID;218this._isNew = true;219this._documentEngineKey = inline._documentEngineKey;220221this._replyToCommentPHID = inline._phid;222223var changeset = this.getChangeset();224225// We're going to figure out where in the document to position the new226// inline. Normally, it goes after any existing inline rows (so if227// several inlines reply to the same line, they appear in chronological228// order).229230// However: if inlines are threaded, we want to put the new inline in231// the right place in the thread. This might be somewhere in the middle,232// so we need to do a bit more work to figure it out.233234// To find the right place in the thread, we're going to look for any235// inline which is at or above the level of the comment we're replying236// to. This means we've reached a new fork of the thread, and should237// put our new inline before the comment we found.238var ancestor_map = {};239var ancestor = inline;240var reply_phid;241while (ancestor) {242reply_phid = ancestor.getReplyToCommentPHID();243if (!reply_phid) {244break;245}246ancestor_map[reply_phid] = true;247ancestor = changeset.getInlineByPHID(reply_phid);248}249250var parent_row = inline._row;251var target_row = parent_row.nextSibling;252while (target_row && JX.Stratcom.hasSigil(target_row, 'inline-row')) {253var target = changeset.getInlineForRow(target_row);254reply_phid = target.getReplyToCommentPHID();255256// If we found an inline which is replying directly to some ancestor257// of this new comment, this is where the new rows go.258if (ancestor_map.hasOwnProperty(reply_phid)) {259break;260}261262target_row = target_row.nextSibling;263}264265var row = this._newRow();266parent_row.parentNode.insertBefore(row, target_row);267268this.setInvisible(true);269this._startDrafts();270271return this;272},273274setChangeset: function(changeset) {275this._changeset = changeset;276return this;277},278279getChangeset: function() {280return this._changeset;281},282283setEditing: function(editing) {284this._isEditing = editing;285return this;286},287288setHidden: function(hidden) {289this._isHidden = hidden;290this._redraw();291return this;292},293294canReply: function() {295return this._hasMenuAction('reply');296},297298canEdit: function() {299return this._hasMenuAction('edit');300},301302canDone: function() {303if (!JX.DOM.scry(this._row, 'input', 'differential-inline-done').length) {304return false;305}306307return true;308},309310canCollapse: function() {311return this._hasMenuAction('collapse');312},313314_newRow: function() {315var attributes = {316sigil: 'inline-row'317};318319var row = JX.$N('tr', attributes);320321JX.Stratcom.getData(row).inline = this;322this._row = row;323324this._id = null;325this._phid = null;326this._isCollapsed = false;327328return row;329},330331setCollapsed: function(collapsed) {332this._closeMenu();333334this._isCollapsed = collapsed;335336var op;337if (collapsed) {338op = 'hide';339} else {340op = 'show';341}342343var inline_uri = this._getInlineURI();344var comment_id = this._id;345346new JX.Workflow(inline_uri, {op: op, ids: comment_id})347.setHandler(JX.bag)348.start();349350this._redraw();351this._didUpdate(true);352},353354isCollapsed: function() {355return this._isCollapsed;356},357358toggleDone: function() {359var uri = this._getInlineURI();360var data = {361op: 'done',362id: this._id363};364365var ondone = JX.bind(this, this._ondone);366367new JX.Workflow(uri, data)368.setHandler(ondone)369.start();370},371372_ondone: function(response) {373var checkbox = JX.DOM.find(374this._row,375'input',376'differential-inline-done');377378checkbox.checked = (response.isChecked ? 'checked' : null);379380var comment = JX.DOM.findAbove(381checkbox,382'div',383'differential-inline-comment');384385JX.DOM.alterClass(comment, 'inline-is-done', response.isChecked);386387// NOTE: This is marking the inline as having an unsubmitted checkmark,388// as opposed to a submitted checkmark. This is different from the389// top-level "draft" state of unsubmitted comments.390JX.DOM.alterClass(comment, 'inline-state-is-draft', response.draftState);391392this._isFixed = response.isChecked;393this._isDraftDone = !!response.draftState;394395this._didUpdate();396},397398create: function(content_state) {399var changeset = this.getChangeset();400if (!this._documentEngineKey) {401this._documentEngineKey = changeset.getResponseDocumentEngineKey();402}403404var uri = this._getInlineURI();405var handler = JX.bind(this, this._oncreateresponse);406var data = this._newRequestData('new', content_state);407408this.setLoading(true);409410new JX.Request(uri, handler)411.setData(data)412.send();413},414415reply: function(with_quote) {416this._closeMenu();417418var content_state = this._newContentState();419if (with_quote) {420var text = this._getActiveContentState().getTextForQuote();421content_state.text = text;422}423424var changeset = this.getChangeset();425return changeset.newInlineReply(this, content_state);426},427428edit: function(content_state, skip_focus) {429this._closeMenu();430431this._skipFocus = !!skip_focus;432433// If you edit an inline ("A"), modify the text ("AB"), cancel, and then434// edit it again: discard the undo state ("AB"). Otherwise we end up435// with an open editor and an active "Undo" link, which is weird.436437if (this._undoRow) {438JX.DOM.remove(this._undoRow);439this._undoRow = null;440441this._undoType = null;442this._undoText = null;443}444445this._applyEdit(content_state);446},447448delete: function(is_ref) {449var uri = this._getInlineURI();450var handler = JX.bind(this, this._ondeleteresponse, false);451452// NOTE: This may be a direct delete (the user clicked on the inline453// itself) or a "refdelete" (the user clicked somewhere else, like the454// preview, but the inline is present on the page).455456// For a "refdelete", we prompt the user to confirm that they want to457// delete the comment, because they can not undo deletions from the458// preview. We could jump the user to the inline instead, but this would459// be somewhat disruptive and make deleting several comments more460// difficult.461462var op;463if (is_ref) {464op = 'refdelete';465} else {466op = 'delete';467}468469var data = this._newRequestData(op);470471this.setLoading(true);472473new JX.Workflow(uri, data)474.setHandler(handler)475.start();476},477478getDisplaySide: function() {479return this._displaySide;480},481482getLineNumber: function() {483return this._number;484},485486getLineLength: function() {487return this._length;488},489490isNewFile: function() {491return this._isNewFile;492},493494getID: function() {495return this._id;496},497498getPHID: function() {499return this._phid;500},501502getChangesetID: function() {503return this._changesetID;504},505506getReplyToCommentPHID: function() {507return this._replyToCommentPHID;508},509510setDeleted: function(deleted) {511this._isDeleted = deleted;512this._redraw();513return this;514},515516setInvisible: function(invisible) {517this._isInvisible = invisible;518this._redraw();519return this;520},521522setLoading: function(loading) {523this._isLoading = loading;524this._redraw();525return this;526},527528_newRequestData: function(operation, content_state) {529var data = {530op: operation,531is_new: this.isNewFile(),532on_right: ((this.getDisplaySide() == 'right') ? 1 : 0),533renderer: this.getChangeset().getRendererKey()534};535536if (operation === 'new') {537var create_data = {538changesetID: this.getChangesetID(),539documentEngineKey: this._documentEngineKey,540replyToCommentPHID: this.getReplyToCommentPHID(),541startOffset: this._startOffset,542endOffset: this._endOffset,543number: this.getLineNumber(),544length: this.getLineLength()545};546547JX.copy(data, create_data);548} else {549var edit_data = {550id: this._id551};552553JX.copy(data, edit_data);554}555556if (content_state) {557data.hasContentState = 1;558JX.copy(data, content_state);559}560561return data;562},563564_oneditresponse: function(response) {565var rows = JX.$H(response.view).getNode();566567this._readInlineState(response.inline);568this._drawEditRows(rows);569570this.setInvisible(true);571},572573_oncreateresponse: function(response) {574var rows = JX.$H(response.view).getNode();575576this._readInlineState(response.inline);577this._drawEditRows(rows);578},579580_readInlineState: function(state) {581this._id = state.id;582583this._state = {584initial: this._newContentStateFromWireFormat(state.state.initial),585committed: this._newContentStateFromWireFormat(state.state.committed),586active: this._newContentStateFromWireFormat(state.state.active)587};588589this._canSuggestEdit = state.canSuggestEdit;590},591592_newContentStateFromWireFormat: function(map) {593if (map === null) {594return null;595}596597return new JX.DiffInlineContentState().readWireFormat(map);598},599600_ondeleteresponse: function(prevent_undo) {601if (!prevent_undo) {602// If there's an existing "unedit" undo element, remove it.603if (this._undoRow) {604JX.DOM.remove(this._undoRow);605this._undoRow = null;606}607608// If there's an existing editor, remove it. This happens when you609// delete a comment from the comment preview area. In this case, we610// read and preserve the text so "Undo" restores it.611var state = null;612if (this._editRow) {613state = this._getActiveContentState().getWireFormat();614JX.DOM.remove(this._editRow);615this._editRow = null;616}617618this._drawUndeleteRows(state);619}620621this.setLoading(false);622this.setDeleted(true);623624this._didUpdate();625},626627_drawUndeleteRows: function(content_state) {628this._undoType = 'undelete';629this._undoState = content_state || null;630631return this._drawUndoRows('undelete', this._row);632},633634_drawUneditRows: function(content_state) {635this._undoType = 'unedit';636this._undoState = content_state;637638return this._drawUndoRows('unedit', null);639},640641_drawUndoRows: function(mode, cursor) {642var templates = this.getChangeset().getUndoTemplates();643644var template;645if (this.getDisplaySide() == 'right') {646template = templates.r;647} else {648template = templates.l;649}650template = JX.$H(template).getNode();651652this._undoRow = this._drawRows(template, cursor, mode);653},654655_drawContentRows: function(rows) {656return this._drawRows(rows, null, 'content');657},658659_drawEditRows: function(rows) {660this.setEditing(true);661this._editRow = this._drawRows(rows, null, 'edit');662663this._drawSuggestionState(this._editRow);664665// TODO: We're just doing this for the rendering side effect of drawing666// the button text.667this.setHasSuggestion(this.getHasSuggestion());668},669670_drawRows: function(rows, cursor, type) {671var first_row = JX.DOM.scry(rows, 'tr')[0];672var row = first_row;673var anchor = cursor || this._row;674cursor = cursor || this._row.nextSibling;675676var result_row;677var next_row;678while (row) {679// Grab this first, since it's going to change once we insert the row680// into the document.681next_row = row.nextSibling;682683// Bind edit and undo rows to this DiffInline object so that684// interactions like hovering work properly.685JX.Stratcom.getData(row).inline = this;686687anchor.parentNode.insertBefore(row, cursor);688cursor = row;689690if (!result_row) {691result_row = row;692}693694if (!this._skipFocus) {695// If the row has a textarea, focus it. This allows the user to start696// typing a comment immediately after a "new", "edit", or "reply"697// action.698699// (When simulating an "edit" on page load, we don't do this.)700701var textareas = JX.DOM.scry(702row,703'textarea',704'inline-content-text');705if (textareas.length) {706var area = textareas[0];707area.focus();708709var length = area.value.length;710JX.TextAreaUtils.setSelectionRange(area, length, length);711}712}713714row = next_row;715}716717JX.Stratcom.invoke('resize');718719return result_row;720},721722_drawSuggestionState: function(row) {723if (this._canSuggestEdit) {724var button = this._getSuggestionButton();725var node = button.getNode();726727// As a side effect of form submission, the button may become728// visually disabled. Re-enable it. This is a bit hacky.729JX.DOM.alterClass(node, 'disabled', false);730node.disabled = false;731732var container = JX.DOM.find(row, 'div', 'inline-edit-buttons');733container.appendChild(node);734}735},736737_getSuggestionButton: function() {738if (!this._suggestionButton) {739var button = new JX.PHUIXButtonView()740.setIcon('fa-pencil-square-o')741.setColor('grey');742743var node = button.getNode();744JX.DOM.alterClass(node, 'inline-button-left', true);745746var onclick = JX.bind(this, this._onSuggestEdit);747JX.DOM.listen(node, 'click', null, onclick);748749this._suggestionButton = button;750}751752return this._suggestionButton;753},754755_onSuggestEdit: function(e) {756e.kill();757758this.setHasSuggestion(!this.getHasSuggestion());759760// Resize the suggestion input for size of the text.761if (this.getHasSuggestion()) {762if (this._editRow) {763var node = this._getSuggestionNode(this._editRow);764if (node) {765node.rows = Math.max(3, node.value.split('\n').length);766}767}768}769770// Save the "hasSuggestion" part of the content state.771this.triggerDraft();772},773774_getActiveContentState: function() {775var state = this._state.active;776777if (this._editRow) {778state.readForm(this._editRow);779}780781return state;782},783784_getCommittedContentState: function() {785return this._state.committed;786},787788_getInitialContentState: function() {789return this._state.initial;790},791792setHasSuggestion: function(has_suggestion) {793var state = this._getActiveContentState();794state.setHasSuggestion(has_suggestion);795796var button = this._getSuggestionButton();797var pht = this.getChangeset().getChangesetList().getTranslations();798if (has_suggestion) {799button800.setIcon('fa-times')801.setText(pht('Discard Edit'));802} else {803button804.setIcon('fa-plus')805.setText(pht('Suggest Edit'));806}807808if (this._editRow) {809JX.DOM.alterClass(this._editRow, 'has-suggestion', has_suggestion);810}811},812813getHasSuggestion: function() {814return this._getActiveContentState().getHasSuggestion();815},816817save: function() {818if (this._shouldDeleteOnSave()) {819JX.DOM.remove(this._editRow);820this._editRow = null;821822this._applyDelete(true);823return;824}825826this._applySave();827},828829_shouldDeleteOnSave: function() {830var active = this._getActiveContentState();831var initial = this._getInitialContentState();832833// When a user clicks "Save", it counts as a "delete" if the content834// of the comment is functionally empty.835836// This isn't a delete if there's any text. Even if the text is a837// quote (so the state is the same as the initial state), we preserve838// it when the user clicks "Save".839if (!active.isTextEmpty()) {840return false;841}842843// This isn't a delete if there's a suggestion and that suggestion is844// different from the initial state. (This means that an inline which845// purely suggests a block of code should be deleted is non-empty.)846if (active.getHasSuggestion()) {847if (!active.isSuggestionSimilar(initial)) {848return false;849}850}851852// Otherwise, this comment is functionally empty, so we can just treat853// a "Save" as a "delete".854return true;855},856857_shouldUndoOnCancel: function() {858var committed = this._getCommittedContentState();859var active = this._getActiveContentState();860var initial = this._getInitialContentState();861862// When a user clicks "Cancel", we only offer to let them "Undo" the863// action if the undo would be substantive.864865// The undo is substantive if the text is nonempty, and not similar to866// the last state.867var versus = committed || initial;868if (!active.isTextEmpty() && !active.isTextSimilar(versus)) {869return true;870}871872// The undo is substantive if there's a suggestion, and the suggestion873// is not similar to the last state.874if (active.getHasSuggestion()) {875if (!active.isSuggestionSimilar(versus)) {876return true;877}878}879880return false;881},882883_applySave: function() {884var handler = JX.bind(this, this._onsaveresponse);885886var state = this._getActiveContentState();887var data = this._newRequestData('save', state.getWireFormat());888889this._applyCall(handler, data);890},891892_applyDelete: function(prevent_undo) {893var handler = JX.bind(this, this._ondeleteresponse, prevent_undo);894895var data = this._newRequestData('delete');896897this._applyCall(handler, data);898},899900_applyCancel: function(state) {901var handler = JX.bind(this, this._onCancelResponse);902903var data = this._newRequestData('cancel', state);904905this._applyCall(handler, data);906},907908_applyEdit: function(state) {909var handler = JX.bind(this, this._oneditresponse);910911var data = this._newRequestData('edit', state);912913this._applyCall(handler, data);914},915916_applyCall: function(handler, data) {917var uri = this._getInlineURI();918919var callback = JX.bind(this, function() {920this.setLoading(false);921handler.apply(null, arguments);922});923924this.setLoading(true);925926new JX.Workflow(uri, data)927.setHandler(callback)928.start();929},930931undo: function() {932JX.DOM.remove(this._undoRow);933this._undoRow = null;934935if (this._undoType === 'undelete') {936var uri = this._getInlineURI();937var data = this._newRequestData('undelete');938var handler = JX.bind(this, this._onundelete);939940this.setDeleted(false);941this.setLoading(true);942943new JX.Request(uri, handler)944.setData(data)945.send();946}947948if (this._undoState !== null) {949this.edit(this._undoState);950}951},952953_onundelete: function() {954this.setLoading(false);955this._didUpdate();956},957958cancel: function() {959// NOTE: Read the state before we remove the editor. Otherwise, we might960// miss text the user has entered into the textarea.961var state = this._getActiveContentState().getWireFormat();962963JX.DOM.remove(this._editRow);964this._editRow = null;965966// When a user clicks "Cancel", we delete the comment if it has never967// been saved: we don't have a non-empty display state to revert to.968var is_delete = (this._getCommittedContentState() === null);969970var is_undo = this._shouldUndoOnCancel();971972// If you "undo" to restore text ("AB") and then "Cancel", we put you973// back in the original text state ("A"). We also send the original974// text ("A") to the server as the current persistent state.975976if (is_undo) {977this._drawUneditRows(state);978}979980if (is_delete) {981// NOTE: We're always suppressing the undo from "delete". We want to982// use the "undo" we just added above instead, which will get us983// back to the ephemeral, client-side editor state.984this._applyDelete(true);985} else {986this.setEditing(false);987this.setInvisible(false);988989var old_state = this._getCommittedContentState();990this._applyCancel(old_state.getWireFormat());991992this._didUpdate(true);993}994},995996_onCancelResponse: function(response) {997// Nothing to do.998},9991000_getSuggestionNode: function(row) {1001try {1002return JX.DOM.find(row, 'textarea', 'inline-content-suggestion');1003} catch (ex) {1004return null;1005}1006},10071008_onsaveresponse: function(response) {1009if (this._editRow) {1010JX.DOM.remove(this._editRow);1011this._editRow = null;1012}10131014this.setEditing(false);1015this.setInvisible(false);10161017var new_row = this._drawContentRows(JX.$H(response.view).getNode());1018JX.DOM.remove(this._row);1019this.bindToRow(new_row);10201021this._didUpdate();1022},10231024_didUpdate: function(local_only) {1025// After making changes to inline comments, refresh the transaction1026// preview at the bottom of the page.1027if (!local_only) {1028this.getChangeset().getChangesetList().redrawPreview();1029}10301031this.getChangeset().getChangesetList().redrawCursor();1032this.getChangeset().getChangesetList().resetHover();10331034// Emit a resize event so that UI elements like the keyboard focus1035// reticle can redraw properly.1036JX.Stratcom.invoke('resize');1037},10381039_redraw: function() {1040var is_invisible =1041(this._isInvisible || this._isDeleted || this._isHidden);1042var is_loading = this._isLoading;1043var is_collapsed = (this._isCollapsed && !this._isHidden);10441045var row = this._row;1046JX.DOM.alterClass(row, 'differential-inline-hidden', is_invisible);1047JX.DOM.alterClass(row, 'differential-inline-loading', is_loading);1048JX.DOM.alterClass(row, 'inline-hidden', is_collapsed);1049},10501051_getInlineURI: function() {1052var changeset = this.getChangeset();1053var list = changeset.getChangesetList();1054return list.getInlineURI();1055},10561057_startDrafts: function() {1058if (this._draftRequest) {1059return;1060}10611062var onresponse = JX.bind(this, this._onDraftResponse);1063var draft = JX.bind(this, this._getDraftState);10641065var uri = this._getInlineURI();1066var request = new JX.PhabricatorShapedRequest(uri, onresponse, draft);10671068// The main transaction code uses a 500ms delay on desktop and a1069// 10s delay on mobile. Perhaps this should be standardized.1070request.setRateLimit(2000);10711072this._draftRequest = request;10731074request.start();1075},10761077_onDraftResponse: function() {1078// For now, do nothing.1079},10801081_getDraftState: function() {1082if (this.isDeleted()) {1083return null;1084}10851086if (!this.isEditing()) {1087return null;1088}10891090var state = this._getActiveContentState();1091if (state.isStateEmpty()) {1092return null;1093}10941095var draft_data = {1096op: 'draft',1097id: this.getID(),1098};10991100JX.copy(draft_data, state.getWireFormat());11011102return draft_data;1103},11041105triggerDraft: function() {1106if (this._draftRequest) {1107this._draftRequest.trigger();1108}1109},11101111activateMenu: function(button, e) {1112// If we already have a menu for this button, let the menu handle the1113// event.1114var data = JX.Stratcom.getData(button);1115if (data.menu) {1116return;1117}11181119e.prevent();11201121var menu = new JX.PHUIXDropdownMenu(button)1122.setWidth(240);11231124var list = new JX.PHUIXActionListView();1125var items = this._newMenuItems(menu);1126for (var ii = 0; ii < items.length; ii++) {1127list.addItem(items[ii]);1128}11291130menu.setContent(list.getNode());11311132data.menu = menu;1133this._menu = menu;11341135menu.listen('open', JX.bind(this, function() {1136var changeset_list = this.getChangeset().getChangesetList();1137changeset_list.selectInline(this, true);1138}));11391140menu.open();1141},11421143_newMenuItems: function(menu) {1144var items = [];11451146for (var ii = 0; ii < this._menuItems.length; ii++) {1147var spec = this._menuItems[ii];11481149var onmenu = JX.bind(this, this._onMenuItem, menu, spec.action, spec);11501151var item = new JX.PHUIXActionView()1152.setIcon(spec.icon)1153.setName(spec.label)1154.setHandler(onmenu);11551156if (spec.key) {1157item.setKeyCommand(spec.key);1158}11591160items.push(item);1161}11621163return items;1164},11651166_onMenuItem: function(menu, action, spec, e) {1167e.prevent();1168menu.close();11691170switch (action) {1171case 'reply':1172this.reply();1173break;1174case 'quote':1175this.reply(true);1176break;1177case 'collapse':1178this.setCollapsed(true);1179break;1180case 'delete':1181this.delete();1182break;1183case 'edit':1184this.edit();1185break;1186case 'raw':1187new JX.Workflow(spec.uri)1188.start();1189break;1190}11911192},11931194_hasMenuAction: function(action) {1195for (var ii = 0; ii < this._menuItems.length; ii++) {1196var spec = this._menuItems[ii];1197if (spec.action === action) {1198return true;1199}1200}1201return false;1202},12031204_closeMenu: function() {1205if (this._menu) {1206this._menu.close();1207}1208},12091210_newContentState: function() {1211return {1212text: '',1213suggestionText: '',1214hasSuggestion: false1215};1216}12171218}12191220});122112221223