1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-18 11:30:55 +01:00

Consolidate changeset rendering logic

Summary:
Ref T5179. Currently, all the changeset rendering logic is in the "populate" behavior, and a lot of it comes in via configuration and is hard to get at.

Instead, surface an object which can control it, and which other behaviors can access more easily.

In particular, this allows us to add a "Load/Reload" item to the view options menu, which would previously have been very challenging.

Load/Reload isn't useful on its own, but is a step away from "Show whitespace as...", "Highlight as...", "Show tabtops as...", "View Unified", "View Side-By-Side", etc.

Test Plan:
  - Viewed Differential.
  - Viewed Diffusion.
  - Viewed large changesets, clicked "Load".
  - Used "Load" and "Reload" from view options menu.
  - Loaded all changes in a large diff, verified "Load" and TOC clicks take precedence over other content loads.
  - Played with content stability stuff.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5179

Differential Revision: https://secure.phabricator.com/D9286
This commit is contained in:
epriestley 2014-05-25 07:13:22 -07:00
parent 4b9765b896
commit 7d0d6fbcf2
7 changed files with 445 additions and 175 deletions

View file

@ -11,7 +11,7 @@ return array(
'core.pkg.js' => '639b2433',
'darkconsole.pkg.js' => 'ca8671ce',
'differential.pkg.css' => 'fbf57382',
'differential.pkg.js' => '74cb0d29',
'differential.pkg.js' => 'eca39a2c',
'diffusion.pkg.css' => '3783278d',
'diffusion.pkg.js' => '077e3ad0',
'maniphest.pkg.css' => 'f88a8402',
@ -352,15 +352,16 @@ return array(
'rsrc/js/application/countdown/timer.js' => '889c96f3',
'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e',
'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'fa187a68',
'rsrc/js/application/differential/ChangesetViewManager.js' => 'db09a523',
'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746',
'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b',
'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79',
'rsrc/js/application/differential/behavior-comment-preview.js' => '127f2018',
'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1',
'rsrc/js/application/differential/behavior-dropdown-menus.js' => '9f0dfafa',
'rsrc/js/application/differential/behavior-dropdown-menus.js' => '64a79839',
'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '00861799',
'rsrc/js/application/differential/behavior-keyboard-nav.js' => '173ce7e7',
'rsrc/js/application/differential/behavior-populate.js' => 'dfdf9f34',
'rsrc/js/application/differential/behavior-populate.js' => 'bdb3e4d0',
'rsrc/js/application/differential/behavior-show-all-comments.js' => '7c273581',
'rsrc/js/application/differential/behavior-show-field-details.js' => '441f2137',
'rsrc/js/application/differential/behavior-show-more.js' => 'dd7e8ef5',
@ -502,6 +503,7 @@ return array(
'aphront-two-column-view-css' => '16ab3ad2',
'aphront-typeahead-control-css' => 'a989b5b3',
'auth-css' => '1e655982',
'changeset-view-manager' => 'db09a523',
'config-options-css' => '7fedf08b',
'conpherence-menu-css' => 'e1e0fdf1',
'conpherence-message-pane-css' => '7703a9a9',
@ -554,11 +556,11 @@ return array(
'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b',
'javelin-behavior-differential-comment-jump' => '71755c79',
'javelin-behavior-differential-diff-radios' => 'e1ff79b1',
'javelin-behavior-differential-dropdown-menus' => '9f0dfafa',
'javelin-behavior-differential-dropdown-menus' => '64a79839',
'javelin-behavior-differential-edit-inline-comments' => '00861799',
'javelin-behavior-differential-feedback-preview' => '127f2018',
'javelin-behavior-differential-keyboard-navigation' => '173ce7e7',
'javelin-behavior-differential-populate' => 'dfdf9f34',
'javelin-behavior-differential-populate' => 'bdb3e4d0',
'javelin-behavior-differential-show-field-details' => '441f2137',
'javelin-behavior-differential-show-more' => 'dd7e8ef5',
'javelin-behavior-differential-toggle-files' => 'ca3f91eb',
@ -1251,12 +1253,31 @@ return array(
2 => 'javelin-util',
3 => 'phabricator-shaped-request',
),
'62e18640' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-typeahead-normalizer',
),
'6453c869' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-fx',
),
'64a79839' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-stratcom',
4 => 'phuix-dropdown-menu',
5 => 'phuix-action-list-view',
6 => 'phuix-action-view',
7 => 'phabricator-phtize',
8 => 'changeset-view-manager',
),
'64ef2fd2' =>
array(
0 => 'javelin-behavior',
@ -1309,13 +1330,6 @@ return array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
),
'62e18640' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-typeahead-normalizer',
),
'76f4ebed' =>
array(
0 => 'javelin-install',
@ -1552,17 +1566,6 @@ return array(
2 => 'javelin-uri',
3 => 'javelin-request',
),
'9f0dfafa' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-stratcom',
4 => 'phuix-dropdown-menu',
5 => 'phuix-action-list-view',
6 => 'phuix-action-view',
7 => 'phabricator-phtize',
),
'a3e2244e' =>
array(
0 => 'javelin-behavior',
@ -1725,6 +1728,14 @@ return array(
2 => 'javelin-util',
3 => 'javelin-request',
),
'bdb3e4d0' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'phabricator-tooltip',
4 => 'changeset-view-manager',
),
'be81801d' =>
array(
0 => 'javelin-behavior',
@ -1883,6 +1894,17 @@ return array(
1 => 'javelin-util',
2 => 'javelin-stratcom',
),
'db09a523' =>
array(
0 => 'javelin-dom',
1 => 'javelin-util',
2 => 'javelin-stratcom',
3 => 'javelin-install',
4 => 'javelin-workflow',
5 => 'javelin-router',
6 => 'javelin-behavior-device',
7 => 'javelin-vector',
),
'dd7e8ef5' =>
array(
0 => 'javelin-behavior',
@ -1897,18 +1919,6 @@ return array(
1 => 'javelin-dom',
2 => 'phabricator-prefab',
),
'dfdf9f34' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-workflow',
2 => 'javelin-util',
3 => 'javelin-dom',
4 => 'javelin-stratcom',
5 => 'javelin-behavior-device',
6 => 'javelin-vector',
7 => 'javelin-router',
8 => 'phabricator-tooltip',
),
'e1ff79b1' =>
array(
0 => 'javelin-behavior',

View file

@ -8,6 +8,46 @@ final class DifferentialChangesetDetailView extends AphrontView {
private $symbolIndex;
private $id;
private $vsChangesetID;
private $renderURI;
private $whitespace;
private $renderingRef;
private $autoload;
public function setAutoload($autoload) {
$this->autoload = $autoload;
return $this;
}
public function getAutoload() {
return $this->autoload;
}
public function setRenderingRef($rendering_ref) {
$this->renderingRef = $rendering_ref;
return $this;
}
public function getRenderingRef() {
return $this->renderingRef;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function getWhitespace() {
return $this->whitespace;
}
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
}
public function getRenderURI() {
return $this->renderURI;
}
public function setChangeset($changeset) {
$this->changeset = $changeset;
@ -36,6 +76,11 @@ final class DifferentialChangesetDetailView extends AphrontView {
return $this->id;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setVsChangesetID($vs_changeset_id) {
$this->vsChangesetID = $vs_changeset_id;
return $this;
@ -139,6 +184,12 @@ final class DifferentialChangesetDetailView extends AphrontView {
$this->getVsChangesetID(),
$this->changeset->getID()),
'right' => $this->changeset->getID(),
'renderURI' => $this->getRenderURI(),
'whitespace' => $this->getWhitespace(),
'highlight' => null,
'renderer' => null,
'ref' => $this->getRenderingRef(),
'autoload' => $this->getAutoload(),
),
'class' => $class,
'id' => $id,
@ -154,9 +205,15 @@ final class DifferentialChangesetDetailView extends AphrontView {
'class' => 'differential-file-icon-header'),
array(
$icon,
$display_filename)),
phutil_tag('div', array('style' => 'clear: both'), ''),
$this->renderChildren(),
$display_filename,
)),
javelin_tag(
'div',
array(
'class' => 'changeset-view-content',
'sigil' => 'changeset-view-content',
),
$this->renderChildren()),
));
}

