Changeset View
Changeset View
Standalone View
Standalone View
webroot/rsrc/js/application/diff/DiffInline.js
Show All 38 Lines | members: { | ||||
_isNew: false, | _isNew: false, | ||||
_isSynthetic: false, | _isSynthetic: false, | ||||
_isHidden: false, | _isHidden: false, | ||||
_editRow: null, | _editRow: null, | ||||
_undoRow: null, | _undoRow: null, | ||||
_undoType: null, | _undoType: null, | ||||
_undoState: null, | _undoState: null, | ||||
_preventUndo: false, | |||||
_draftRequest: null, | _draftRequest: null, | ||||
_skipFocus: false, | _skipFocus: false, | ||||
_menu: null, | _menu: null, | ||||
_startOffset: null, | _startOffset: null, | ||||
_endOffset: null, | _endOffset: null, | ||||
_isSelected: false, | _isSelected: false, | ||||
▲ Show 20 Lines • Show All 100 Lines • ▼ Show 20 Lines | members: { | ||||
getStartOffset: function() { | getStartOffset: function() { | ||||
return this._startOffset; | return this._startOffset; | ||||
}, | }, | ||||
getEndOffset: function() { | getEndOffset: function() { | ||||
return this._endOffset; | return this._endOffset; | ||||
}, | }, | ||||
_setPreventUndo: function(prevent_undo) { | |||||
this._preventUndo = prevent_undo; | |||||
}, | |||||
_getPreventUndo: function() { | |||||
return this._preventUndo; | |||||
}, | |||||
setIsSelected: function(is_selected) { | setIsSelected: function(is_selected) { | ||||
this._isSelected = is_selected; | this._isSelected = is_selected; | ||||
if (this._row) { | if (this._row) { | ||||
JX.DOM.alterClass( | JX.DOM.alterClass( | ||||
this._row, | this._row, | ||||
'inline-comment-selected', | 'inline-comment-selected', | ||||
this._isSelected); | this._isSelected); | ||||
▲ Show 20 Lines • Show All 267 Lines • ▼ Show 20 Lines | edit: function(content_state, skip_focus) { | ||||
if (this._undoRow) { | if (this._undoRow) { | ||||
JX.DOM.remove(this._undoRow); | JX.DOM.remove(this._undoRow); | ||||
this._undoRow = null; | this._undoRow = null; | ||||
this._undoType = null; | this._undoType = null; | ||||
this._undoText = null; | this._undoText = null; | ||||
} | } | ||||
var uri = this._getInlineURI(); | this._applyEdit(content_state); | ||||
var handler = JX.bind(this, this._oneditresponse); | |||||
var data = this._newRequestData('edit', content_state); | |||||
this.setLoading(true); | |||||
new JX.Request(uri, handler) | |||||
.setData(data) | |||||
.send(); | |||||
}, | }, | ||||
delete: function(is_ref) { | delete: function(is_ref) { | ||||
var uri = this._getInlineURI(); | var uri = this._getInlineURI(); | ||||
var handler = JX.bind(this, this._ondeleteresponse); | var handler = JX.bind(this, this._ondeleteresponse, false); | ||||
// NOTE: This may be a direct delete (the user clicked on the inline | // NOTE: This may be a direct delete (the user clicked on the inline | ||||
// itself) or a "refdelete" (the user clicked somewhere else, like the | // itself) or a "refdelete" (the user clicked somewhere else, like the | ||||
// preview, but the inline is present on the page). | // preview, but the inline is present on the page). | ||||
// For a "refdelete", we prompt the user to confirm that they want to | // For a "refdelete", we prompt the user to confirm that they want to | ||||
// delete the comment, because they can not undo deletions from the | // delete the comment, because they can not undo deletions from the | ||||
// preview. We could jump the user to the inline instead, but this would | // preview. We could jump the user to the inline instead, but this would | ||||
▲ Show 20 Lines • Show All 103 Lines • ▼ Show 20 Lines | members: { | ||||
}, | }, | ||||
_oneditresponse: function(response) { | _oneditresponse: function(response) { | ||||
var rows = JX.$H(response.view).getNode(); | var rows = JX.$H(response.view).getNode(); | ||||
this._readInlineState(response.inline); | this._readInlineState(response.inline); | ||||
this._drawEditRows(rows); | this._drawEditRows(rows); | ||||
this.setLoading(false); | |||||
this.setInvisible(true); | this.setInvisible(true); | ||||
}, | }, | ||||
_oncreateresponse: function(response) { | _oncreateresponse: function(response) { | ||||
var rows = JX.$H(response.view).getNode(); | var rows = JX.$H(response.view).getNode(); | ||||
this._readInlineState(response.inline); | this._readInlineState(response.inline); | ||||
this._drawEditRows(rows); | this._drawEditRows(rows); | ||||
Show All 14 Lines | members: { | ||||
_newContentStateFromWireFormat: function(map) { | _newContentStateFromWireFormat: function(map) { | ||||
if (map === null) { | if (map === null) { | ||||
return null; | return null; | ||||
} | } | ||||
return new JX.DiffInlineContentState().readWireFormat(map); | return new JX.DiffInlineContentState().readWireFormat(map); | ||||
}, | }, | ||||
_ondeleteresponse: function() { | _ondeleteresponse: function(prevent_undo) { | ||||
if (!prevent_undo) { | |||||
// If there's an existing "unedit" undo element, remove it. | // If there's an existing "unedit" undo element, remove it. | ||||
if (this._undoRow) { | if (this._undoRow) { | ||||
JX.DOM.remove(this._undoRow); | JX.DOM.remove(this._undoRow); | ||||
this._undoRow = null; | this._undoRow = null; | ||||
} | } | ||||
// If there's an existing editor, remove it. This happens when you | // If there's an existing editor, remove it. This happens when you | ||||
// delete a comment from the comment preview area. In this case, we | // delete a comment from the comment preview area. In this case, we | ||||
// read and preserve the text so "Undo" restores it. | // read and preserve the text so "Undo" restores it. | ||||
var state = null; | var state = null; | ||||
if (this._editRow) { | if (this._editRow) { | ||||
state = this._getActiveContentState().getWireFormat(); | state = this._getActiveContentState().getWireFormat(); | ||||
JX.DOM.remove(this._editRow); | JX.DOM.remove(this._editRow); | ||||
this._editRow = null; | this._editRow = null; | ||||
} | } | ||||
if (!this._getPreventUndo()) { | |||||
this._drawUndeleteRows(state); | this._drawUndeleteRows(state); | ||||
} | } | ||||
this.setLoading(false); | this.setLoading(false); | ||||
this.setDeleted(true); | this.setDeleted(true); | ||||
this._didUpdate(); | this._didUpdate(); | ||||
}, | }, | ||||
▲ Show 20 Lines • Show All 42 Lines • ▼ Show 20 Lines | members: { | ||||
}, | }, | ||||
_drawRows: function(rows, cursor, type) { | _drawRows: function(rows, cursor, type) { | ||||
var first_row = JX.DOM.scry(rows, 'tr')[0]; | var first_row = JX.DOM.scry(rows, 'tr')[0]; | ||||
var row = first_row; | var row = first_row; | ||||
var anchor = cursor || this._row; | var anchor = cursor || this._row; | ||||
cursor = cursor || this._row.nextSibling; | cursor = cursor || this._row.nextSibling; | ||||
var result_row; | var result_row; | ||||
var next_row; | var next_row; | ||||
while (row) { | while (row) { | ||||
// Grab this first, since it's going to change once we insert the row | // Grab this first, since it's going to change once we insert the row | ||||
// into the document. | // into the document. | ||||
next_row = row.nextSibling; | next_row = row.nextSibling; | ||||
// Bind edit and undo rows to this DiffInline object so that | // Bind edit and undo rows to this DiffInline object so that | ||||
▲ Show 20 Lines • Show All 127 Lines • ▼ Show 20 Lines | members: { | ||||
}, | }, | ||||
getHasSuggestion: function() { | getHasSuggestion: function() { | ||||
return this._getActiveContentState().getHasSuggestion(); | return this._getActiveContentState().getHasSuggestion(); | ||||
}, | }, | ||||
save: function() { | save: function() { | ||||
if (this._shouldDeleteOnSave()) { | if (this._shouldDeleteOnSave()) { | ||||
this._setPreventUndo(true); | JX.DOM.remove(this._editRow); | ||||
this._applyDelete(); | this._editRow = null; | ||||
this._applyDelete(true); | |||||
return; | return; | ||||
} | } | ||||
this._applySave(); | this._applySave(); | ||||
}, | }, | ||||
_shouldDeleteOnSave: function() { | _shouldDeleteOnSave: function() { | ||||
var state = this._getActiveContentState(); | var active = this._getActiveContentState(); | ||||
var initial = this._getInitialContentState(); | |||||
// TODO: This is greatly simplified because we don't track all the | // When a user clicks "Save", it counts as a "delete" if the content | ||||
// state we need yet. | // of the comment is functionally empty. | ||||
return !state.getText().length; | // This isn't a delete if there's any text. Even if the text is a | ||||
}, | // quote (so the state is the same as the initial state), we preserve | ||||
// it when the user clicks "Save". | |||||
_shouldDeleteOnCancel: function() { | if (!active.isTextEmpty()) { | ||||
var state = this._getActiveContentState(); | return false; | ||||
} | |||||
// TODO: This is greatly simplified, too. | // This isn't a delete if there's a suggestion and that suggestion is | ||||
// different from the initial state. (This means that an inline which | |||||
// purely suggests a block of code should be deleted is non-empty.) | |||||
if (active.getHasSuggestion()) { | |||||
if (!active.isSuggestionSimilar(initial)) { | |||||
return false; | |||||
} | |||||
} | |||||
return !state.getText().length; | // Otherwise, this comment is functionally empty, so we can just treat | ||||
// a "Save" as a "delete". | |||||
return true; | |||||
}, | }, | ||||
_shouldUndoOnCancel: function() { | _shouldUndoOnCancel: function() { | ||||
var new_state = this._getActiveContentState().getWireFormat(); | var committed = this._getCommittedContentState(); | ||||
var old_state = this._getCommittedContentState().getWireFormat(); | var active = this._getActiveContentState(); | ||||
var initial = this._getInitialContentState(); | |||||
// TODO: This is also simplified. | |||||
// When a user clicks "Cancel", we only offer to let them "Undo" the | |||||
var is_empty = this._isVoidContentState(new_state); | // action if the undo would be substantive. | ||||
var is_same = this._isSameContentState(new_state, old_state); | |||||
// The undo is substantive if the text is nonempty, and not similar to | |||||
// the last state. | |||||
var versus = committed || initial; | |||||
if (!active.isTextEmpty() && !active.isTextSimilar(versus)) { | |||||
return true; | |||||
} | |||||
if (!is_empty && !is_same) { | // The undo is substantive if there's a suggestion, and the suggestion | ||||
// is not similar to the last state. | |||||
if (active.getHasSuggestion()) { | |||||
if (!active.isSuggestionSimilar(versus)) { | |||||
return true; | return true; | ||||
} | } | ||||
} | |||||
return false; | return false; | ||||
}, | }, | ||||
_applySave: function() { | _applySave: function() { | ||||
var handler = JX.bind(this, this._onsaveresponse); | var handler = JX.bind(this, this._onsaveresponse); | ||||
var state = this._getActiveContentState(); | var state = this._getActiveContentState(); | ||||
var data = this._newRequestData('save', state.getWireFormat()); | var data = this._newRequestData('save', state.getWireFormat()); | ||||
this._applyCall(handler, data); | this._applyCall(handler, data); | ||||
}, | }, | ||||
_applyDelete: function() { | _applyDelete: function(prevent_undo) { | ||||
var handler = JX.bind(this, this._ondeleteresponse); | var handler = JX.bind(this, this._ondeleteresponse, prevent_undo); | ||||
var data = this._newRequestData('delete'); | var data = this._newRequestData('delete'); | ||||
this._applyCall(handler, data); | this._applyCall(handler, data); | ||||
}, | }, | ||||
_applyCancel: function(state) { | _applyCancel: function(state) { | ||||
var handler = JX.bind(this, this._onCancelResponse); | var handler = JX.bind(this, this._onCancelResponse); | ||||
var data = this._newRequestData('cancel', state); | var data = this._newRequestData('cancel', state); | ||||
this._applyCall(handler, data); | this._applyCall(handler, data); | ||||
}, | }, | ||||
_applyEdit: function(state) { | |||||
var handler = JX.bind(this, this._oneditresponse); | |||||
var data = this._newRequestData('edit', state); | |||||
this._applyCall(handler, data); | |||||
}, | |||||
_applyCall: function(handler, data) { | _applyCall: function(handler, data) { | ||||
var uri = this._getInlineURI(); | var uri = this._getInlineURI(); | ||||
var callback = JX.bind(this, function() { | var callback = JX.bind(this, function() { | ||||
this.setLoading(false); | this.setLoading(false); | ||||
handler.apply(null, arguments); | handler.apply(null, arguments); | ||||
}); | }); | ||||
Show All 27 Lines | members: { | ||||
}, | }, | ||||
_onundelete: function() { | _onundelete: function() { | ||||
this.setLoading(false); | this.setLoading(false); | ||||
this._didUpdate(); | this._didUpdate(); | ||||
}, | }, | ||||
cancel: function() { | cancel: function() { | ||||
// NOTE: Read the state before we remove the editor. Otherwise, we might | |||||
// miss text the user has entered into the textarea. | |||||
var state = this._getActiveContentState().getWireFormat(); | |||||
JX.DOM.remove(this._editRow); | JX.DOM.remove(this._editRow); | ||||
this._editRow = null; | this._editRow = null; | ||||
if (this._shouldDeleteOnCancel()) { | // When a user clicks "Cancel", we delete the comment if it has never | ||||
this._setPreventUndo(true); | // been saved: we don't have a non-empty display state to revert to. | ||||
this._applyDelete(); | var is_delete = (this._getCommittedContentState() === null); | ||||
return; | |||||
} | |||||
if (this._shouldUndoOnCancel()) { | var is_undo = this._shouldUndoOnCancel(); | ||||
var state = this._getActiveContentState().getWireFormat(); | |||||
this._drawUneditRows(state); | |||||
} | |||||
// If you "undo" to restore text ("AB") and then "Cancel", we put you | // If you "undo" to restore text ("AB") and then "Cancel", we put you | ||||
// back in the original text state ("A"). We also send the original | // back in the original text state ("A"). We also send the original | ||||
// text ("A") to the server as the current persistent state. | // text ("A") to the server as the current persistent state. | ||||
if (is_undo) { | |||||
this._drawUneditRows(state); | |||||
} | |||||
if (is_delete) { | |||||
// NOTE: We're always suppressing the undo from "delete". We want to | |||||
// use the "undo" we just added above instead, which will get us | |||||
// back to the ephemeral, client-side editor state. | |||||
this._applyDelete(true); | |||||
} else { | |||||
this.setEditing(false); | this.setEditing(false); | ||||
this.setInvisible(false); | this.setInvisible(false); | ||||
var old_state = this._getCommittedContentState(); | var old_state = this._getCommittedContentState(); | ||||
this._applyCancel(old_state.getWireFormat()); | this._applyCancel(old_state.getWireFormat()); | ||||
this._didUpdate(true); | this._didUpdate(true); | ||||
} | |||||
}, | }, | ||||
_onCancelResponse: function(response) { | _onCancelResponse: function(response) { | ||||
// Nothing to do. | // Nothing to do. | ||||
}, | }, | ||||
_getSuggestionNode: function(row) { | _getSuggestionNode: function(row) { | ||||
try { | try { | ||||
▲ Show 20 Lines • Show All 80 Lines • ▼ Show 20 Lines | _getDraftState: function() { | ||||
if (this.isDeleted()) { | if (this.isDeleted()) { | ||||
return null; | return null; | ||||
} | } | ||||
if (!this.isEditing()) { | if (!this.isEditing()) { | ||||
return null; | return null; | ||||
} | } | ||||
var state = this._getActiveContentState().getWireFormat(); | var state = this._getActiveContentState(); | ||||
if (this._isVoidContentState(state)) { | if (state.isStateEmpty()) { | ||||
return null; | return null; | ||||
} | } | ||||
var draft_data = { | var draft_data = { | ||||
op: 'draft', | op: 'draft', | ||||
id: this.getID(), | id: this.getID(), | ||||
}; | }; | ||||
JX.copy(draft_data, state); | JX.copy(draft_data, state.getWireFormat()); | ||||
return draft_data; | return draft_data; | ||||
}, | }, | ||||
triggerDraft: function() { | triggerDraft: function() { | ||||
if (this._draftRequest) { | if (this._draftRequest) { | ||||
this._draftRequest.trigger(); | this._draftRequest.trigger(); | ||||
} | } | ||||
▲ Show 20 Lines • Show All 99 Lines • ▼ Show 20 Lines | |||||
}, | }, | ||||
_newContentState: function() { | _newContentState: function() { | ||||
return { | return { | ||||
text: '', | text: '', | ||||
suggestionText: '', | suggestionText: '', | ||||
hasSuggestion: false | hasSuggestion: false | ||||
}; | }; | ||||
}, | |||||
_isVoidContentState: function(state) { | |||||
if (!state.text) { | |||||
return true; | |||||
} | } | ||||
return (!state.text.length && !state.suggestionText.length); | |||||
}, | |||||
_isSameContentState: function(u, v) { | |||||
return ( | |||||
((u === null) === (v === null)) && | |||||
(u.text === v.text) && | |||||
(u.suggestionText === v.suggestionText) && | |||||
(u.hasSuggestion === v.hasSuggestion)); | |||||
} | |||||
} | } | ||||
}); | }); |