Bug 1156205: show a scrollbar in the social share dropdown list inside the Loop conversation window when the number of items exceeds the maximum height. r=Standard8

This commit is contained in:
Mike de Boer 2015-05-08 10:32:46 +02:00
parent 6066ad2557
commit 9191fc7264
17 changed files with 299 additions and 167 deletions

View File

@ -161,6 +161,9 @@ loop.conversation = (function(mozL10n) {
mozLoop: navigator.mozLoop}
), document.querySelector('#main'));
document.body.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId
}));

View File

@ -161,6 +161,9 @@ loop.conversation = (function(mozL10n) {
mozLoop={navigator.mozLoop}
/>, document.querySelector('#main'));
document.body.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId
}));

View File

@ -140,7 +140,7 @@ loop.conversationViews = (function(mozL10n) {
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var AcceptCallView = React.createClass({displayName: "AcceptCallView",
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
callType: React.PropTypes.string.isRequired,

View File

@ -140,7 +140,7 @@ loop.conversationViews = (function(mozL10n) {
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var AcceptCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
callType: React.PropTypes.string.isRequired,

View File

@ -117,7 +117,7 @@ loop.panel = (function(_, mozL10n) {
* Availability drop down menu subview.
*/
var AvailabilityDropdown = React.createClass({displayName: "AvailabilityDropdown",
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
getInitialState: function() {
return {
@ -322,7 +322,7 @@ loop.panel = (function(_, mozL10n) {
mozLoop: React.PropTypes.object.isRequired
},
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
mixins: [sharedMixins.DropdownMenuMixin(), sharedMixins.WindowCloseMixin],
handleClickSettingsEntry: function() {
// XXX to be implemented at the same time as unhiding the entry
@ -941,6 +941,7 @@ loop.panel = (function(_, mozL10n) {
), document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
// Notify the window that we've finished initalization and initial layout
var evtObject = document.createEvent('Event');

View File

@ -117,7 +117,7 @@ loop.panel = (function(_, mozL10n) {
* Availability drop down menu subview.
*/
var AvailabilityDropdown = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
getInitialState: function() {
return {
@ -322,7 +322,7 @@ loop.panel = (function(_, mozL10n) {
mozLoop: React.PropTypes.object.isRequired
},
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
mixins: [sharedMixins.DropdownMenuMixin(), sharedMixins.WindowCloseMixin],
handleClickSettingsEntry: function() {
// XXX to be implemented at the same time as unhiding the entry
@ -941,6 +941,7 @@ loop.panel = (function(_, mozL10n) {
/>, document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
// Notify the window that we've finished initalization and initial layout
var evtObject = document.createEvent('Event');

View File

@ -113,6 +113,7 @@ loop.roomViews = (function(mozL10n) {
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"visually-hidden": true,
"share-button-unavailable": !this.props.socialShareButtonAvailable,
"hide": !this.props.show
});
@ -170,7 +171,7 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -185,7 +186,8 @@ loop.roomViews = (function(mozL10n) {
getInitialState: function() {
return {
copiedUrl: false,
editMode: false
editMode: false,
newRoomName: ""
};
},
@ -239,31 +241,31 @@ loop.roomViews = (function(mozL10n) {
React.createElement("a", {className: cx({hide: !canAddContext, "room-invitation-addcontext": true}),
onClick: this.handleAddContextClick},
mozL10n.get("context_add_some_label")
),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: "btn btn-info btn-email",
onClick: this.handleEmailButtonClick},
mozL10n.get("email_link_button")
),
React.createElement("button", {className: "btn btn-info btn-copy",
onClick: this.handleCopyButtonClick},
this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")
),
React.createElement("button", {className: "btn btn-info btn-share",
ref: "anchor",
onClick: this.handleShareButtonClick},
mozL10n.get("share_button3")
)
),
React.createElement(SocialShareDropdown, {
dispatcher: this.props.dispatcher,
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareButtonAvailable: this.props.socialShareButtonAvailable,
socialShareProviders: this.props.socialShareProviders,
ref: "menu"})
)
),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: "btn btn-info btn-email",
onClick: this.handleEmailButtonClick},
mozL10n.get("email_link_button")
),
React.createElement("button", {className: "btn btn-info btn-copy",
onClick: this.handleCopyButtonClick},
this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")
),
React.createElement("button", {className: "btn btn-info btn-share",
ref: "anchor",
onClick: this.handleShareButtonClick},
mozL10n.get("share_button3")
)
),
React.createElement(SocialShareDropdown, {
dispatcher: this.props.dispatcher,
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareButtonAvailable: this.props.socialShareButtonAvailable,
socialShareProviders: this.props.socialShareProviders,
ref: "menu"}),
React.createElement(DesktopRoomContextView, {
dispatcher: this.props.dispatcher,
editMode: this.state.editMode,

View File

@ -113,6 +113,7 @@ loop.roomViews = (function(mozL10n) {
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"visually-hidden": true,
"share-button-unavailable": !this.props.socialShareButtonAvailable,
"hide": !this.props.show
});
@ -170,7 +171,7 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -185,7 +186,8 @@ loop.roomViews = (function(mozL10n) {
getInitialState: function() {
return {
copiedUrl: false,
editMode: false
editMode: false,
newRoomName: ""
};
},
@ -240,30 +242,30 @@ loop.roomViews = (function(mozL10n) {
onClick={this.handleAddContextClick}>
{mozL10n.get("context_add_some_label")}
</a>
<div className="btn-group call-action-group">
<button className="btn btn-info btn-email"
onClick={this.handleEmailButtonClick}>
{mozL10n.get("email_link_button")}
</button>
<button className="btn btn-info btn-copy"
onClick={this.handleCopyButtonClick}>
{this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")}
</button>
<button className="btn btn-info btn-share"
ref="anchor"
onClick={this.handleShareButtonClick}>
{mozL10n.get("share_button3")}
</button>
</div>
<SocialShareDropdown
dispatcher={this.props.dispatcher}
roomUrl={this.props.roomData.roomUrl}
show={this.state.showMenu}
socialShareButtonAvailable={this.props.socialShareButtonAvailable}
socialShareProviders={this.props.socialShareProviders}
ref="menu" />
</div>
<div className="btn-group call-action-group">
<button className="btn btn-info btn-email"
onClick={this.handleEmailButtonClick}>
{mozL10n.get("email_link_button")}
</button>
<button className="btn btn-info btn-copy"
onClick={this.handleCopyButtonClick}>
{this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")}
</button>
<button className="btn btn-info btn-share"
ref="anchor"
onClick={this.handleShareButtonClick}>
{mozL10n.get("share_button3")}
</button>
</div>
<SocialShareDropdown
dispatcher={this.props.dispatcher}
roomUrl={this.props.roomData.roomUrl}
show={this.state.showMenu}
socialShareButtonAvailable={this.props.socialShareButtonAvailable}
socialShareProviders={this.props.socialShareProviders}
ref="menu" />
<DesktopRoomContextView
dispatcher={this.props.dispatcher}
editMode={this.state.editMode}

View File

@ -426,9 +426,9 @@ p {
border-radius: 2px;
}
body[dir=rtl] .dropdown-menu-item {
body[dir=rtl] .dropdown-menu {
left: auto;
right: 10px;
right: 0;
}
.dropdown-menu-item {

View File

@ -866,7 +866,6 @@ html, .fx-embedded, #main,
}
.room-invitation-content {
order: 1;
flex: 1 1 auto;
display: flex;
flex-flow: column nowrap;
@ -882,7 +881,7 @@ html, .fx-embedded, #main,
}
.room-invitation-overlay .btn-group {
padding: 0;
padding: 0 0 1em 0;
}
.room-invitation-addcontext {
@ -913,6 +912,14 @@ body[dir="rtl"] .room-invitation-addcontext {
text-align: start;
bottom: auto;
top: 0;
overflow: hidden;
overflow-y: auto;
}
/* When the dropdown is showing a vertical scrollbar, compensate for its width. */
body[platform="other"] .share-service-dropdown.overflow > .dropdown-menu-item,
body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
-moz-padding-end: 20px;
}
.share-service-dropdown.share-button-unavailable {
@ -988,7 +995,6 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
position: relative;
left: auto;
bottom: auto;
order: 2;
flex: 0 1 auto;
}

View File

@ -83,110 +83,165 @@ loop.shared.mixins = (function() {
/**
* Dropdown menu mixin.
*
* @param {Sring} [boundingBoxSelector] Selector that points to an element that
* defines the constraints this dropdown
* is shown within. If not provided,
* `document.body` is assumed to be the
* constraining element.
* @type {Object}
*/
var DropdownMenuMixin = {
get documentBody() {
return rootObject.document.body;
},
var DropdownMenuMixin = function(boundingBoxSelector) {
return {
get documentBody() {
return rootObject.document.body;
},
getInitialState: function() {
return {showMenu: false};
},
getInitialState: function() {
return {
showMenu: false
};
},
_onBodyClick: function(event) {
var menuButton = this.refs["menu-button"] && this.refs["menu-button"].getDOMNode();
if (this.refs.anchor) {
menuButton = this.refs.anchor.getDOMNode();
_onBodyClick: function(event) {
var menuButton = this.refs["menu-button"] && this.refs["menu-button"].getDOMNode();
if (this.refs.anchor) {
menuButton = this.refs.anchor.getDOMNode();
}
// If a menu button/ anchor is defined and clicked on, it will be in charge
// of hiding or showing the popup.
if (event.target !== menuButton) {
this.setState({ showMenu: false });
}
},
_correctMenuPosition: function() {
var menu = this.refs.menu && this.refs.menu.getDOMNode();
if (!menu) {
return;
}
if (menu.style.maxWidth)
menu.style.maxWidth = "none";
if (menu.style.maxHeight)
menu.style.maxHeight = "none";
// Correct the position of the menu only if necessary.
var x, y, boundingBox, boundingRect;
// Amount of pixels that the dropdown needs to stay away from the edges of
// the page body.
var boundOffset = 4;
var menuNodeRect = menu.getBoundingClientRect();
// If the menu dimensions are constrained to a bounding element, instead of
// the document body, find that element.
if (boundingBoxSelector) {
boundingBox = this.documentBody.querySelector(boundingBoxSelector);
if (boundingBox) {
boundingRect = boundingBox.getBoundingClientRect();
}
}
if (!boundingRect) {
boundingRect = {
height: this.documentBody.offsetHeight,
left: 0,
top: 0,
width: this.documentBody.offsetWidth
};
}
// Make sure the menu position will be a certain fixed amount of pixels away
// from the border of the bounding box.
boundingRect.width -= boundOffset;
boundingRect.height -= boundOffset;
var x = menuNodeRect.left;
var y = menuNodeRect.top;
// If there's an anchor present, position it relative to it first.
var anchor = this.refs.anchor && this.refs.anchor.getDOMNode();
if (anchor) {
// XXXmikedeboer: at the moment we only support positioning centered above
// anchor node. Please add more modes as necessary.
var anchorNodeRect = anchor.getBoundingClientRect();
// Because we're _correcting_ the position of the dropdown, we assume that
// the node is positioned absolute at 0,0 coordinates (top left).
x = Math.floor(anchorNodeRect.left - (menuNodeRect.width / 2) + (anchorNodeRect.width / 2));
y = Math.floor(anchorNodeRect.top - menuNodeRect.height - anchorNodeRect.height);
}
var overflowX = false;
var overflowY = false;
// Check the horizontal overflow.
if (x + menuNodeRect.width > boundingRect.width) {
// Anchor positioning is already relative, so don't subtract it again.
x = Math.floor(boundingRect.width - ((anchor ? 0 : x) + menuNodeRect.width));
overflowX = true;
}
// Check the vertical overflow.
if (y + menuNodeRect.height > boundingRect.height) {
// Anchor positioning is already relative, so don't subtract it again.
y = Math.floor(boundingRect.height - ((anchor ? 0 : y) + menuNodeRect.height));
overflowY = true;
}
if (anchor || overflowX) {
// Set the maximum dimensions that the menu DOMNode may grow to. The
// content overflow style should be defined in CSS.
// Since we don't care much about horizontal overflow currently, this
// doesn't really do much for now.
if (menuNodeRect.width > boundingRect.width) {
menu.classList.add("overflow");
menu.style.maxWidth = boundingRect.width + "px";
}
menu.style.marginLeft = x + "px";
} else if (!menu.style.marginLeft) {
menu.style.marginLeft = "auto";
}
if (anchor || overflowY) {
if (menuNodeRect.height > (boundingRect.height + y)) {
menu.classList.add("overflow");
// Set the maximum dimensions that the menu DOMNode may grow to. The
// content overflow style should be defined in CSS.
menu.style.maxHeight = (boundingRect.height + y) + "px";
// Since we just adjusted the max-height of the menu - thus its actual
// height as well - we need to adjust its vertical offset with the same
// amount.
y += menuNodeRect.height - (boundingRect.height + y);
}
menu.style.marginTop = y + "px";
} else if (!menu.style.marginLeft) {
menu.style.marginTop = "auto";
}
menu.style.visibility = "visible";
},
componentDidMount: function() {
this.documentBody.addEventListener("click", this._onBodyClick);
rootObject.addEventListener("blur", this.hideDropdownMenu);
},
componentWillUnmount: function() {
this.documentBody.removeEventListener("click", this._onBodyClick);
rootObject.removeEventListener("blur", this.hideDropdownMenu);
},
showDropdownMenu: function() {
this.setState({showMenu: true}, this._correctMenuPosition);
},
hideDropdownMenu: function() {
this.setState({showMenu: false}, function() {
var menu = this.refs.menu && this.refs.menu.getDOMNode();
if (menu) {
menu.style.visibility = "hidden";
}
});
},
toggleDropdownMenu: function() {
this[this.state.showMenu ? "hideDropdownMenu" : "showDropdownMenu"]();
}
// If a menu button/ anchor is defined and clicked on, it will be in charge
// of hiding or showing the popup.
if (event.target !== menuButton) {
this.setState({ showMenu: false });
}
},
_correctMenuPosition: function() {
var menu = this.refs.menu && this.refs.menu.getDOMNode();
if (!menu) {
return;
}
// Correct the position of the menu only if necessary.
var x, y;
var menuNodeRect = menu.getBoundingClientRect();
var x = menuNodeRect.left;
var y = menuNodeRect.top;
// Amount of pixels that the dropdown needs to stay away from the edges of
// the page body.
var bodyMargin = 10;
var bodyRect = {
height: this.documentBody.offsetHeight - bodyMargin,
width: this.documentBody.offsetWidth - bodyMargin
};
// If there's an anchor present, position it relative to it first.
var anchor = this.refs.anchor && this.refs.anchor.getDOMNode();
if (anchor) {
// XXXmikedeboer: at the moment we only support positioning centered above
// anchor node. Please add more modes as necessary.
var anchorNodeRect = anchor.getBoundingClientRect();
// Because we're _correcting_ the position of the dropdown, we assume that
// the node is positioned absolute at 0,0 coordinates (top left).
x = anchorNodeRect.left - (menuNodeRect.width / 2) + (anchorNodeRect.width / 2);
y = anchorNodeRect.top - menuNodeRect.height - anchorNodeRect.height;
}
var overflowX = false;
var overflowY = false;
// Check the horizontal overflow.
if (x + menuNodeRect.width > bodyRect.width) {
// Anchor positioning is already relative, so don't subtract it again.
x = bodyRect.width - ((anchor ? 0 : x) + menuNodeRect.width);
overflowX = true;
}
// Check the vertical overflow.
if (y + menuNodeRect.height > bodyRect.height) {
// Anchor positioning is already relative, so don't subtract it again.
y = bodyRect.height - ((anchor ? 0 : y) + menuNodeRect.height);
overflowY = true;
}
if (anchor || overflowX) {
menu.style.marginLeft = x + "px";
} else if (!menu.style.marginLeft) {
menu.style.marginLeft = "auto";
}
if (anchor || overflowY) {
menu.style.marginTop = y + "px";
} else if (!menu.style.marginLeft) {
menu.style.marginTop = "auto";
}
},
componentDidMount: function() {
this.documentBody.addEventListener("click", this._onBodyClick);
rootObject.addEventListener("blur", this.hideDropdownMenu);
},
componentWillUnmount: function() {
this.documentBody.removeEventListener("click", this._onBodyClick);
rootObject.removeEventListener("blur", this.hideDropdownMenu);
},
showDropdownMenu: function() {
this.setState({showMenu: true});
rootObject.setTimeout(this._correctMenuPosition, 0);
},
hideDropdownMenu: function() {
this.setState({showMenu: false});
},
toggleDropdownMenu: function() {
this[this.state.showMenu ? "hideDropdownMenu" : "showDropdownMenu"]();
}
};
};
/**

View File

@ -260,6 +260,25 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
return { major: Infinity, minor: 0 };
};
/**
* Helper to get the current short platform string, based on the return value
* of `getOS`.
* Possible return values are 'mac', 'win' or 'other'.
*
* @param {String} [os] Optional string for the OS, used in tests only.
* @return {String} 'mac', 'win' or 'other'.
*/
var getPlatform = function(os) {
os = getOS(os);
var platform = "other";
if (os.indexOf("mac") > -1) {
platform = "mac";
} else if (os.indexOf("win") > -1) {
platform = "win";
}
return platform;
};
/**
* Helper to allow getting some of the location data in a way that's compatible
* with stubbing for unit tests.
@ -632,6 +651,7 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
getBoolPreference: getBoolPreference,
getOS: getOS,
getOSVersion: getOSVersion,
getPlatform: getPlatform,
isChrome: isChrome,
isFirefox: isFirefox,
isFirefoxOS: isFirefoxOS,

View File

@ -83,7 +83,7 @@ loop.shared.views = (function(_, l10n) {
* loop.shared.utils.SCREEN_SHARE_STATES
*/
var ScreenShareControlButton = React.createClass({displayName: "ScreenShareControlButton",
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -151,7 +151,8 @@ loop.shared.views = (function(_, l10n) {
var dropdownMenuClasses = cx({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
"hide": !this.state.showMenu,
"visually-hidden": true
});
var windowSharingClasses = cx({
"disabled": this.state.windowSharingDisabled

View File

@ -83,7 +83,7 @@ loop.shared.views = (function(_, l10n) {
* loop.shared.utils.SCREEN_SHARE_STATES
*/
var ScreenShareControlButton = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -151,7 +151,8 @@ loop.shared.views = (function(_, l10n) {
var dropdownMenuClasses = cx({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
"hide": !this.state.showMenu,
"visually-hidden": true
});
var windowSharingClasses = cx({
"disabled": this.state.windowSharingDisabled

View File

@ -386,7 +386,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var InitiateCallButton = React.createClass({displayName: "InitiateCallButton",
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
caption: React.PropTypes.string.isRequired,

View File

@ -386,7 +386,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var InitiateCallButton = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
caption: React.PropTypes.string.isRequired,

View File

@ -319,6 +319,43 @@ describe("loop.shared.utils", function() {
});
});
describe("#getPlatform", function() {
it("should recognize the OSX userAgent string", function() {
var UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:37.0) Gecko/20100101 Firefox/37.0";
var result = sharedUtils.getPlatform(UA);
expect(result).eql("mac");
});
it("should recognize the Windows userAgent string", function() {
var UA = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:10.0) Gecko/20100101 Firefox/10.0";
var result = sharedUtils.getPlatform(UA);
expect(result).eql("win");
});
it("should recognize the Linux userAgent string", function() {
var UA = "Mozilla/5.0 (X11; Linux i686 on x86_64; rv:10.0) Gecko/20100101 Firefox/10.0";
var result = sharedUtils.getPlatform(UA);
expect(result).eql("other");
});
it("should recognize the OSX oscpu string", function() {
var oscpu = "Intel Mac OS X 10.10";
var result = sharedUtils.getPlatform(oscpu);
expect(result).eql("mac");
});
it("should recognize the Windows oscpu string", function() {
var oscpu = "Windows NT 5.3; Win64; x64";
var result = sharedUtils.getPlatform(oscpu);
expect(result).eql("win");
});
});
describe("#objectDiff", function() {
var a, b, diff;