View file

@ -131,8 +131,9 @@ final class DifferentialChangesetListView extends AphrontView {
));
$output = array();
$mapping = array();
$ids = array();
foreach ($changesets as $key => $changeset) {
$file = $changeset->getFilename();
$class = 'differential-changeset';
if (!$this->inlineURI) {
@ -143,6 +144,9 @@ final class DifferentialChangesetListView extends AphrontView {
$detail = new DifferentialChangesetDetailView();
$uniq_id = 'diff-'.$changeset->getAnchorName();
$detail->setID($uniq_id);
$view_options = $this->renderViewOptionsDropdown(
$detail,
$ref,
@ -153,22 +157,24 @@ final class DifferentialChangesetListView extends AphrontView {
$detail->setSymbolIndex(idx($this->symbolIndexes, $key));
$detail->setVsChangesetID(idx($this->vsMap, $changeset->getID()));
$detail->setEditable(true);
$detail->setRenderingRef($ref);
$detail->setAutoload(isset($this->visibleChangesets[$key]));
$detail->setRenderURI($this->renderURI);
$detail->setWhitespace($this->whitespace);
$uniq_id = 'diff-'.$changeset->getAnchorName();
if (isset($this->visibleChangesets[$key])) {
$load = 'Loading...';
$mapping[$uniq_id] = $ref;
} else {
$load = javelin_tag(
'a',
array(
'href' => '#'.$uniq_id,
'sigil' => 'differential-load',
'meta' => array(
'id' => $uniq_id,
'ref' => $ref,
'id' => $detail->getID(),
'kill' => true,
),
'sigil' => 'differential-load',
'mustcapture' => true,
),
pht('Load'));
@ -181,14 +187,14 @@ final class DifferentialChangesetListView extends AphrontView {
),
phutil_tag('div', array('class' => 'differential-loading'), $load)));
$output[] = $detail->render();
$ids[] = $detail->getID();
}
$this->requireResource('aphront-tooltip-css');
$this->initBehavior('differential-populate', array(
'registry' => $mapping,
'whitespace' => $this->whitespace,
'uri' => $this->renderURI,
'changesetViewIDs' => $ids,
));
$this->initBehavior('differential-show-more', array(

View file

@ -312,11 +312,10 @@ final class DifferentialDiffTableOfContentsView extends AphrontView {
'a',
array(
'href' => '#'.$changeset->getAnchorName(),
'sigil' => 'differential-load',
'meta' => array(
'id' => 'diff-'.$changeset->getAnchorName(),
'ref' => $ref,
),
'sigil' => 'differential-load',
),
$display_file);
}

View file

@ -0,0 +1,269 @@
/**
* @provides changeset-view-manager
* @requires javelin-dom
* javelin-util
* javelin-stratcom
* javelin-install
* javelin-workflow
* javelin-router
* javelin-behavior-device
* javelin-vector
*/
JX.install('ChangesetViewManager', {
construct : function(node) {
this._node = node;
var data = this._getNodeData();
this._renderURI = data.renderURI;
this._ref = data.ref;
this._whitespace = data.whitespace;
this._renderer = data.renderer;
this._highlight = data.highlight;
},
members: {
_node: null,
_loaded: false,
_sequence: 0,
_stabilize: false,
_renderURI: null,
_ref: null,
_whitespace: null,
_renderer: null,
_highlight: null,
/**
* Has the content of this changeset been loaded?
*
* This method returns `true` if a request has been fired, even if the
* response has not returned yet.
*
* @return bool True if the content has been loaded.
*/
isLoaded: function() {
return this._loaded;
},
/**
* Configure stabilization of the document position on content load.
*
* When we dump the changeset into the document, we can try to stabilize
* the document scroll position so that the user doesn't feel like they
* are jumping around as things load in. This is generally useful when
* populating initial changes.
*
* However, if a user explicitly requests a content load by clicking a
* "Load" link or using the dropdown menu, this stabilization generally
* feels unnatural, so we don't use it in response to explicit user action.
*
* @param bool True to stabilize the next content fill.
* @return this
*/
setStabilize: function(stabilize) {
this._stabilize = stabilize;
return this;
},
/**
* Should this changeset load immediately when the page loads?
*
* Normally, changes load immediately, but if a diff or commit is very
* large we stop doing this and have the user load files explicitly, or
* choose to load everything.
*
* @return bool True if the changeset should load automatically when the
* page loads.
*/
shouldAutoload: function() {
return this._getNodeData().autoload;
},
/**
* Load this changeset, if it isn't already loading.
*
* This fires a request to fill the content of this changeset, provided
* there isn't already a request in flight. To force a reload, use
* @{method:reload}.
*
* @return this
*/
load: function() {
if (this._loaded) {
return this;
}
return this.reload();
},
/**
* Reload the changeset content.
*
* This method always issues a request, even if the content is already
* loading. To load conditionally, use @{method:load}.
*
* @return this
*/
reload: function() {
this._loaded = true;
this._sequence++;
var data = this._getNodeData();
var params = {
ref: this._ref,
whitespace: this._whitespace,
renderer: this._getRenderer()
};
var workflow = new JX.Workflow(this._renderURI, params)
.setHandler(JX.bind(this, this._onresponse, this._sequence));
var routable = workflow.getRoutable();
routable
.setPriority(500)
.setType('content')
.setKey(this._getRoutableKey());
JX.Router.getInstance().queue(routable);
JX.DOM.setContent(
this._getContentFrame(),
JX.$N(
'div',
{className: 'differential-loading'},
'Loading...'));
return this;
},
/**
* Get the active @{class:JX.Routable} for this changeset.
*
* After issuing a request with @{method:load} or @{method:reload}, you
* can adjust routable settings (like priority) by querying the routable
* with this method. Note that there may not be a current routable.
*
* @return JX.Routable|null Active routable, if one exists.
*/
getRoutable: function() {
return JX.Router.getInstance().getRoutableByKey(this._getRoutableKey());
},
_getRenderer: function() {
// TODO: This is a big pile of TODOs.
// NOTE: If you load the page at one device resolution and then resize to
// a different one we don't re-render the diffs, because it's a
// complicated mess and you could lose inline comments, cursor positions,
// etc.
var renderer = (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
// TODO: Once 1up works better, figure out when to show it.
renderer = '2up';
return renderer;
},
_getNodeData: function() {
return JX.Stratcom.getData(this._node);
},
_onresponse: function(sequence, response) {
if (sequence != this._sequence) {
// If this isn't the most recent request, ignore it. This normally
// means the user changed view settings between the time the page loaded
// and the content filled.
return;
}
// As we populate the changeset list, we try to hold the document scroll
// position steady, so that, e.g., users who want to leave a comment on a
// diff with a large number of changes don't constantly have the text
// area scrolled off the bottom of the screen until the entire diff loads.
//
// There are two three major cases here:
//
// - If we're near the top of the document, never scroll.
// - If we're near the bottom of the document, always scroll.
// - Otherwise, scroll if the changes were above the midline of the
// viewport.
var target = this._node;
var old_pos = JX.Vector.getScroll();
var old_view = JX.Vector.getViewport();
var old_dim = JX.Vector.getDocument();
// Number of pixels away from the top or bottom of the document which
// count as "nearby".
var sticky = 480;
var near_top = (old_pos.y <= sticky);
var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
var target_pos = JX.Vector.getPos(target);
var target_dim = JX.Vector.getDim(target);
var target_mid = (target_pos.y + (target_dim.y / 2));
var view_mid = (old_pos.y + (old_view.y / 2));
var above_mid = (target_mid < view_mid);
var frame = this._getContentFrame();
JX.DOM.setContent(frame, JX.$H(response.changeset));
if (this._stabilize) {
if (!near_top) {
if (near_bot || above_mid) {
// Figure out how much taller the document got.
var delta = (JX.Vector.getDocument().y - old_dim.y);
window.scrollTo(old_pos.x, old_pos.y + delta);
}
}
this._stabilize = false;
}
if (response.coverage) {
for (var k in response.coverage) {
try {
JX.DOM.replace(JX.$(k), JX.$H(response.coverage[k]));
} catch (ignored) {
// Not terribly important.
}
}
}
},
_getContentFrame: function() {
return JX.DOM.find(this._node, 'div', 'changeset-view-content');
},
_getRoutableKey: function() {
return 'changeset-view.' + this._ref + '.' + this._sequence;
}
},
statics: {
getForNode: function(node) {
var data = JX.Stratcom.getData(node);
if (!data.changesetViewManager) {
data.changesetViewManager = new JX.ChangesetViewManager(node);
}
return data.changesetViewManager;
}
}
});

View file

@ -8,6 +8,7 @@
* phuix-action-list-view
* phuix-action-view
* phabricator-phtize
* changeset-view-manager
*/
JX.behavior('differential-dropdown-menus', function(config) {
@ -97,8 +98,24 @@ JX.behavior('differential-dropdown-menus', function(config) {
});
list.addItem(visible_item);
add_link('fa-files-o', pht('Browse in Diffusion'), data.diffusionURI);
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) {
var changeset = JX.DOM.findAbove(
button,
'div',
'differential-changeset');
var view = JX.ChangesetViewManager.getForNode(changeset);
view.reload();
e.prevent();
menu.close();
});
list.addItem(up_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);
@ -108,6 +125,12 @@ JX.behavior('differential-dropdown-menus', function(config) {
menu.setContent(list.getNode());
menu.listen('open', function() {
var changeset = JX.DOM.findAbove(
button,
'div',
'differential-changeset');
var view = JX.ChangesetViewManager.getForNode(changeset);
// 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
@ -132,8 +155,22 @@ JX.behavior('differential-dropdown-menus', function(config) {
.setHandler(function(e) { e.prevent(); });
}
visible_item.setDisabled(true);
visible_item.setName(pht("Can't Toggle Unloaded File"));
// TODO: This is temporary and just makes testing easier. It will do
// some mojo soon.
if (view.isLoaded()) {
up_item
.setIcon('fa-refresh')
.setName('Reload');
} else {
up_item
.setIcon('fa-refresh')
.setName('Load');
}
visible_item
.setDisabled(true)
.setIcon('fa-expand')
.setName(pht("Can't Toggle Unloaded File"));
var diffs = JX.DOM.scry(
JX.$(data.containerID),
'table',

View file

@ -1,151 +1,44 @@
/**
* @provides javelin-behavior-differential-populate
* @requires javelin-behavior
* javelin-workflow
* javelin-util
* javelin-dom
* javelin-stratcom
* javelin-behavior-device
* javelin-vector
* javelin-router
* phabricator-tooltip
* changeset-view-manager
*/
JX.behavior('differential-populate', function(config) {
function onresponse(target_id, response) {
// As we populate the diff, we try to hold the document scroll position
// steady, so that, e.g., users who want to leave a comment on a diff with a
// large number of changes don't constantly have the text area scrolled off
// the bottom of the screen until the entire diff loads.
//
// There are two three major cases here:
//
// - If we're near the top of the document, never scroll.
// - If we're near the bottom of the document, always scroll.
// - Otherwise, scroll if the changes were above the midline of the
// viewport.
var target = JX.$(target_id);
var old_pos = JX.Vector.getScroll();
var old_view = JX.Vector.getViewport();
var old_dim = JX.Vector.getDocument();
// Number of pixels away from the top or bottom of the document which
// count as "nearby".
var sticky = 480;
var near_top = (old_pos.y <= sticky);
var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
var target_pos = JX.Vector.getPos(target);
var target_dim = JX.Vector.getDim(target);
var target_mid = (target_pos.y + (target_dim.y / 2));
var view_mid = (old_pos.y + (old_view.y / 2));
var above_mid = (target_mid < view_mid);
JX.DOM.replace(target, JX.$H(response.changeset));
if (!near_top) {
if (near_bot || above_mid) {
// Figure out how much taller the document got.
var delta = (JX.Vector.getDocument().y - old_dim.y);
window.scrollTo(old_pos.x, old_pos.y + delta);
}
}
if (response.coverage) {
for (var k in response.coverage) {
try {
JX.DOM.replace(JX.$(k), JX.$H(response.coverage[k]));
} catch (ignored) {
// Not terribly important.
}
}
for (var ii = 0; ii < config.changesetViewIDs.length; ii++) {
var id = config.changesetViewIDs[ii];
var view = JX.ChangesetViewManager.getForNode(JX.$(id));
if (view.shouldAutoload()) {
view.setStabilize(true).load();
}
}
// NOTE: If you load the page at one device resolution and then resize to
// a different one we don't re-render the diffs, because it's a complicated
// mess and you could lose inline comments, cursor positions, etc.
var renderer = (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
// TODO: Once 1up works better, figure out when to show it.
renderer = '2up';
var get_key = function(id) {
return 'differential-populate.' + id;
};
var load = function(id, data) {
var routable = new JX.Workflow(config.uri, data)
.setHandler(JX.bind(null, onresponse, id))
.getRoutable();
routable
.setPriority(500)
.setType('content')
.setKey(get_key(id));
JX.Router.getInstance().queue(routable);
return routable;
};
for (var k in config.registry) {
var data = {
ref : config.registry[k],
whitespace: config.whitespace,
renderer: renderer
};
load(k, data);
}
var highlighted = null;
var highlight_class = null;
JX.Stratcom.listen(
'click',
'differential-load',
function(e) {
var meta = e.getNodeData('differential-load');
var diff;
try {
diff = JX.$(meta.id);
} catch (ex) {
// Already loaded.
}
if (diff) {
JX.DOM.setContent(
diff,
JX.$H('<div class="differential-loading">Loading...</div>'));
// When a user explicitly clicks "Load" (or clicks a link in the table
// of contents) prioritize this request if it already exists. If it
// doesn't, make a new high-priority request.
var key = get_key(meta.id);
var routable = JX.Router.getInstance().getRoutableByKey(key);
if (!routable) {
var data = {
ref : meta.ref,
whitespace : config.whitespace
};
routable = load(meta.id, data);
}
var changeset = JX.$(meta.id);
var view = JX.ChangesetViewManager.getForNode(changeset);
view.load();
var routable = view.getRoutable();
if (routable) {
routable.setPriority(2000);
}
if (meta.kill) {
e.kill();
}
});
var highlighted = null;
var highlight_class = null;
JX.Stratcom.listen(
['mouseover', 'mouseout'],
['differential-changeset', 'tag:td'],
@ -202,5 +95,4 @@ JX.behavior('differential-populate', function(config) {
});
});