Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F21409670
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js
index 27b539a311..b5fde20329 100644
--- a/webroot/rsrc/js/application/diff/DiffChangesetList.js
+++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js
@@ -1,675 +1,711 @@
/**
* @provides phabricator-diff-changeset-list
* @requires javelin-install
* @javelin
*/
JX.install('DiffChangesetList', {
construct: function() {
this._changesets = [];
var onload = JX.bind(this, this._ifawake, this._onload);
JX.Stratcom.listen('click', 'differential-load', onload);
var onmore = JX.bind(this, this._ifawake, this._onmore);
JX.Stratcom.listen('click', 'show-more', onmore);
var onmenu = JX.bind(this, this._ifawake, this._onmenu);
JX.Stratcom.listen('click', 'differential-view-options', onmenu);
var onhide = JX.bind(this, this._ifawake, this._onhide);
JX.Stratcom.listen('click', 'hide-inline', onhide);
var onreveal = JX.bind(this, this._ifawake, this._onreveal);
JX.Stratcom.listen('click', 'reveal-inline', onreveal);
var onedit = JX.bind(this, this._ifawake, this._onaction, 'edit');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-edit'],
onedit);
var ondone = JX.bind(this, this._ifawake, this._onaction, 'done');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-done'],
ondone);
var ondelete = JX.bind(this, this._ifawake, this._onaction, 'delete');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-delete'],
ondelete);
var onreply = JX.bind(this, this._ifawake, this._onaction, 'reply');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-reply'],
onreply);
},
properties: {
translations: null,
inlineURI: null
},
members: {
_initialized: false,
_asleep: true,
_changesets: null,
_cursorItem: null,
- _lastKeyboardManager: null,
+
+ _focusNode: null,
+ _focusStart: null,
+ _focusEnd: null,
sleep: function() {
this._asleep = true;
},
wake: function() {
this._asleep = false;
if (this._initialized) {
return;
}
this._initialized = true;
var pht = this.getTranslations();
var label;
label = pht('Jump to next change.');
this._installJumpKey('j', label, 1);
label = pht('Jump to previous change.');
this._installJumpKey('k', label, -1);
label = pht('Jump to next file.');
this._installJumpKey('J', label, 1, 'file');
label = pht('Jump to previous file.');
this._installJumpKey('K', label, -1, 'file');
label = pht('Jump to next inline comment.');
this._installJumpKey('n', label, 1, 'comment');
label = pht('Jump to previous inline comment.');
this._installJumpKey('p', label, -1, 'comment');
label = pht('Jump to the table of contents.');
this._installKey('t', label, this._ontoc);
label = pht('Reply to selected inline comment.');
this._installKey('r', label, this._onreply);
label = pht('Edit selected inline comment.');
this._installKey('e', label, this._onedit);
},
isAsleep: function() {
return this._asleep;
},
newChangesetForNode: function(node) {
var changeset = JX.DiffChangeset.getForNode(node);
this._changesets.push(changeset);
changeset.setChangesetList(this);
return changeset;
},
getChangesetForNode: function(node) {
return JX.DiffChangeset.getForNode(node);
},
getInlineByID: function(id) {
var inline = null;
for (var ii = 0; ii < this._changesets.length; ii++) {
inline = this._changesets[ii].getInlineByID(id);
if (inline) {
break;
}
}
return inline;
},
_ifawake: function(f) {
// This function takes another function and only calls it if the
// changeset list is awake, so we basically just ignore events when we
// are asleep. This may move up the stack at some point as we do more
// with Quicksand/Sheets.
if (this.isAsleep()) {
return;
}
return f.apply(this, [].slice.call(arguments, 1));
},
_onload: function(e) {
var data = e.getNodeData('differential-load');
// NOTE: We can trigger a load from either an explicit "Load" link on
// the changeset, or by clicking a link in the table of contents. If
// the event was a table of contents link, we let the anchor behavior
// run normally.
if (data.kill) {
e.kill();
}
var node = JX.$(data.id);
var changeset = this.getChangesetForNode(node);
changeset.load();
// TODO: Move this into Changeset.
var routable = changeset.getRoutable();
if (routable) {
routable.setPriority(2000);
}
},
_installKey: function(key, label, handler) {
handler = JX.bind(this, this._ifawake, handler);
return new JX.KeyboardShortcut(key, label)
.setHandler(handler)
.register();
},
_installJumpKey: function(key, label, delta, filter) {
filter = filter || null;
var handler = JX.bind(this, this._onjumpkey, delta, filter);
return this._installKey(key, label, handler);
},
_ontoc: function(manager) {
var toc = JX.$('toc');
manager.scrollTo(toc);
},
_onreply: function(manager) {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.inline;
if (inline.canReply()) {
- manager.focusOn(null);
+ this.setFocus(null);
inline.reply();
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to reply to.'));
},
_onedit: function(manager) {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.inline;
if (inline.canEdit()) {
- manager.focusOn(null);
+ this.setFocus(null);
inline.edit();
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to edit.'));
},
_warnUser: function(message) {
new JX.Notification()
.setContent(message)
.alterClassName('jx-notification-alert', true)
.setDuration(1000)
.show();
},
_onjumpkey: function(delta, filter, manager) {
var state = this._getSelectionState();
var cursor = state.cursor;
var items = state.items;
// If there's currently no selection and the user tries to go back,
// don't do anything.
if ((cursor === null) && (delta < 0)) {
return;
}
while (true) {
if (cursor === null) {
cursor = 0;
} else {
cursor = cursor + delta;
}
// If we've gone backward past the first change, bail out.
if (cursor < 0) {
return;
}
// If we've gone forward off the end of the list, bail out.
if (cursor >= items.length) {
return;
}
// If we're selecting things of a particular type (like only files)
// and the next item isn't of that type, move past it.
if (filter !== null) {
if (items[cursor].type !== filter) {
continue;
}
}
// Otherwise, we've found a valid item to select.
break;
}
this._setSelectionState(items[cursor], manager);
},
_getSelectionState: function() {
var items = this._getSelectableItems();
var cursor = null;
if (this._cursorItem !== null) {
for (var ii = 0; ii < items.length; ii++) {
var item = items[ii];
if (this._cursorItem.target === item.target) {
cursor = ii;
break;
}
}
}
return {
cursor: cursor,
items: items
};
},
_setSelectionState: function(item, manager) {
this._cursorItem = item;
this._redrawSelection(manager, true);
return this;
},
_redrawSelection: function(manager, scroll) {
- manager = manager || this._lastKeyboardManager;
- this._lastKeyboardManager = manager;
-
- if (this.isAsleep()) {
- manager.focusOn(null);
- return;
- }
-
var cursor = this._cursorItem;
if (!cursor) {
- manager.focusOn(null);
+ this.setFocus(null);
return;
}
- manager.focusOn(cursor.nodes.begin, cursor.nodes.end);
+ this.setFocus(cursor.nodes.begin, cursor.nodes.end);
if (scroll) {
manager.scrollTo(cursor.nodes.begin);
}
return this;
},
_getSelectableItems: function() {
var result = [];
for (var ii = 0; ii < this._changesets.length; ii++) {
var items = this._changesets[ii].getSelectableItems();
for (var jj = 0; jj < items.length; jj++) {
result.push(items[jj]);
}
}
return result;
},
_onmore: function(e) {
e.kill();
var node = e.getNode('differential-changeset');
var changeset = this.getChangesetForNode(node);
var data = e.getNodeData('show-more');
var target = e.getNode('context-target');
changeset.loadContext(data.range, target);
},
_onmenu: function(e) {
var button = e.getNode('differential-view-options');
var data = JX.Stratcom.getData(button);
if (data.menu) {
// We've already built this menu, so we can let the menu itself handle
// the event.
return;
}
e.prevent();
var pht = this.getTranslations();
var node = JX.DOM.findAbove(
button,
'div',
'differential-changeset');
var changeset = this.getChangesetForNode(node);
var menu = new JX.PHUIXDropdownMenu(button);
var list = new JX.PHUIXActionListView();
var add_link = function(icon, name, href, local) {
if (!href) {
return;
}
var link = new JX.PHUIXActionView()
.setIcon(icon)
.setName(name)
.setHref(href)
.setHandler(function(e) {
if (local) {
window.location.assign(href);
} else {
window.open(href);
}
menu.close();
e.prevent();
});
list.addItem(link);
return link;
};
var reveal_item = new JX.PHUIXActionView()
.setIcon('fa-eye');
list.addItem(reveal_item);
var visible_item = new JX.PHUIXActionView()
.setHandler(function(e) {
var diff = JX.DOM.scry(
JX.$(data.containerID),
'table',
'differential-diff');
JX.Stratcom.invoke('differential-toggle-file', null, {diff: diff});
e.prevent();
menu.close();
});
list.addItem(visible_item);
add_link('fa-file-text', pht('Browse in Diffusion'), data.diffusionURI);
add_link('fa-file-o', pht('View Standalone'), data.standaloneURI);
var up_item = new JX.PHUIXActionView()
.setHandler(function(e) {
if (changeset.isLoaded()) {
var renderer = changeset.getRenderer();
if (renderer == '1up') {
renderer = '2up';
} else {
renderer = '1up';
}
changeset.setRenderer(renderer);
}
changeset.reload();
e.prevent();
menu.close();
});
list.addItem(up_item);
var encoding_item = new JX.PHUIXActionView()
.setIcon('fa-font')
.setName(pht('Change Text Encoding...'))
.setHandler(function(e) {
var params = {
encoding: changeset.getEncoding()
};
new JX.Workflow('/services/encoding/', params)
.setHandler(function(r) {
changeset.setEncoding(r.encoding);
changeset.reload();
})
.start();
e.prevent();
menu.close();
});
list.addItem(encoding_item);
var highlight_item = new JX.PHUIXActionView()
.setIcon('fa-sun-o')
.setName(pht('Highlight As...'))
.setHandler(function(e) {
var params = {
highlight: changeset.getHighlight()
};
new JX.Workflow('/services/highlight/', params)
.setHandler(function(r) {
changeset.setHighlight(r.highlight);
changeset.reload();
})
.start();
e.prevent();
menu.close();
});
list.addItem(highlight_item);
add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI);
add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI);
add_link('fa-pencil', pht('Open in Editor'), data.editor, true);
add_link('fa-wrench', pht('Configure Editor'), data.editorConfigure);
menu.setContent(list.getNode());
menu.listen('open', function() {
// When the user opens the menu, check if there are any "Show More"
// links in the changeset body. If there aren't, disable the "Show
// Entire File" menu item since it won't change anything.
var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');
if (nodes.length) {
reveal_item
.setDisabled(false)
.setName(pht('Show All Context'))
.setIcon('fa-file-o')
.setHandler(function(e) {
changeset.loadAllContext();
e.prevent();
menu.close();
});
} else {
reveal_item
.setDisabled(true)
.setIcon('fa-file')
.setName(pht('All Context Shown'))
.setHandler(function(e) { e.prevent(); });
}
encoding_item.setDisabled(!changeset.isLoaded());
highlight_item.setDisabled(!changeset.isLoaded());
if (changeset.isLoaded()) {
if (changeset.getRenderer() == '2up') {
up_item
.setIcon('fa-list-alt')
.setName(pht('View Unified'));
} else {
up_item
.setIcon('fa-files-o')
.setName(pht('View Side-by-Side'));
}
} else {
up_item
.setIcon('fa-refresh')
.setName(pht('Load Changes'));
}
visible_item
.setDisabled(true)
.setIcon('fa-expand')
.setName(pht('Can\'t Toggle Unloaded File'));
var diffs = JX.DOM.scry(
JX.$(data.containerID),
'table',
'differential-diff');
if (diffs.length > 1) {
JX.$E(
'More than one node with sigil "differential-diff" was found in "'+
data.containerID+'."');
} else if (diffs.length == 1) {
var diff = diffs[0];
visible_item.setDisabled(false);
if (JX.Stratcom.getData(diff).hidden) {
visible_item
.setName(pht('Expand File'))
.setIcon('fa-expand');
} else {
visible_item
.setName(pht('Collapse File'))
.setIcon('fa-compress');
}
} else {
// Do nothing when there is no diff shown in the table. For example,
// the file is binary.
}
});
data.menu = menu;
menu.open();
},
_onhide: function(e) {
this._onhidereveal(e, true);
},
_onreveal: function(e) {
this._onhidereveal(e, false);
},
_onhidereveal: function(e, is_hide) {
e.kill();
var inline = this._getInlineForEvent(e);
inline.setHidden(is_hide);
},
_onaction: function(action, e) {
e.kill();
var inline = this._getInlineForEvent(e);
var is_ref = false;
// If we don't have a natural inline object, the user may have clicked
// an action (like "Delete") inside a preview element at the bottom of
// the page.
// If they did, try to find an associated normal inline to act on, and
// pretend they clicked that instead. This makes the overall state of
// the page more consistent.
// However, there may be no normal inline (for example, because it is
// on a version of the diff which is not visible). In this case, we
// act by reference.
if (inline === null) {
var data = e.getNodeData('differential-inline-comment');
inline = this.getInlineByID(data.id);
if (inline) {
is_ref = true;
} else {
switch (action) {
case 'delete':
this._deleteInlineByID(data.id);
return;
}
}
}
// TODO: For normal operations, highlight the inline range here.
switch (action) {
case 'edit':
inline.edit();
break;
case 'done':
inline.toggleDone();
break;
case 'delete':
inline.delete(is_ref);
break;
case 'reply':
inline.reply();
break;
}
},
redrawPreview: function() {
// TODO: This isn't the cleanest way to find the preview form, but
// rendering no longer has direct access to it.
var forms = JX.DOM.scry(document.body, 'form', 'transaction-append');
if (forms.length) {
JX.DOM.invoke(forms[0], 'shouldRefresh');
}
},
+ setFocus: function(node, extended_node) {
+ this._focusStart = node;
+ this._focusEnd = extended_node;
+ this._redrawFocus();
+ },
+
+ _redrawFocus: function() {
+ var node = this._focusStart;
+ var extended_node = this._focusEnd || node;
+
+ var reticle = this._getFocusNode();
+ if (!node) {
+ JX.DOM.remove(reticle);
+ return;
+ }
+
+ // Outset the reticle some pixels away from the element, so there's some
+ // space between the focused element and the outline.
+ var p = JX.Vector.getPos(node);
+ var s = JX.Vector.getAggregateScrollForNode(node);
+
+ p.add(s).add(-4, -4).setPos(reticle);
+ // Compute the size we need to extend to the full extent of the focused
+ // nodes.
+ JX.Vector.getPos(extended_node)
+ .add(-p.x, -p.y)
+ .add(JX.Vector.getDim(extended_node))
+ .add(8, 8)
+ .setDim(reticle);
+
+ JX.DOM.getContentFrame().appendChild(reticle);
+ },
+
+ _getFocusNode: function() {
+ if (!this._focusNode) {
+ var node = JX.$N('div', {className : 'keyboard-focus-focus-reticle'});
+ this._focusNode = node;
+ }
+ return this._focusNode;
+ },
+
_deleteInlineByID: function(id) {
var uri = this.getInlineURI();
var data = {
op: 'refdelete',
id: id
};
var handler = JX.bind(this, this.redrawPreview);
new JX.Workflow(uri, data)
.setHandler(handler)
.start();
},
_getInlineForEvent: function(e) {
var node = e.getNode('differential-changeset');
if (!node) {
return null;
}
var changeset = this.getChangesetForNode(node);
var inline_row = e.getNode('inline-row');
return changeset.getInlineForRow(inline_row);
}
}
});
diff --git a/webroot/rsrc/js/core/KeyboardShortcutManager.js b/webroot/rsrc/js/core/KeyboardShortcutManager.js
index c9a1fbe64e..281c12a8f3 100644
--- a/webroot/rsrc/js/core/KeyboardShortcutManager.js
+++ b/webroot/rsrc/js/core/KeyboardShortcutManager.js
@@ -1,201 +1,158 @@
/**
* @provides phabricator-keyboard-shortcut-manager
* @requires javelin-install
* javelin-util
* javelin-stratcom
* javelin-dom
* javelin-vector
* @javelin
*/
JX.install('KeyboardShortcutManager', {
construct : function() {
this._shortcuts = [];
JX.Stratcom.listen('keypress', null, JX.bind(this, this._onkeypress));
JX.Stratcom.listen('keydown', null, JX.bind(this, this._onkeydown));
JX.Stratcom.listen('keyup', null, JX.bind(this, this._onkeyup));
},
statics : {
_instance : null,
/**
* Some keys don't invoke keypress events in some browsers. We handle these
* on keydown instead of keypress.
*/
_downkeys : {
left: 1,
right: 1,
up: 1,
down: 1
},
/**
* Some keys require Alt to be pressed in order to type them on certain
* keyboard layouts.
*/
_altkeys: {
// "Alt+L" on German layouts.
'@': 1,
// "Alt+Shift+7" on German layouts.
'\\': 1
},
getInstance : function() {
if (!JX.KeyboardShortcutManager._instance) {
JX.KeyboardShortcutManager._instance = new JX.KeyboardShortcutManager();
}
return JX.KeyboardShortcutManager._instance;
}
},
members : {
_shortcuts : null,
- _focusReticle : null,
/**
* Instead of calling this directly, you should call
* KeyboardShortcut.register().
*/
addKeyboardShortcut : function(s) {
this._shortcuts.push(s);
},
getShortcutDescriptions : function() {
var desc = [];
for (var ii = 0; ii < this._shortcuts.length; ii++) {
desc.push({
keys : this._shortcuts[ii].getKeys(),
description : this._shortcuts[ii].getDescription()
});
}
return desc;
},
/**
* Scroll an element into view.
*/
scrollTo : function(node) {
var scroll_distance = JX.Vector.getAggregateScrollForNode(node);
var node_position = JX.$V(node);
JX.DOM.scrollToPosition(0, node_position.y + scroll_distance.y - 60);
},
- /**
- * Move the keyboard shortcut focus to an element.
- *
- * @param Node Node to focus, or pass null to clear the focus.
- * @param Node To focus multiple nodes (like rows in a table), specify the
- * top-left node as the first parameter and the bottom-right
- * node as the focus extension.
- * @return void
- */
- focusOn : function(node, extended_node) {
- this._clearReticle();
-
- if (!node) {
- return;
- }
-
- var r = JX.$N('div', {className : 'keyboard-focus-focus-reticle'});
-
- extended_node = extended_node || node;
-
- // Outset the reticle some pixels away from the element, so there's some
- // space between the focused element and the outline.
- var p = JX.Vector.getPos(node);
- var s = JX.Vector.getAggregateScrollForNode(node);
-
- p.add(s).add(-4, -4).setPos(r);
- // Compute the size we need to extend to the full extent of the focused
- // nodes.
- JX.Vector.getPos(extended_node)
- .add(-p.x, -p.y)
- .add(JX.Vector.getDim(extended_node))
- .add(8, 8)
- .setDim(r);
- JX.DOM.getContentFrame().appendChild(r);
-
- this._focusReticle = r;
- },
-
- _clearReticle : function() {
- this._focusReticle && JX.DOM.remove(this._focusReticle);
- this._focusReticle = null;
- },
_onkeypress : function(e) {
if (!(this._getKey(e) in JX.KeyboardShortcutManager._downkeys)) {
this._onkeyhit(e);
}
},
_onkeyhit : function(e) {
var self = JX.KeyboardShortcutManager;
var raw = e.getRawEvent();
if (raw.ctrlKey || raw.metaKey) {
// Never activate keyboard shortcuts if modifier keys are also
// depressed.
return;
}
// For most keystrokes, don't activate keyboard shortcuts if the Alt
// key is depressed. However, we continue if the character requires the
// use of Alt to type it on some keyboard layouts.
var key = this._getKey(e);
if (raw.altKey && !(key in self._altkeys)) {
return;
}
var target = e.getTarget();
var ignore = ['input', 'select', 'textarea', 'object', 'embed'];
if (JX.DOM.isType(target, ignore)) {
// Never activate keyboard shortcuts if the user has some other control
// focused.
return;
}
var key = this._getKey(e);
var shortcuts = this._shortcuts;
for (var ii = 0; ii < shortcuts.length; ii++) {
var keys = shortcuts[ii].getKeys();
for (var jj = 0; jj < keys.length; jj++) {
if (keys[jj] == key) {
shortcuts[ii].getHandler()(this);
e.kill(); // Consume the event
return;
}
}
}
},
_onkeydown : function(e) {
this._handleTooltipKeyEvent(e, true);
if (this._getKey(e) in JX.KeyboardShortcutManager._downkeys) {
this._onkeyhit(e);
}
},
_onkeyup : function(e) {
this._handleTooltipKeyEvent(e, false);
},
_getKey : function(e) {
return e.getSpecialKey() || String.fromCharCode(e.getRawEvent().charCode);
},
_handleTooltipKeyEvent : function(e, is_keydown) {
if (e.getRawEvent().keyCode != 18) {
// If this isn't the alt/option key, don't do anything.
return;
}
// Fire all the shortcut handlers.
var shortcuts = this._shortcuts;
for (var ii = 0; ii < shortcuts.length; ii++) {
var handler = shortcuts[ii].getTooltipHandler();
handler && handler(this, is_keydown);
}
}
}
});
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jul 31, 11:18 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
16432378
Default Alt Text
(25 KB)
Attached To
Mode
rP Phabricator
Attached
Detach File
Event Timeline
Log In to Comment