Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/js/application/diff/DiffInline.js
12242 views
1
/**
2
* @provides phabricator-diff-inline
3
* @requires javelin-dom
4
* phabricator-diff-inline-content-state
5
* @javelin
6
*/
7
8
JX.install('DiffInline', {
9
10
construct : function() {
11
this._state = {};
12
},
13
14
members: {
15
_id: null,
16
_phid: null,
17
_changesetID: null,
18
_row: null,
19
_number: null,
20
_length: null,
21
_displaySide: null,
22
_isNewFile: null,
23
_replyToCommentPHID: null,
24
_snippet: null,
25
_menuItems: null,
26
_documentEngineKey: null,
27
28
_isDeleted: false,
29
_isInvisible: false,
30
_isLoading: false,
31
32
_changeset: null,
33
34
_isCollapsed: false,
35
_isDraft: null,
36
_isDraftDone: null,
37
_isFixed: null,
38
_isEditing: false,
39
_isNew: false,
40
_isSynthetic: false,
41
_isHidden: false,
42
43
_editRow: null,
44
_undoRow: null,
45
_undoType: null,
46
_undoState: null,
47
48
_draftRequest: null,
49
_skipFocus: false,
50
_menu: null,
51
52
_startOffset: null,
53
_endOffset: null,
54
_isSelected: false,
55
_canSuggestEdit: false,
56
57
_state: null,
58
59
bindToRow: function(row) {
60
this._row = row;
61
62
var row_data = JX.Stratcom.getData(row);
63
row_data.inline = this;
64
this._isCollapsed = row_data.hidden || false;
65
66
// TODO: Get smarter about this once we do more editing, this is pretty
67
// hacky.
68
var comment = JX.DOM.find(row, 'div', 'differential-inline-comment');
69
var data = JX.Stratcom.getData(comment);
70
71
this._readInlineState(data);
72
this._phid = data.phid;
73
74
if (data.on_right) {
75
this._displaySide = 'right';
76
} else {
77
this._displaySide = 'left';
78
}
79
80
this._number = parseInt(data.number, 10);
81
this._length = parseInt(data.length, 10);
82
83
this._isNewFile = data.isNewFile;
84
85
this._replyToCommentPHID = data.replyToCommentPHID;
86
87
this._isDraft = data.isDraft;
88
this._isFixed = data.isFixed;
89
this._isGhost = data.isGhost;
90
this._isSynthetic = data.isSynthetic;
91
this._isDraftDone = data.isDraftDone;
92
93
this._changesetID = data.changesetID;
94
this._isNew = false;
95
this._snippet = data.snippet;
96
this._menuItems = data.menuItems;
97
this._documentEngineKey = data.documentEngineKey;
98
99
this._startOffset = data.startOffset;
100
this._endOffset = data.endOffset;
101
102
this._isEditing = data.isEditing;
103
104
if (this._isEditing) {
105
// NOTE: The "original" shipped down in the DOM may reflect a draft
106
// which we're currently editing. This flow is a little clumsy, but
107
// reasonable until some future change moves away from "send down
108
// the inline, then immediately click edit".
109
this.edit(null, true);
110
} else {
111
this.setInvisible(false);
112
}
113
114
this._startDrafts();
115
116
return this;
117
},
118
119
isDraft: function() {
120
return this._isDraft;
121
},
122
123
isDone: function() {
124
return this._isFixed;
125
},
126
127
isEditing: function() {
128
return this._isEditing;
129
},
130
131
isUndo: function() {
132
return !!this._undoRow;
133
},
134
135
isDeleted: function() {
136
return this._isDeleted;
137
},
138
139
isSynthetic: function() {
140
return this._isSynthetic;
141
},
142
143
isDraftDone: function() {
144
return this._isDraftDone;
145
},
146
147
isHidden: function() {
148
return this._isHidden;
149
},
150
151
isGhost: function() {
152
return this._isGhost;
153
},
154
155
getStartOffset: function() {
156
return this._startOffset;
157
},
158
159
getEndOffset: function() {
160
return this._endOffset;
161
},
162
163
setIsSelected: function(is_selected) {
164
this._isSelected = is_selected;
165
166
if (this._row) {
167
JX.DOM.alterClass(
168
this._row,
169
'inline-comment-selected',
170
this._isSelected);
171
}
172
173
return this;
174
},
175
176
bindToRange: function(data) {
177
this._displaySide = data.displaySide;
178
this._number = parseInt(data.number, 10);
179
this._length = parseInt(data.length, 10);
180
this._isNewFile = data.isNewFile;
181
this._changesetID = data.changesetID;
182
this._isNew = true;
183
184
if (data.hasOwnProperty('startOffset')) {
185
this._startOffset = data.startOffset;
186
} else {
187
this._startOffset = null;
188
}
189
190
if (data.hasOwnProperty('endOffset')) {
191
this._endOffset = data.endOffset;
192
} else {
193
this._endOffset = null;
194
}
195
196
// Insert the comment after any other comments which already appear on
197
// the same row.
198
var parent_row = JX.DOM.findAbove(data.target, 'tr');
199
var target_row = parent_row.nextSibling;
200
while (target_row && JX.Stratcom.hasSigil(target_row, 'inline-row')) {
201
target_row = target_row.nextSibling;
202
}
203
204
var row = this._newRow();
205
parent_row.parentNode.insertBefore(row, target_row);
206
207
this.setInvisible(true);
208
this._startDrafts();
209
210
return this;
211
},
212
213
bindToReply: function(inline) {
214
this._displaySide = inline._displaySide;
215
this._number = inline._number;
216
this._length = inline._length;
217
this._isNewFile = inline._isNewFile;
218
this._changesetID = inline._changesetID;
219
this._isNew = true;
220
this._documentEngineKey = inline._documentEngineKey;
221
222
this._replyToCommentPHID = inline._phid;
223
224
var changeset = this.getChangeset();
225
226
// We're going to figure out where in the document to position the new
227
// inline. Normally, it goes after any existing inline rows (so if
228
// several inlines reply to the same line, they appear in chronological
229
// order).
230
231
// However: if inlines are threaded, we want to put the new inline in
232
// the right place in the thread. This might be somewhere in the middle,
233
// so we need to do a bit more work to figure it out.
234
235
// To find the right place in the thread, we're going to look for any
236
// inline which is at or above the level of the comment we're replying
237
// to. This means we've reached a new fork of the thread, and should
238
// put our new inline before the comment we found.
239
var ancestor_map = {};
240
var ancestor = inline;
241
var reply_phid;
242
while (ancestor) {
243
reply_phid = ancestor.getReplyToCommentPHID();
244
if (!reply_phid) {
245
break;
246
}
247
ancestor_map[reply_phid] = true;
248
ancestor = changeset.getInlineByPHID(reply_phid);
249
}
250
251
var parent_row = inline._row;
252
var target_row = parent_row.nextSibling;
253
while (target_row && JX.Stratcom.hasSigil(target_row, 'inline-row')) {
254
var target = changeset.getInlineForRow(target_row);
255
reply_phid = target.getReplyToCommentPHID();
256
257
// If we found an inline which is replying directly to some ancestor
258
// of this new comment, this is where the new rows go.
259
if (ancestor_map.hasOwnProperty(reply_phid)) {
260
break;
261
}
262
263
target_row = target_row.nextSibling;
264
}
265
266
var row = this._newRow();
267
parent_row.parentNode.insertBefore(row, target_row);
268
269
this.setInvisible(true);
270
this._startDrafts();
271
272
return this;
273
},
274
275
setChangeset: function(changeset) {
276
this._changeset = changeset;
277
return this;
278
},
279
280
getChangeset: function() {
281
return this._changeset;
282
},
283
284
setEditing: function(editing) {
285
this._isEditing = editing;
286
return this;
287
},
288
289
setHidden: function(hidden) {
290
this._isHidden = hidden;
291
this._redraw();
292
return this;
293
},
294
295
canReply: function() {
296
return this._hasMenuAction('reply');
297
},
298
299
canEdit: function() {
300
return this._hasMenuAction('edit');
301
},
302
303
canDone: function() {
304
if (!JX.DOM.scry(this._row, 'input', 'differential-inline-done').length) {
305
return false;
306
}
307
308
return true;
309
},
310
311
canCollapse: function() {
312
return this._hasMenuAction('collapse');
313
},
314
315
_newRow: function() {
316
var attributes = {
317
sigil: 'inline-row'
318
};
319
320
var row = JX.$N('tr', attributes);
321
322
JX.Stratcom.getData(row).inline = this;
323
this._row = row;
324
325
this._id = null;
326
this._phid = null;
327
this._isCollapsed = false;
328
329
return row;
330
},
331
332
setCollapsed: function(collapsed) {
333
this._closeMenu();
334
335
this._isCollapsed = collapsed;
336
337
var op;
338
if (collapsed) {
339
op = 'hide';
340
} else {
341
op = 'show';
342
}
343
344
var inline_uri = this._getInlineURI();
345
var comment_id = this._id;
346
347
new JX.Workflow(inline_uri, {op: op, ids: comment_id})
348
.setHandler(JX.bag)
349
.start();
350
351
this._redraw();
352
this._didUpdate(true);
353
},
354
355
isCollapsed: function() {
356
return this._isCollapsed;
357
},
358
359
toggleDone: function() {
360
var uri = this._getInlineURI();
361
var data = {
362
op: 'done',
363
id: this._id
364
};
365
366
var ondone = JX.bind(this, this._ondone);
367
368
new JX.Workflow(uri, data)
369
.setHandler(ondone)
370
.start();
371
},
372
373
_ondone: function(response) {
374
var checkbox = JX.DOM.find(
375
this._row,
376
'input',
377
'differential-inline-done');
378
379
checkbox.checked = (response.isChecked ? 'checked' : null);
380
381
var comment = JX.DOM.findAbove(
382
checkbox,
383
'div',
384
'differential-inline-comment');
385
386
JX.DOM.alterClass(comment, 'inline-is-done', response.isChecked);
387
388
// NOTE: This is marking the inline as having an unsubmitted checkmark,
389
// as opposed to a submitted checkmark. This is different from the
390
// top-level "draft" state of unsubmitted comments.
391
JX.DOM.alterClass(comment, 'inline-state-is-draft', response.draftState);
392
393
this._isFixed = response.isChecked;
394
this._isDraftDone = !!response.draftState;
395
396
this._didUpdate();
397
},
398
399
create: function(content_state) {
400
var changeset = this.getChangeset();
401
if (!this._documentEngineKey) {
402
this._documentEngineKey = changeset.getResponseDocumentEngineKey();
403
}
404
405
var uri = this._getInlineURI();
406
var handler = JX.bind(this, this._oncreateresponse);
407
var data = this._newRequestData('new', content_state);
408
409
this.setLoading(true);
410
411
new JX.Request(uri, handler)
412
.setData(data)
413
.send();
414
},
415
416
reply: function(with_quote) {
417
this._closeMenu();
418
419
var content_state = this._newContentState();
420
if (with_quote) {
421
var text = this._getActiveContentState().getTextForQuote();
422
content_state.text = text;
423
}
424
425
var changeset = this.getChangeset();
426
return changeset.newInlineReply(this, content_state);
427
},
428
429
edit: function(content_state, skip_focus) {
430
this._closeMenu();
431
432
this._skipFocus = !!skip_focus;
433
434
// If you edit an inline ("A"), modify the text ("AB"), cancel, and then
435
// edit it again: discard the undo state ("AB"). Otherwise we end up
436
// with an open editor and an active "Undo" link, which is weird.
437
438
if (this._undoRow) {
439
JX.DOM.remove(this._undoRow);
440
this._undoRow = null;
441
442
this._undoType = null;
443
this._undoText = null;
444
}
445
446
this._applyEdit(content_state);
447
},
448
449
delete: function(is_ref) {
450
var uri = this._getInlineURI();
451
var handler = JX.bind(this, this._ondeleteresponse, false);
452
453
// NOTE: This may be a direct delete (the user clicked on the inline
454
// itself) or a "refdelete" (the user clicked somewhere else, like the
455
// preview, but the inline is present on the page).
456
457
// For a "refdelete", we prompt the user to confirm that they want to
458
// delete the comment, because they can not undo deletions from the
459
// preview. We could jump the user to the inline instead, but this would
460
// be somewhat disruptive and make deleting several comments more
461
// difficult.
462
463
var op;
464
if (is_ref) {
465
op = 'refdelete';
466
} else {
467
op = 'delete';
468
}
469
470
var data = this._newRequestData(op);
471
472
this.setLoading(true);
473
474
new JX.Workflow(uri, data)
475
.setHandler(handler)
476
.start();
477
},
478
479
getDisplaySide: function() {
480
return this._displaySide;
481
},
482
483
getLineNumber: function() {
484
return this._number;
485
},
486
487
getLineLength: function() {
488
return this._length;
489
},
490
491
isNewFile: function() {
492
return this._isNewFile;
493
},
494
495
getID: function() {
496
return this._id;
497
},
498
499
getPHID: function() {
500
return this._phid;
501
},
502
503
getChangesetID: function() {
504
return this._changesetID;
505
},
506
507
getReplyToCommentPHID: function() {
508
return this._replyToCommentPHID;
509
},
510
511
setDeleted: function(deleted) {
512
this._isDeleted = deleted;
513
this._redraw();
514
return this;
515
},
516
517
setInvisible: function(invisible) {
518
this._isInvisible = invisible;
519
this._redraw();
520
return this;
521
},
522
523
setLoading: function(loading) {
524
this._isLoading = loading;
525
this._redraw();
526
return this;
527
},
528
529
_newRequestData: function(operation, content_state) {
530
var data = {
531
op: operation,
532
is_new: this.isNewFile(),
533
on_right: ((this.getDisplaySide() == 'right') ? 1 : 0),
534
renderer: this.getChangeset().getRendererKey()
535
};
536
537
if (operation === 'new') {
538
var create_data = {
539
changesetID: this.getChangesetID(),
540
documentEngineKey: this._documentEngineKey,
541
replyToCommentPHID: this.getReplyToCommentPHID(),
542
startOffset: this._startOffset,
543
endOffset: this._endOffset,
544
number: this.getLineNumber(),
545
length: this.getLineLength()
546
};
547
548
JX.copy(data, create_data);
549
} else {
550
var edit_data = {
551
id: this._id
552
};
553
554
JX.copy(data, edit_data);
555
}
556
557
if (content_state) {
558
data.hasContentState = 1;
559
JX.copy(data, content_state);
560
}
561
562
return data;
563
},
564
565
_oneditresponse: function(response) {
566
var rows = JX.$H(response.view).getNode();
567
568
this._readInlineState(response.inline);
569
this._drawEditRows(rows);
570
571
this.setInvisible(true);
572
},
573
574
_oncreateresponse: function(response) {
575
var rows = JX.$H(response.view).getNode();
576
577
this._readInlineState(response.inline);
578
this._drawEditRows(rows);
579
},
580
581
_readInlineState: function(state) {
582
this._id = state.id;
583
584
this._state = {
585
initial: this._newContentStateFromWireFormat(state.state.initial),
586
committed: this._newContentStateFromWireFormat(state.state.committed),
587
active: this._newContentStateFromWireFormat(state.state.active)
588
};
589
590
this._canSuggestEdit = state.canSuggestEdit;
591
},
592
593
_newContentStateFromWireFormat: function(map) {
594
if (map === null) {
595
return null;
596
}
597
598
return new JX.DiffInlineContentState().readWireFormat(map);
599
},
600
601
_ondeleteresponse: function(prevent_undo) {
602
if (!prevent_undo) {
603
// If there's an existing "unedit" undo element, remove it.
604
if (this._undoRow) {
605
JX.DOM.remove(this._undoRow);
606
this._undoRow = null;
607
}
608
609
// If there's an existing editor, remove it. This happens when you
610
// delete a comment from the comment preview area. In this case, we
611
// read and preserve the text so "Undo" restores it.
612
var state = null;
613
if (this._editRow) {
614
state = this._getActiveContentState().getWireFormat();
615
JX.DOM.remove(this._editRow);
616
this._editRow = null;
617
}
618
619
this._drawUndeleteRows(state);
620
}
621
622
this.setLoading(false);
623
this.setDeleted(true);
624
625
this._didUpdate();
626
},
627
628
_drawUndeleteRows: function(content_state) {
629
this._undoType = 'undelete';
630
this._undoState = content_state || null;
631
632
return this._drawUndoRows('undelete', this._row);
633
},
634
635
_drawUneditRows: function(content_state) {
636
this._undoType = 'unedit';
637
this._undoState = content_state;
638
639
return this._drawUndoRows('unedit', null);
640
},
641
642
_drawUndoRows: function(mode, cursor) {
643
var templates = this.getChangeset().getUndoTemplates();
644
645
var template;
646
if (this.getDisplaySide() == 'right') {
647
template = templates.r;
648
} else {
649
template = templates.l;
650
}
651
template = JX.$H(template).getNode();
652
653
this._undoRow = this._drawRows(template, cursor, mode);
654
},
655
656
_drawContentRows: function(rows) {
657
return this._drawRows(rows, null, 'content');
658
},
659
660
_drawEditRows: function(rows) {
661
this.setEditing(true);
662
this._editRow = this._drawRows(rows, null, 'edit');
663
664
this._drawSuggestionState(this._editRow);
665
666
// TODO: We're just doing this for the rendering side effect of drawing
667
// the button text.
668
this.setHasSuggestion(this.getHasSuggestion());
669
},
670
671
_drawRows: function(rows, cursor, type) {
672
var first_row = JX.DOM.scry(rows, 'tr')[0];
673
var row = first_row;
674
var anchor = cursor || this._row;
675
cursor = cursor || this._row.nextSibling;
676
677
var result_row;
678
var next_row;
679
while (row) {
680
// Grab this first, since it's going to change once we insert the row
681
// into the document.
682
next_row = row.nextSibling;
683
684
// Bind edit and undo rows to this DiffInline object so that
685
// interactions like hovering work properly.
686
JX.Stratcom.getData(row).inline = this;
687
688
anchor.parentNode.insertBefore(row, cursor);
689
cursor = row;
690
691
if (!result_row) {
692
result_row = row;
693
}
694
695
if (!this._skipFocus) {
696
// If the row has a textarea, focus it. This allows the user to start
697
// typing a comment immediately after a "new", "edit", or "reply"
698
// action.
699
700
// (When simulating an "edit" on page load, we don't do this.)
701
702
var textareas = JX.DOM.scry(
703
row,
704
'textarea',
705
'inline-content-text');
706
if (textareas.length) {
707
var area = textareas[0];
708
area.focus();
709
710
var length = area.value.length;
711
JX.TextAreaUtils.setSelectionRange(area, length, length);
712
}
713
}
714
715
row = next_row;
716
}
717
718
JX.Stratcom.invoke('resize');
719
720
return result_row;
721
},
722
723
_drawSuggestionState: function(row) {
724
if (this._canSuggestEdit) {
725
var button = this._getSuggestionButton();
726
var node = button.getNode();
727
728
// As a side effect of form submission, the button may become
729
// visually disabled. Re-enable it. This is a bit hacky.
730
JX.DOM.alterClass(node, 'disabled', false);
731
node.disabled = false;
732
733
var container = JX.DOM.find(row, 'div', 'inline-edit-buttons');
734
container.appendChild(node);
735
}
736
},
737
738
_getSuggestionButton: function() {
739
if (!this._suggestionButton) {
740
var button = new JX.PHUIXButtonView()
741
.setIcon('fa-pencil-square-o')
742
.setColor('grey');
743
744
var node = button.getNode();
745
JX.DOM.alterClass(node, 'inline-button-left', true);
746
747
var onclick = JX.bind(this, this._onSuggestEdit);
748
JX.DOM.listen(node, 'click', null, onclick);
749
750
this._suggestionButton = button;
751
}
752
753
return this._suggestionButton;
754
},
755
756
_onSuggestEdit: function(e) {
757
e.kill();
758
759
this.setHasSuggestion(!this.getHasSuggestion());
760
761
// Resize the suggestion input for size of the text.
762
if (this.getHasSuggestion()) {
763
if (this._editRow) {
764
var node = this._getSuggestionNode(this._editRow);
765
if (node) {
766
node.rows = Math.max(3, node.value.split('\n').length);
767
}
768
}
769
}
770
771
// Save the "hasSuggestion" part of the content state.
772
this.triggerDraft();
773
},
774
775
_getActiveContentState: function() {
776
var state = this._state.active;
777
778
if (this._editRow) {
779
state.readForm(this._editRow);
780
}
781
782
return state;
783
},
784
785
_getCommittedContentState: function() {
786
return this._state.committed;
787
},
788
789
_getInitialContentState: function() {
790
return this._state.initial;
791
},
792
793
setHasSuggestion: function(has_suggestion) {
794
var state = this._getActiveContentState();
795
state.setHasSuggestion(has_suggestion);
796
797
var button = this._getSuggestionButton();
798
var pht = this.getChangeset().getChangesetList().getTranslations();
799
if (has_suggestion) {
800
button
801
.setIcon('fa-times')
802
.setText(pht('Discard Edit'));
803
} else {
804
button
805
.setIcon('fa-plus')
806
.setText(pht('Suggest Edit'));
807
}
808
809
if (this._editRow) {
810
JX.DOM.alterClass(this._editRow, 'has-suggestion', has_suggestion);
811
}
812
},
813
814
getHasSuggestion: function() {
815
return this._getActiveContentState().getHasSuggestion();
816
},
817
818
save: function() {
819
if (this._shouldDeleteOnSave()) {
820
JX.DOM.remove(this._editRow);
821
this._editRow = null;
822
823
this._applyDelete(true);
824
return;
825
}
826
827
this._applySave();
828
},
829
830
_shouldDeleteOnSave: function() {
831
var active = this._getActiveContentState();
832
var initial = this._getInitialContentState();
833
834
// When a user clicks "Save", it counts as a "delete" if the content
835
// of the comment is functionally empty.
836
837
// This isn't a delete if there's any text. Even if the text is a
838
// quote (so the state is the same as the initial state), we preserve
839
// it when the user clicks "Save".
840
if (!active.isTextEmpty()) {
841
return false;
842
}
843
844
// This isn't a delete if there's a suggestion and that suggestion is
845
// different from the initial state. (This means that an inline which
846
// purely suggests a block of code should be deleted is non-empty.)
847
if (active.getHasSuggestion()) {
848
if (!active.isSuggestionSimilar(initial)) {
849
return false;
850
}
851
}
852
853
// Otherwise, this comment is functionally empty, so we can just treat
854
// a "Save" as a "delete".
855
return true;
856
},
857
858
_shouldUndoOnCancel: function() {
859
var committed = this._getCommittedContentState();
860
var active = this._getActiveContentState();
861
var initial = this._getInitialContentState();
862
863
// When a user clicks "Cancel", we only offer to let them "Undo" the
864
// action if the undo would be substantive.
865
866
// The undo is substantive if the text is nonempty, and not similar to
867
// the last state.
868
var versus = committed || initial;
869
if (!active.isTextEmpty() && !active.isTextSimilar(versus)) {
870
return true;
871
}
872
873
// The undo is substantive if there's a suggestion, and the suggestion
874
// is not similar to the last state.
875
if (active.getHasSuggestion()) {
876
if (!active.isSuggestionSimilar(versus)) {
877
return true;
878
}
879
}
880
881
return false;
882
},
883
884
_applySave: function() {
885
var handler = JX.bind(this, this._onsaveresponse);
886
887
var state = this._getActiveContentState();
888
var data = this._newRequestData('save', state.getWireFormat());
889
890
this._applyCall(handler, data);
891
},
892
893
_applyDelete: function(prevent_undo) {
894
var handler = JX.bind(this, this._ondeleteresponse, prevent_undo);
895
896
var data = this._newRequestData('delete');
897
898
this._applyCall(handler, data);
899
},
900
901
_applyCancel: function(state) {
902
var handler = JX.bind(this, this._onCancelResponse);
903
904
var data = this._newRequestData('cancel', state);
905
906
this._applyCall(handler, data);
907
},
908
909
_applyEdit: function(state) {
910
var handler = JX.bind(this, this._oneditresponse);
911
912
var data = this._newRequestData('edit', state);
913
914
this._applyCall(handler, data);
915
},
916
917
_applyCall: function(handler, data) {
918
var uri = this._getInlineURI();
919
920
var callback = JX.bind(this, function() {
921
this.setLoading(false);
922
handler.apply(null, arguments);
923
});
924
925
this.setLoading(true);
926
927
new JX.Workflow(uri, data)
928
.setHandler(callback)
929
.start();
930
},
931
932
undo: function() {
933
JX.DOM.remove(this._undoRow);
934
this._undoRow = null;
935
936
if (this._undoType === 'undelete') {
937
var uri = this._getInlineURI();
938
var data = this._newRequestData('undelete');
939
var handler = JX.bind(this, this._onundelete);
940
941
this.setDeleted(false);
942
this.setLoading(true);
943
944
new JX.Request(uri, handler)
945
.setData(data)
946
.send();
947
}
948
949
if (this._undoState !== null) {
950
this.edit(this._undoState);
951
}
952
},
953
954
_onundelete: function() {
955
this.setLoading(false);
956
this._didUpdate();
957
},
958
959
cancel: function() {
960
// NOTE: Read the state before we remove the editor. Otherwise, we might
961
// miss text the user has entered into the textarea.
962
var state = this._getActiveContentState().getWireFormat();
963
964
JX.DOM.remove(this._editRow);
965
this._editRow = null;
966
967
// When a user clicks "Cancel", we delete the comment if it has never
968
// been saved: we don't have a non-empty display state to revert to.
969
var is_delete = (this._getCommittedContentState() === null);
970
971
var is_undo = this._shouldUndoOnCancel();
972
973
// If you "undo" to restore text ("AB") and then "Cancel", we put you
974
// back in the original text state ("A"). We also send the original
975
// text ("A") to the server as the current persistent state.
976
977
if (is_undo) {
978
this._drawUneditRows(state);
979
}
980
981
if (is_delete) {
982
// NOTE: We're always suppressing the undo from "delete". We want to
983
// use the "undo" we just added above instead, which will get us
984
// back to the ephemeral, client-side editor state.
985
this._applyDelete(true);
986
} else {
987
this.setEditing(false);
988
this.setInvisible(false);
989
990
var old_state = this._getCommittedContentState();
991
this._applyCancel(old_state.getWireFormat());
992
993
this._didUpdate(true);
994
}
995
},
996
997
_onCancelResponse: function(response) {
998
// Nothing to do.
999
},
1000
1001
_getSuggestionNode: function(row) {
1002
try {
1003
return JX.DOM.find(row, 'textarea', 'inline-content-suggestion');
1004
} catch (ex) {
1005
return null;
1006
}
1007
},
1008
1009
_onsaveresponse: function(response) {
1010
if (this._editRow) {
1011
JX.DOM.remove(this._editRow);
1012
this._editRow = null;
1013
}
1014
1015
this.setEditing(false);
1016
this.setInvisible(false);
1017
1018
var new_row = this._drawContentRows(JX.$H(response.view).getNode());
1019
JX.DOM.remove(this._row);
1020
this.bindToRow(new_row);
1021
1022
this._didUpdate();
1023
},
1024
1025
_didUpdate: function(local_only) {
1026
// After making changes to inline comments, refresh the transaction
1027
// preview at the bottom of the page.
1028
if (!local_only) {
1029
this.getChangeset().getChangesetList().redrawPreview();
1030
}
1031
1032
this.getChangeset().getChangesetList().redrawCursor();
1033
this.getChangeset().getChangesetList().resetHover();
1034
1035
// Emit a resize event so that UI elements like the keyboard focus
1036
// reticle can redraw properly.
1037
JX.Stratcom.invoke('resize');
1038
},
1039
1040
_redraw: function() {
1041
var is_invisible =
1042
(this._isInvisible || this._isDeleted || this._isHidden);
1043
var is_loading = this._isLoading;
1044
var is_collapsed = (this._isCollapsed && !this._isHidden);
1045
1046
var row = this._row;
1047
JX.DOM.alterClass(row, 'differential-inline-hidden', is_invisible);
1048
JX.DOM.alterClass(row, 'differential-inline-loading', is_loading);
1049
JX.DOM.alterClass(row, 'inline-hidden', is_collapsed);
1050
},
1051
1052
_getInlineURI: function() {
1053
var changeset = this.getChangeset();
1054
var list = changeset.getChangesetList();
1055
return list.getInlineURI();
1056
},
1057
1058
_startDrafts: function() {
1059
if (this._draftRequest) {
1060
return;
1061
}
1062
1063
var onresponse = JX.bind(this, this._onDraftResponse);
1064
var draft = JX.bind(this, this._getDraftState);
1065
1066
var uri = this._getInlineURI();
1067
var request = new JX.PhabricatorShapedRequest(uri, onresponse, draft);
1068
1069
// The main transaction code uses a 500ms delay on desktop and a
1070
// 10s delay on mobile. Perhaps this should be standardized.
1071
request.setRateLimit(2000);
1072
1073
this._draftRequest = request;
1074
1075
request.start();
1076
},
1077
1078
_onDraftResponse: function() {
1079
// For now, do nothing.
1080
},
1081
1082
_getDraftState: function() {
1083
if (this.isDeleted()) {
1084
return null;
1085
}
1086
1087
if (!this.isEditing()) {
1088
return null;
1089
}
1090
1091
var state = this._getActiveContentState();
1092
if (state.isStateEmpty()) {
1093
return null;
1094
}
1095
1096
var draft_data = {
1097
op: 'draft',
1098
id: this.getID(),
1099
};
1100
1101
JX.copy(draft_data, state.getWireFormat());
1102
1103
return draft_data;
1104
},
1105
1106
triggerDraft: function() {
1107
if (this._draftRequest) {
1108
this._draftRequest.trigger();
1109
}
1110
},
1111
1112
activateMenu: function(button, e) {
1113
// If we already have a menu for this button, let the menu handle the
1114
// event.
1115
var data = JX.Stratcom.getData(button);
1116
if (data.menu) {
1117
return;
1118
}
1119
1120
e.prevent();
1121
1122
var menu = new JX.PHUIXDropdownMenu(button)
1123
.setWidth(240);
1124
1125
var list = new JX.PHUIXActionListView();
1126
var items = this._newMenuItems(menu);
1127
for (var ii = 0; ii < items.length; ii++) {
1128
list.addItem(items[ii]);
1129
}
1130
1131
menu.setContent(list.getNode());
1132
1133
data.menu = menu;
1134
this._menu = menu;
1135
1136
menu.listen('open', JX.bind(this, function() {
1137
var changeset_list = this.getChangeset().getChangesetList();
1138
changeset_list.selectInline(this, true);
1139
}));
1140
1141
menu.open();
1142
},
1143
1144
_newMenuItems: function(menu) {
1145
var items = [];
1146
1147
for (var ii = 0; ii < this._menuItems.length; ii++) {
1148
var spec = this._menuItems[ii];
1149
1150
var onmenu = JX.bind(this, this._onMenuItem, menu, spec.action, spec);
1151
1152
var item = new JX.PHUIXActionView()
1153
.setIcon(spec.icon)
1154
.setName(spec.label)
1155
.setHandler(onmenu);
1156
1157
if (spec.key) {
1158
item.setKeyCommand(spec.key);
1159
}
1160
1161
items.push(item);
1162
}
1163
1164
return items;
1165
},
1166
1167
_onMenuItem: function(menu, action, spec, e) {
1168
e.prevent();
1169
menu.close();
1170
1171
switch (action) {
1172
case 'reply':
1173
this.reply();
1174
break;
1175
case 'quote':
1176
this.reply(true);
1177
break;
1178
case 'collapse':
1179
this.setCollapsed(true);
1180
break;
1181
case 'delete':
1182
this.delete();
1183
break;
1184
case 'edit':
1185
this.edit();
1186
break;
1187
case 'raw':
1188
new JX.Workflow(spec.uri)
1189
.start();
1190
break;
1191
}
1192
1193
},
1194
1195
_hasMenuAction: function(action) {
1196
for (var ii = 0; ii < this._menuItems.length; ii++) {
1197
var spec = this._menuItems[ii];
1198
if (spec.action === action) {
1199
return true;
1200
}
1201
}
1202
return false;
1203
},
1204
1205
_closeMenu: function() {
1206
if (this._menu) {
1207
this._menu.close();
1208
}
1209
},
1210
1211
_newContentState: function() {
1212
return {
1213
text: '',
1214
suggestionText: '',
1215
hasSuggestion: false
1216
};
1217
}
1218
1219
}
1220
1221
});
1222
1223