"use strict";
/**
* GitGraph
*
* @constructor
*
* @param {Object} options - GitGraph options
* @param {String} [options.elementId = "gitGraph"] - Id of the canvas container
* @param {Template|String|Object} [options.template] - Template of the graph
* @param {String} [options.author = "Sergio Flores <saxo-guy@epic.com>"] - Default author for commits
* @param {String} [options.mode = (null|"compact")] - Display mode
* @param {HTMLElement} [options.canvas] - DOM canvas (ex: document.getElementById("id"))
* @param {String} [options.orientation = ("vertical-reverse"|"horizontal"|"horizontal-reverse")] - Graph orientation
* @param {Boolean} [options.reverseArrow = false] - Make arrows point to ancestors if true
* @param {Number} [options.initCommitOffsetX = 0] - Add custom offsetX to initial commit.
* @param {Number} [options.initCommitOffsetY = 0] - Add custom offsetY to initial commit.
*
* @this GitGraph
**/
function GitGraph(options) {
// Options
options = _isObject(options) ? options : {};
this.elementId = (typeof options.elementId === "string") ? options.elementId : "gitGraph";
this.author = (typeof options.author === "string") ? options.author : "Sergio Flores <saxo-guy@epic.com>";
this.reverseArrow = _booleanOptionOr(options.reverseArrow, false);
// Template management
if ((typeof options.template === "string") || _isObject(options.template)) {
this.template = this.newTemplate(options.template);
} else if (options.template instanceof Template) {
this.template = options.template;
} else {
this.template = this.newTemplate("metro");
}
this.mode = options.mode || null;
if (this.mode === "compact") {
this.template.commit.message.display = false;
}
// Orientation
switch (options.orientation) {
case "vertical-reverse":
this.template.commit.spacingY *= -1;
this.orientation = "vertical-reverse";
this.template.branch.labelRotation = _isNullOrUndefined(options, "template.branch.labelRotation") ?
0 : options.template.branch.labelRotation;
this.template.commit.tag.spacingY *= -1;
break;
case "horizontal":
this.template.commit.message.display = false;
this.template.commit.spacingX = this.template.commit.spacingY;
this.template.branch.spacingY = this.template.branch.spacingX;
this.template.commit.spacingY = 0;
this.template.branch.spacingX = 0;
this.orientation = "horizontal";
this.template.branch.labelRotation = _isNullOrUndefined(options, "template.branch.labelRotation") ?
-90 : options.template.branch.labelRotation;
this.template.commit.tag.spacingX = -this.template.commit.spacingX;
this.template.commit.tag.spacingY = this.template.branch.spacingY;
break;
case "horizontal-reverse":
this.template.commit.message.display = false;
this.template.commit.spacingX = -this.template.commit.spacingY;
this.template.branch.spacingY = this.template.branch.spacingX;
this.template.commit.spacingY = 0;
this.template.branch.spacingX = 0;
this.orientation = "horizontal-reverse";
this.template.branch.labelRotation = _isNullOrUndefined(options, "template.branch.labelRotation") ?
90 : options.template.branch.labelRotation;
this.template.commit.tag.spacingX = -this.template.commit.spacingY;
this.template.commit.tag.spacingY = this.template.branch.spacingY;
break;
default:
this.orientation = "vertical";
this.template.branch.labelRotation = _isNullOrUndefined(options, "template.branch.labelRotation") ?
0 : options.template.branch.labelRotation;
break;
}
this.marginX = this.template.branch.spacingX + this.template.commit.dot.size * 2;
this.marginY = this.template.branch.spacingY + this.template.commit.dot.size * 2;
this.offsetX = 0;
this.offsetY = 0;
// Canvas init
this.canvas = document.getElementById(this.elementId) || options.canvas;
this.context = this.canvas.getContext("2d");
this.context.textBaseline = "center";
// Tooltip layer
this.tooltip = document.createElement("div");
this.tooltip.className = "gitgraph-tooltip";
this.tooltip.style.position = "fixed";
this.tooltip.style.display = "none";
// Add tooltip div into body
document.body.appendChild(this.tooltip);
// Navigation vars
this.HEAD = null;
this.branches = [];
this.commits = [];
// Utilities
this.columnMax = 0; // nb of column for message position
this.commitOffsetX = options.initCommitOffsetX || 0;
this.commitOffsetY = options.initCommitOffsetY || 0;
// Bindings
this.mouseMoveOptions = {
handleEvent: this.hover,
gitgraph: this
};
this.canvas.addEventListener("mousemove", this.mouseMoveOptions, false);
this.mouseDownOptions = {
handleEvent: this.click,
gitgraph: this
};
this.canvas.addEventListener("mousedown", this.mouseDownOptions, false);
// Render on window resize
window.onresize = this.render.bind(this);
}
/**
* Disposing canvas event handlers
*
* @this GitGraph
**/
GitGraph.prototype.dispose = function () {
this.canvas.removeEventListener("mousemove", this.mouseMoveOptions, false);
this.canvas.removeEventListener("mousedown", this.mouseDownOptions, false);
};
/**
* Create new branch
*
* @param {(String | Object)} options - Branch name | Options of Branch
*
* @see Branch
* @this GitGraph
*
* @return {Branch} New branch
**/
GitGraph.prototype.branch = function (options) {
// Options
if (typeof options === "string") {
var name = options;
options = {};
options.name = name;
}
options = _isObject(options) ? options : {};
options.parent = this;
options.parentBranch = options.parentBranch || this.HEAD;
// Add branch
var branch = new Branch(options);
this.branches.push(branch);
// Return
return branch;
};
/**
* Create new orphan branch
*
* @param {(String | Object)} options - Branch name | Options of Branch
*
* @see Branch
* @this GitGraph
*
* @return {Branch} New branch
**/
GitGraph.prototype.orphanBranch = function (options) {
// Options
if (typeof options === "string") {
var name = options;
options = {};
options.name = name;
}
options = _isObject(options) ? options : {};
options.parent = this;
// Add branch
var branch = new Branch(options);
this.branches.push(branch);
// Return
return branch;
};
/**
* Commit on HEAD
*
* @param {Object} options - Options of commit
*
* @see Commit
* @this GitGraph
*
* @return {GitGraph} this - Return the main object so we can chain
**/
GitGraph.prototype.commit = function (options) {
this.HEAD.commit(options);
// Return the main object so we can chain
return this;
};
/**
* Tag the HEAD
*
* @param {Object} options - Options of tag
*
* @see Tag
* @this GitGraph
*
* @return {GitGraph} this - Return the main object so we can chain
**/
GitGraph.prototype.tag = function (options) {
this.HEAD.tag(options);
// Return the main object so we can chain
return this;
};
/**
* Create a new template
*
* @param {(String|Object)} options - The template name, or the template options
*
* @return {Template}
**/
GitGraph.prototype.newTemplate = function (options) {
if (typeof options === "string") {
return new Template().get(options);
}
return new Template(options);
};
/**
* Render the canvas
*
* @this GitGraph
**/
GitGraph.prototype.render = function () {
this.scalingFactor = _getScale(this.context);
// Resize canvas
var unscaledResolution = {
x: Math.abs((this.columnMax + 1) * this.template.branch.spacingX) +
Math.abs(this.commitOffsetX) +
this.marginX * 2,
y: Math.abs((this.columnMax + 1) * this.template.branch.spacingY) +
Math.abs(this.commitOffsetY) +
this.marginY * 2
};
if (this.template.commit.message.display) {
unscaledResolution.x += 800;
}
unscaledResolution.x += this.template.commit.widthExtension;
this.canvas.style.width = unscaledResolution.x + "px";
this.canvas.style.height = unscaledResolution.y + "px";
this.canvas.width = unscaledResolution.x * this.scalingFactor;
this.canvas.height = unscaledResolution.y * this.scalingFactor;
// Clear All
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Add some margin
this.context.translate(this.marginX, this.marginY);
// Translate for inverse orientation
if (this.template.commit.spacingY > 0) {
this.context.translate(0, this.canvas.height - this.marginY * 2);
this.offsetY = this.canvas.height - this.marginY * 2;
}
if (this.template.commit.spacingX > 0) {
this.context.translate(this.canvas.width - this.marginX * 2, 0);
this.offsetX = this.canvas.width - this.marginX * 2;
}
// Scale the context when every transformations have been made.
this.context.scale(this.scalingFactor, this.scalingFactor);
// Render branches
for (var i = this.branches.length - 1, branch; !!(branch = this.branches[i]); i--) {
branch.render();
}
this.tagNum = 0;
// Render commits after to put them on the foreground
for (var j = 0, commit; !!(commit = this.commits[j]); j++) {
commit.render();
}
_emitEvent(this.canvas, "graph:render", {
id: this.elementId
});
};
/**
* A callback for each commit
*
* @callback commitCallback
* @param {Commit} commit - A commit
* @param {Boolean} mouseOver - True, if the mouse is currently hovering over the commit
* @param {Event} event - The DOM event (e.g. a click event)
*/
/**
* A formatter for commit
*
* @callback commitFormatter
* @param {Commit} commit - The commit to format
*/
/**
* Hover event on commit dot
*
* @param {MouseEvent} event - Mouse event
* @param {commitCallback} callbackFn - A callback function that will be called for each commit
*
* @this GitGraph
**/
GitGraph.prototype.applyCommits = function (event, callbackFn) {
// Fallback onto layerX/layerY for older versions of Firefox.
function getOffsetById(id) {
var el = document.getElementById(id);
var rect = el.getBoundingClientRect();
return {
top: rect.top + document.body.scrollTop,
left: rect.left + document.body.scrollLeft
};
}
var offsetX = event.offsetX || (event.pageX - getOffsetById(this.elementId).left);
var offsetY = event.offsetY || (event.pageY - getOffsetById(this.elementId).top);
for (var i = 0, commit; !!(commit = this.commits[i]); i++) {
var distanceX = (commit.x + (this.offsetX + this.marginX) / this.scalingFactor - offsetX);
var distanceY = (commit.y + (this.offsetY + this.marginY) / this.scalingFactor - offsetY);
var distanceBetweenCommitCenterAndMouse = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
var isOverCommit = distanceBetweenCommitCenterAndMouse < this.template.commit.dot.size;
callbackFn(commit, isOverCommit, event);
}
};
/**
* Hover event on commit dot
*
* @param {MouseEvent} event - Mouse event
*
* @this GitGraph
**/
GitGraph.prototype.hover = function (event) {
var self = this.gitgraph;
var isOut = true;
function showCommitTooltip(commit) {
if (!commit.tooltipDisplay) {
return;
}
// Fix firefox MouseEvent
if (typeof InstallTrigger !== "undefined") /* == (is Firefox) */ {
event.x = event.x ? event.x : event.clientX;
event.y = event.y ? event.y : event.clientY;
}
self.tooltip.style.left = event.x + "px"; // TODO Scroll bug
self.tooltip.style.top = event.y + "px"; // TODO Scroll bug
if (self.template.commit.tooltipHTMLFormatter !== null) {
self.tooltip.innerHTML = self.template.commit.tooltipHTMLFormatter(commit);
} else {
self.tooltip.textContent = commit.sha1 + " - " + commit.message;
}
self.tooltip.style.display = "block";
}
function emitCommitEvent(commit, event) {
var mouseEventOptions = {
author: commit.author,
message: commit.message,
date: commit.date,
sha1: commit.sha1
};
_emitEvent(self.canvas, "commit:" + event, mouseEventOptions);
}
self.applyCommits(event, function (commit, isOverCommit, event) {
if (isOverCommit) {
if (!self.template.commit.message.display && self.template.commit.shouldDisplayTooltipsInCompactMode) {
showCommitTooltip(commit);
}
// Don't emit event if we already were over a commit.
if (!commit.isMouseOver) {
emitCommitEvent(commit, "mouseover");
}
isOut = false;
commit.isMouseOver = true;
} else {
// Don't emit event if we already were out of a commit.
if (commit.isMouseOver) {
emitCommitEvent(commit, "mouseout");
}
commit.isMouseOver = false;
}
});
if (isOut) {
self.tooltip.style.display = "none";
}
};
/**
* Click event on commit dot
*
* @param {MouseEvent} event - Mouse event
*
* @this GitGraph
**/
GitGraph.prototype.click = function (event) {
this.gitgraph.applyCommits(event, function (commit, isOverCommit, event) {
if (!isOverCommit) {
return;
}
if (commit.onClick !== null) {
commit.onClick(commit, true, event);
}
});
};
// --------------------------------------------------------------------
// ----------------------- Branch ------------------------
// --------------------------------------------------------------------
/**
* Branch
*
* @constructor
*
* @param {Object} options - Options of branch
* @param {GitGraph} options.parent - GitGraph constructor
* @param {Branch} [options.parentBranch = options.parentCommit.branch] - Parent branch
* @param {Commit} [options.parentCommit = _getLast(options.parentBranch.commits)] - Parent commit
* @param {String} [options.name = "no-name"] - Branch name
* @param {Object} [options.commitDefaultOptions = {}] - Default options for commits
*
* @this Branch
**/
function Branch(options) {
// Check integrity
if (options.parent instanceof GitGraph === false) {
return;
}
// Options
options = _isObject(options) ? options : {};
this.parent = options.parent;
if (options.parentCommit && options.parentBranch) {
if (options.parentCommit.branch !== options.parentBranch) {
return;
}
this.parentCommit = options.parentCommit;
this.parentBranch = options.parentBranch;
} else if (options.parentCommit) {
this.parentCommit = options.parentCommit;
this.parentBranch = options.parentCommit.branch;
} else if (options.parentBranch) {
this.parentCommit = _getParentCommitFromBranch(options.parentBranch);
this.parentBranch = options.parentBranch;
} else {
this.parentCommit = null;
this.parentBranch = null;
}
this.name = (typeof options.name === "string") ? options.name : "no-name";
this.commitDefaultOptions = _isObject(options.commitDefaultOptions) ? options.commitDefaultOptions : {};
this.context = this.parent.context;
this.template = this.parent.template;
this.lineWidth = options.lineWidth || this.template.branch.lineWidth;
this.lineDash = options.lineDash || this.template.branch.lineDash;
this.showLabel = _booleanOptionOr(options.showLabel, this.template.branch.showLabel);
this.spacingX = this.template.branch.spacingX;
this.spacingY = this.template.branch.spacingY;
this.size = 0;
this.height = 0;
this.width = 0;
this.commits = [];
this.path = []; // Path to draw, this is an array of points {x, y, type("start"|"join"|"end")}
// Column number calculation for auto-color & auto-offset
if (typeof options.column === "number") {
this.column = options.column;
} else {
this.column = 0;
this.calculColumn();
}
this.parent.columnMax = (this.column > this.parent.columnMax) ? this.column : this.parent.columnMax;
// Options with auto value
this.offsetX = this.column * this.spacingX;
this.offsetY = this.column * this.spacingY;
// Add start point
if (this.parentBranch) {
if (this.parentCommit === _getParentCommitFromBranch(this.parentBranch)) {
this.startPoint = {
x: this.parentBranch.offsetX - this.parent.commitOffsetX + this.template.commit.spacingX,
y: this.parentBranch.offsetY - this.parent.commitOffsetY + this.template.commit.spacingY,
type: "start"
};
} else {
this.startPoint = {
x: this.parentCommit.x,
y: this.parentCommit.y,
type: "start"
};
}
} else {
this.startPoint = null;
}
var columnIndex = (this.column % this.template.colors.length);
this.color = options.color || this.template.branch.color || this.template.colors[columnIndex];
// Checkout on this new branch
this.checkout();
}
/**
* Create new branch
*
* @param {(String | Object)} options - Branch name | Options of Branch
*
* @see Branch
* @this Branch
*
* @return {Branch} New Branch
**/
Branch.prototype.branch = function (options) {
// Options
if (typeof options === "string") {
var name = options;
options = {};
options.name = name;
}
options = _isObject(options) ? options : {};
options.parent = this.parent;
options.parentBranch = options.parentBranch || this;
// Add branch
var branch = new Branch(options);
this.parent.branches.push(branch);
// Return
return branch;
};
/**
* Render the branch
*
* @this Branch
**/
Branch.prototype.render = function () {
this.context.beginPath();
for (var i = 0, point; !!(point = this.path[i]); i++) {
if (point.type === "start") {
this.context.moveTo(point.x, point.y);
} else {
if (this.template.branch.mergeStyle === "bezier") {
var path = this.path[i - 1];
this.context.bezierCurveTo(
path.x - this.template.commit.spacingX / 2, path.y - this.template.commit.spacingY / 2,
point.x + this.template.commit.spacingX / 2, point.y + this.template.commit.spacingY / 2,
point.x, point.y
);
} else {
this.context.lineTo(point.x, point.y);
}
}
}
this.context.lineWidth = this.lineWidth;
this.context.strokeStyle = this.color;
var prevLineDash;
if (this.context.setLineDash !== undefined) {
prevLineDash = this.context.getLineDash();
this.context.setLineDash(this.lineDash);
}
this.context.stroke();
this.context.closePath();
//Restore previous line dash setting, if any
if (prevLineDash !== undefined) {
this.context.setLineDash(prevLineDash);
}
};
/**
* Add a commit
*
* @param {(String | Object)} [options] - Message | Options of commit
* @param {String} [options.detailId] - Id of detail DOM Element
*
* @see Commit
*
* @this Branch
**/
Branch.prototype.commit = function (options) {
if (typeof (options) === "string") {
options = {
message: options
};
} else if (typeof (options) !== "object") {
options = {};
}
options.arrowDisplay = this.template.arrow.active;
options.branch = this;
var columnIndex = (this.column % this.template.colors.length);
options.color = options.color ||
this.commitDefaultOptions.color ||
this.template.commit.color ||
this.template.colors[columnIndex];
options.parent = this.parent;
options.parentCommit = options.parentCommit || _getParentCommitFromBranch(this);
// Special compact mode
if (this.parent.mode === "compact" &&
_getLast(this.parent.commits) &&
_getLast(this.parent.commits).branch !== options.branch &&
options.branch.commits.length &&
options.type !== "mergeCommit") {
this.parent.commitOffsetX -= this.template.commit.spacingX;
this.parent.commitOffsetY -= this.template.commit.spacingY;
}
options.messageColor = options.messageColor ||
this.commitDefaultOptions.messageColor ||
this.template.commit.message.color ||
options.color ||
null;
options.labelColor = options.labelColor ||
this.commitDefaultOptions.labelColor ||
this.template.branch.labelColor ||
options.color ||
null;
options.tagColor = options.tagColor ||
this.commitDefaultOptions.tagColor ||
this.template.commit.tag.color ||
options.color ||
null;
options.dotColor = options.dotColor ||
this.commitDefaultOptions.dotColor ||
this.template.commit.dot.color ||
options.color ||
null;
options.x = this.offsetX - this.parent.commitOffsetX;
options.y = this.offsetY - this.parent.commitOffsetY;
// Detail
var isVertical = this.parent.orientation === "vertical";
var isNotCompact = this.parent.mode !== "compact";
if (typeof options.detailId === "string" && isVertical && isNotCompact) {
options.detail = document.getElementById(options.detailId);
} else {
options.detail = null;
}
// Check collision (Cause of special compact mode)
var previousCommit = _getLast(options.branch.commits) || {};
var commitPosition = options.x + options.y;
var previousCommitPosition = previousCommit.x + previousCommit.y;
var isCommitAtSamePlaceThanPreviousOne = (commitPosition === previousCommitPosition);
if (isCommitAtSamePlaceThanPreviousOne) {
this.parent.commitOffsetX += this.template.commit.spacingX;
this.parent.commitOffsetY += this.template.commit.spacingY;
options.x = this.offsetX - this.parent.commitOffsetX;
options.y = this.offsetY - this.parent.commitOffsetY;
}
// Fork case: Parent commit from parent branch
if (options.parentCommit instanceof Commit === false && this.parentBranch instanceof Branch) {
options.parentCommit = this.parentCommit;
}
// First commit
var isFirstBranch = !(options.parentCommit instanceof Commit);
var isPathBeginning = this.path.length === 0;
options.showLabel = (isPathBeginning && this.showLabel) ? true : false;
if (options.showLabel) {
options.x -= this.template.commit.spacingX;
options.y -= this.template.commit.spacingY;
}
var commit = new Commit(options);
this.commits.push(commit);
// Add point(s) to path
var point = {
x: commit.x,
y: commit.y,
type: "join"
};
if (!isFirstBranch && isPathBeginning) {
this.pushPath(this.startPoint);
// Trace path from parent branch if it has commits already
if (this.parentBranch.commits.length > 0) {
this.pushPath({
x: this.startPoint.x - this.parentBranch.offsetX + this.offsetX - this.template.commit.spacingX,
y: this.startPoint.y - this.parentBranch.offsetY + this.offsetY - this.template.commit.spacingY,
type: "join"
});
var parent = _clone(this.startPoint);
parent.type = "join";
this.parentBranch.pushPath(parent);
}
} else if (isPathBeginning) {
point.type = "start";
}
this.pushPath(point);
// Increment commitOffset for next commit position
this.parent.commitOffsetX += this.template.commit.spacingX * (options.showLabel ? 2 : 1);
this.parent.commitOffsetY += this.template.commit.spacingY * (options.showLabel ? 2 : 1);
// Add height of detail div (normal vertical mode only)
if (commit.detail !== null) {
commit.detail.style.display = "block";
this.parent.commitOffsetY -= commit.detail.clientHeight - 40;
}
// Auto-render
this.parent.render();
// Return the main object so we can chain
return this;
};
/**
* Tag the last commit of the branch.
*
* @param {(String | Object)} [options] - Message | Options of the tag
* @param {String} [options.tag] - Message of the tag
* @param {String} [options.tagColor] - Color of the tag
* @param {String} [options.tagFont] - Font of the tag
* @param {Boolean} [options.displayTagBox] - If true, display a box around the tag
*
* @see Tag
*
* @this Branch
* */
Branch.prototype.tag = function (options) {
if (typeof options === "string") {
options = {
tag: options
};
}
options = _isObject(options) ? options : {};
var lastCommit = _getLast(this.commits);
if (_isObject(lastCommit)) {
_assignTagOptionsToCommit(lastCommit, options);
this.parent.render();
}
// Return the main object so we can chain
return this;
};
/**
* Checkout onto this branch
*
* @this Branch
**/
Branch.prototype.checkout = function () {
this.parent.HEAD = this;
};
/**
* Delete this branch
*
* @this Branch
**/
Branch.prototype.delete = function () {
this.isDeleted = true;
};
/**
* Merge branch
*
* @param {Branch} [target = this.parent.HEAD]
* @param {(String | Object)} [commitOptions] - Message | Options of commit
* @param {Boolean} [commitOptions.fastForward = false] - If true, merge should use fast-forward if possible
*
* @this Branch
*
* @return {Branch} this
**/
Branch.prototype.merge = function (target, commitOptions) {
// Merge target
var targetBranch = target || this.parent.HEAD;
// Check integrity of target
if (targetBranch instanceof Branch === false || targetBranch === this) {
return this;
}
// Merge commit
var defaultMessage = "Merge branch `" + this.name + "` into `" + targetBranch.name + "`";
if (typeof commitOptions !== "object") {
var message = commitOptions;
commitOptions = {};
commitOptions.message = (typeof message === "string") ? message : defaultMessage;
} else {
commitOptions.message = commitOptions.message || defaultMessage;
}
commitOptions.type = "mergeCommit";
commitOptions.parentCommit = _getParentCommitFromBranch(this);
var branchParentCommit = this.commits[0].parentCommit;
var parentBranchLastCommit = _getLast(targetBranch.commits);
var isFastForwardPossible = (branchParentCommit.sha1 === parentBranchLastCommit.sha1);
if (commitOptions.fastForward && isFastForwardPossible) {
var isGraphHorizontal = _isHorizontal(this.parent);
this.color = targetBranch.color;
// Make branch path follow target branch ones
if (isGraphHorizontal) {
var targetBranchY = targetBranch.path[1].y;
this.path.forEach(function (point) {
point.y = targetBranchY;
});
} else {
var targetBranchX = targetBranch.path[1].x;
this.path.forEach(function (point) {
point.x = targetBranchX;
});
}
this.commits.forEach(function (commit) {
if (isGraphHorizontal) {
commit.y = branchParentCommit.y;
} else {
commit.x = branchParentCommit.x;
}
commit.labelColor = branchParentCommit.labelColor;
commit.messageColor = branchParentCommit.messageColor;
commit.dotColor = branchParentCommit.dotColor;
commit.dotStrokeColor = branchParentCommit.dotStrokeColor;
});
} else {
targetBranch.commit(commitOptions);
// Add points to path
var targetCommit = _getLast(targetBranch.commits);
var endOfBranch = {
x: this.offsetX + this.template.commit.spacingX * (targetCommit.showLabel ? 3 : 2) - this.parent.commitOffsetX,
y: this.offsetY + this.template.commit.spacingY * (targetCommit.showLabel ? 3 : 2) - this.parent.commitOffsetY,
type: "join"
};
this.pushPath(_clone(endOfBranch));
var mergeCommit = {
x: targetCommit.x,
y: targetCommit.y,
type: "end"
};
this.pushPath(mergeCommit);
endOfBranch.type = "start";
this.pushPath(endOfBranch); // End of branch for future commits
}
// Auto-render
this.parent.render();
// Checkout on target
this.parent.HEAD = targetBranch;
// Return the main object so we can chain
return this;
};
/**
* Calcul column
*
* @this Branch
**/
Branch.prototype.calculColumn = function () {
var candidates = [];
for (var i = 0, branch; !!(branch = this.parent.branches[i]); i++) {
if (!branch.isDeleted) {
if (!(branch.column in candidates)) {
candidates[branch.column] = 0;
}
candidates[branch.column]++;
}
}
this.column = 0;
for (;; this.column++) {
if (!(this.column in candidates) || candidates[this.column] === 0) {
break;
}
}
};
/**
* Push a new point to path.
* This method will combine duplicate points and reject reversed points.
*
* @this Branch
*/
Branch.prototype.pushPath = function (point) {
var lastPoint = _getLast(this.path);
if (!lastPoint) {
this.path.push(point);
} else if (lastPoint.x === point.x && lastPoint.y === point.y) {
if (lastPoint.type !== "start" && point.type === "end") {
lastPoint.type = "end";
} else if (point.type === "join") {
} else {
this.path.push(point);
}
} else {
if (point.type === "join") {
if ((point.x - lastPoint.x) * this.template.commit.spacingX < 0) {
this.path.push(point);
} else if ((point.y - lastPoint.y) * this.template.commit.spacingY < 0) {
this.path.push(point);
}
} else {
this.path.push(point);
}
}
};
// --------------------------------------------------------------------
// ----------------------- Commit ------------------------
// --------------------------------------------------------------------
/**
* Commit
*
* @constructor
*
* @param {Object} options - Commit options
* @param {GitGraph} options.parent - GitGraph constructor
* @param {Number} options.x - Position X (dot)
* @param {Number} options.y - Position Y (dot)
* @param {String} options.color - Master color (dot & message)
* @param {Boolean} options.arrowDisplay - Add a arrow under commit dot
* @param {String} [options.author = this.parent.author] - Author name & email
* @param {String} [options.date] - Date of commit, default is now
* @param {String} [options.detail] - DOM Element of detail part
* @param {String} [options.sha1] - Sha1, default is a random short sha1
* @param {Commit} [options.parentCommit] - Parent commit
* @param {String} [options.type = ("mergeCommit"|null)] - Type of commit
*
* @param {String} [options.tag] - Tag of the commit
* @param {String} [options.tagColor = options.color] - Color of the tag
* @param {String} [options.tagFont = this.template.commit.tag.font] - Font of the tag
* @param {String} [options.displayTagBox = true] - If true, display a box around the tag
*
* @param {String} [options.dotColor = options.color] - Specific dot color
* @param {Number} [options.dotSize = this.template.commit.dot.size] - Dot size
* @param {Number} [options.dotStrokeWidth = this.template.commit.dot.strokeWidth] - Dot stroke width
* @param {Number} [options.dotStrokeColor = this.template.commit.dot.strokeColor]
*
* @param {String} [options.message = "He doesn't like George Michael! Boooo!"] - Commit message
* @param {String} [options.messageColor = options.color] - Specific message color
* @param {String} [options.messageFont = this.template.commit.message.font] - Font of the message
* @param {Boolean} [options.messageDisplay = this.template.commit.message.display] - Commit message display policy
* @param {Boolean} [options.messageAuthorDisplay = this.template.commit.message.displayAuthor] - Commit message author policy
* @param {Boolean} [options.messageBranchDisplay = this.template.commit.message.displayBranch] - Commit message author policy
* @param {Boolean} [options.messageHashDisplay = this.template.commit.message.displayHash] - Commit message hash policy
*
* @param {String} [options.labelColor = options.color] - Specific label color
* @param {String} [options.labelFont = this.template.branch.labelFont] - Font used for labels
*
* @param {Boolean} [options.tooltipDisplay = true] - Tooltip message display policy
* @param {commitCallback} [options.onClick] - OnClick event for the commit dot
* @param {Object} [options.representedObject] - Any object which is related to this commit. Can be used in onClick or the formatter. Useful to bind the commit to external objects such as database id etc.
*
* @this Commit
**/
function Commit(options) {
// Check integrity
if (options.parent instanceof GitGraph === false) {
return;
}
// Options
options = _isObject(options) ? options : {};
this.parent = options.parent;
this.template = this.parent.template;
this.context = this.parent.context;
this.branch = options.branch;
this.author = options.author || this.parent.author;
this.date = options.date || new Date().toUTCString();
this.detail = options.detail || null;
this.sha1 = options.sha1 || (Math.random(100)).toString(16).substring(3, 10);
this.message = options.message || "He doesn't like George Michael! Boooo!";
this.arrowDisplay = options.arrowDisplay;
this.messageDisplay = _booleanOptionOr(options.messageDisplay, this.template.commit.message.display);
this.messageAuthorDisplay = _booleanOptionOr(options.messageAuthorDisplay, this.template.commit.message.displayAuthor);
this.messageBranchDisplay = _booleanOptionOr(options.messageBranchDisplay, this.template.commit.message.displayBranch);
this.messageHashDisplay = _booleanOptionOr(options.messageHashDisplay, this.template.commit.message.displayHash);
this.messageColor = options.messageColor || options.color;
this.messageFont = options.messageFont || this.template.commit.message.font;
this.dotColor = options.dotColor || options.color;
this.dotSize = options.dotSize || this.template.commit.dot.size;
this.dotStrokeWidth = options.dotStrokeWidth || this.template.commit.dot.strokeWidth;
this.dotStrokeColor = options.dotStrokeColor || this.template.commit.dot.strokeColor || options.color;
this.type = options.type || null;
this.tooltipDisplay = _booleanOptionOr(options.tooltipDisplay, true);
this.onClick = options.onClick || null;
this.representedObject = options.representedObject || null;
this.parentCommit = options.parentCommit;
this.x = options.x;
this.y = options.y;
this.showLabel = options.showLabel;
this.labelColor = options.labelColor || options.color;
this.labelFont = options.labelFont || this.template.branch.labelFont;
_assignTagOptionsToCommit(this, options);
this.parent.commits.push(this);
}
/**
* Render the commit
*
* @this Commit
**/
Commit.prototype.render = function () {
var commitOffsetForTags = this.template.commit.tag.spacingX;
var commitOffsetLeft = (this.parent.columnMax + 1) * this.template.branch.spacingX + commitOffsetForTags;
// Label
if (this.showLabel) {
/*
* For cases where we want a 0 or 180 degree label rotation in horizontal mode,
* we need to modify the position of the label to sit centrally above the commit dot.
*/
if (_isHorizontal(this.parent) &&
(this.template.branch.labelRotation % 180 === 0)) {
/*
* Take into account the dot size and the height of the label
* (calculated from the font size) to arrive at the Y position.
*/
var yNegativeMargin = this.y - this.dotSize - _getFontHeight(this.labelFont);
_drawTextBG(this.context,
this.x,
yNegativeMargin,
this.branch.name,
this.labelColor,
this.labelFont,
this.template.branch.labelRotation,
true);
} else {
_drawTextBG(this.context,
this.x + this.template.commit.spacingX,
this.y + this.template.commit.spacingY,
this.branch.name,
this.labelColor,
this.labelFont,
this.template.branch.labelRotation,
true);
}
}
// Dot
this.context.beginPath();
this.context.arc(this.x, this.y, this.dotSize, 0, 2 * Math.PI, false);
this.context.fillStyle = this.dotColor;
this.context.strokeStyle = this.dotStrokeColor;
this.context.lineWidth = this.dotStrokeWidth;
if (typeof (this.dotStrokeWidth) === "number") {
this.context.stroke();
}
this.context.fill();
this.context.closePath();
// Arrow
if (this.arrowDisplay && this.parentCommit instanceof Commit) {
this.arrow();
}
// Tag
if (this.tag !== null) {
var tag = new Tag(this, {
color: this.tagColor,
font: this.tagFont
});
commitOffsetLeft += tag.width - commitOffsetForTags;
}
// Detail
if (this.detail !== null) {
this.detail.style.left = this.parent.canvas.offsetLeft + commitOffsetLeft + this.x + 30 + "px";
this.detail.style.top = this.parent.canvas.offsetTop + this.y + 40 + "px";
this.detail.width = 30;
}
// Message
if (this.messageDisplay) {
var message = this.message;
if (this.messageHashDisplay) {
message = this.sha1 + " " + message;
}
if (this.messageAuthorDisplay) {
message = message + (this.author ? " - " + this.author : "");
}
if (this.messageBranchDisplay) {
message = (this.branch.name ? "[" + this.branch.name + "] " : "") + message;
}
this.context.font = this.messageFont;
this.context.fillStyle = this.messageColor;
this.context.fillText(message, commitOffsetLeft, this.y + this.dotSize / 2);
}
};
/**
* Render a arrow before commit
*
* @this Commit
**/
Commit.prototype.arrow = function Arrow() {
// Options
var size = this.template.arrow.size;
var color = this.template.arrow.color || this.branch.color;
var isReversed = this.parent.reverseArrow;
function rotate(y, x) {
var direction = (isReversed) ? -1 : 1;
return Math.atan2(direction * y, direction * x);
}
// Angles calculation
var alpha = rotate(this.parentCommit.y - this.y, this.parentCommit.x - this.x);
// Merge & Fork case
var isForkCommit = (this === this.branch.commits[0]);
if (this.type === "mergeCommit" || isForkCommit) {
var deltaColumn = (this.parentCommit.branch.column - this.branch.column);
var commitSpaceDelta = (this.showLabel ? 2 : 1);
var alphaX = this.template.branch.spacingX * deltaColumn + this.template.commit.spacingX * commitSpaceDelta;
var isPushedInY = (isForkCommit || isReversed) &&
Math.abs(this.y - this.parentCommit.y) > Math.abs(this.template.commit.spacingY);
var isOnSameXThanParent = (this.x === this.parentCommit.x);
if (_isVertical(this.parent) && (isPushedInY || isOnSameXThanParent)) {
alphaX = 0;
}
var alphaY = this.template.branch.spacingY * deltaColumn + this.template.commit.spacingY * commitSpaceDelta;
var isPushedInX = (isForkCommit || isReversed) &&
Math.abs(this.x - this.parentCommit.x) > Math.abs(this.template.commit.spacingX);
var isOnSameYThanParent = (this.y === this.parentCommit.y);
if (_isHorizontal(this.parent) && (isPushedInX || isOnSameYThanParent)) {
alphaY = 0;
}
alpha = rotate(alphaY, alphaX);
color = this.parentCommit.branch.color;
}
var delta = Math.PI / 7; // Delta between left & right (radian)
var arrowX = (isReversed) ? this.parentCommit.x : this.x;
var arrowY = (isReversed) ? this.parentCommit.y : this.y;
// Top
var h = this.template.commit.dot.size + this.template.arrow.offset;
var x1 = h * Math.cos(alpha) + arrowX;
var y1 = h * Math.sin(alpha) + arrowY;
// Bottom left
var x2 = (h + size) * Math.cos(alpha - delta) + arrowX;
var y2 = (h + size) * Math.sin(alpha - delta) + arrowY;
// Bottom center
var x3 = (h + size / 2) * Math.cos(alpha) + arrowX;
var y3 = (h + size / 2) * Math.sin(alpha) + arrowY;
// Bottom right
var x4 = (h + size) * Math.cos(alpha + delta) + arrowX;
var y4 = (h + size) * Math.sin(alpha + delta) + arrowY;
this.context.beginPath();
this.context.fillStyle = color;
this.context.moveTo(x1, y1); // Top
this.context.lineTo(x2, y2); // Bottom left
this.context.quadraticCurveTo(x3, y3, x4, y4); // Bottom center
this.context.lineTo(x4, y4); // Bottom right
this.context.fill();
};
// --------------------------------------------------------------------
// ----------------------- Tag ------------------------
// --------------------------------------------------------------------
/**
* Tag
*
* @constructor
*
* @param {Commit} commit - Tagged commit
* @param {Object} [options] - Tag options
* @param {String} [options.color = commit.color] - Specific tag color
* @param {String} [options.font = commit.template.commit.tag.font] - Font of the tag
* @return {Tag}
*
* @this Tag
* */
function Tag(commit, options) {
if (!_isObject(commit)) {
throw new Error("You can't tag a commit that doesn't exist");
}
options = _isObject(options) ? options : {};
this.color = options.color || commit.color;
this.font = options.font || commit.template.commit.tag.font;
// Set context font for calculations
var originalFont = commit.context.font;
commit.context.font = this.font;
var textWidth = commit.context.measureText(commit.tag).width;
this.width = Math.max(commit.template.commit.tag.spacingX, textWidth);
commit.parent.tagNum++;
var x = 0;
var y = 0;
if (_isHorizontal(commit.parent)) {
x = commit.x - commit.dotSize / 2;
y = ((commit.parent.columnMax + 1) * commit.template.commit.tag.spacingY) - commit.template.commit.tag.spacingY / 2 + (commit.parent.tagNum % 2) * _getFontHeight(this.font) * 1.5;
} else {
x = ((commit.parent.columnMax + 1) * commit.template.commit.tag.spacingX) - commit.template.commit.tag.spacingX / 2 + textWidth / 2;
y = commit.y - commit.dotSize / 2;
}
_drawTextBG(commit.context, x, y, commit.tag, this.color, this.font, 0, commit.displayTagBox);
// Reset original context font
commit.context.font = originalFont;
return this;
}
// --------------------------------------------------------------------
// ----------------------- Template ------------------------
// --------------------------------------------------------------------
/**
* Template
*
* @constructor
*
* @param {Object} options - Template options
* @param {Array} [options.colors] - Colors scheme: One color for each column
* @param {String} [options.arrow.color] - Arrow color
* @param {Number} [options.arrow.size] - Arrow size
* @param {Number} [options.arrow.offset] - Arrow offset
* @param {String} [options.branch.color] - Branch color
* @param {Number} [options.branch.lineWidth] - Branch line width
* @param {String} [options.branch.mergeStyle = ("bezier"|"straight")] - Branch merge style
* @param {Number} [options.branch.spacingX] - Space between branches
* @param {Number} [options.branch.spacingY] - Space between branches
* @param {Number} [options.commit.spacingX] - Space between commits
* @param {Number} [options.commit.spacingY] - Space between commits
* @param {Number} [options.commit.widthExtension = 0] - Additional width to be added to the calculated width
* @param {String} [options.commit.color] - Master commit color (dot & message)
* @param {String} [options.commit.dot.color] - Commit dot color
* @param {Number} [options.commit.dot.size] - Commit dot size
* @param {Number} [options.commit.dot.strokeWidth] - Commit dot stroke width
* @param {Number} [options.commit.dot.strokeColor] - Commit dot stroke color
* @param {String} [options.commit.message.color] - Commit message color
* @param {Boolean} [options.commit.message.display] - Commit display policy
* @param {Boolean} [options.commit.message.displayAuthor] - Commit message author policy
* @param {Boolean} [options.commit.message.displayBranch] - Commit message branch policy
* @param {Boolean} [options.commit.message.displayHash] - Commit message hash policy
* @param {String} [options.commit.message.font = "normal 12pt Calibri"] - Commit message font
* @param {Boolean} [options.commit.shouldDisplayTooltipsInCompactMode] - Tooltips policy
* @param {commitFormatter} [options.commit.tooltipHTMLFormatter = true] - Formatter for the tooltip contents.
*
* @this Template
**/
function Template(options) {
// Options
options = _isObject(options) ? options : {};
options.branch = options.branch || {};
options.arrow = options.arrow || {};
options.commit = options.commit || {};
options.commit.dot = options.commit.dot || {};
options.commit.tag = options.commit.tag || {};
options.commit.message = options.commit.message || {};
// One color per column
this.colors = options.colors || ["#6963FF", "#47E8D4", "#6BDB52", "#E84BA5", "#FFA657"];
// Branch style
this.branch = {};
this.branch.color = options.branch.color || null; // Only one color
this.branch.lineWidth = options.branch.lineWidth || 2;
this.branch.lineDash = options.branch.lineDash || [];
this.branch.showLabel = options.branch.showLabel || false;
this.branch.labelColor = options.branch.labelColor || null;
this.branch.labelFont = options.branch.labelFont || "normal 8pt Calibri";
/*
* Set to 'null' by default, as a value of '0' can no longer be used to test
* whether rotation angle has been defined
* ('0' is an acceptable value).
*/
this.branch.labelRotation = options.branch.labelRotation !== undefined ?
options.branch.labelRotation : null;
// Merge style = "bezier" | "straight"
this.branch.mergeStyle = options.branch.mergeStyle || "bezier";
// Space between branches
this.branch.spacingX = (typeof options.branch.spacingX === "number") ? options.branch.spacingX : 20;
this.branch.spacingY = options.branch.spacingY || 0;
// Arrow style
this.arrow = {};
this.arrow.size = options.arrow.size || null;
this.arrow.color = options.arrow.color || null;
this.arrow.active = typeof (this.arrow.size) === "number";
this.arrow.offset = options.arrow.offset || 2;
// Commit style
this.commit = {};
this.commit.spacingX = options.commit.spacingX || 0;
this.commit.spacingY = (typeof options.commit.spacingY === "number") ? options.commit.spacingY : 25;
this.commit.widthExtension = (typeof options.commit.widthExtension === "number") ? options.commit.widthExtension : 0;
this.commit.tooltipHTMLFormatter = options.commit.tooltipHTMLFormatter || null;
this.commit.shouldDisplayTooltipsInCompactMode = _booleanOptionOr(options.commit.shouldDisplayTooltipsInCompactMode, true);
// Only one color, if null message takes branch color (full commit)
this.commit.color = options.commit.color || null;
this.commit.dot = {};
// Only one color, if null message takes branch color (only dot)
this.commit.dot.color = options.commit.dot.color || null;
this.commit.dot.size = options.commit.dot.size || 3;
this.commit.dot.strokeWidth = options.commit.dot.strokeWidth || null;
this.commit.dot.strokeColor = options.commit.dot.strokeColor || null;
this.commit.tag = {};
this.commit.tag.color = options.commit.tag.color || this.commit.dot.color;
this.commit.tag.font = options.commit.tag.font || options.commit.message.font || "normal 10pt Calibri";
this.commit.tag.spacingX = this.branch.spacingX;
this.commit.tag.spacingY = this.commit.spacingY;
this.commit.message = {};
this.commit.message.display = _booleanOptionOr(options.commit.message.display, true);
this.commit.message.displayAuthor = _booleanOptionOr(options.commit.message.displayAuthor, true);
this.commit.message.displayBranch = _booleanOptionOr(options.commit.message.displayBranch, true);
this.commit.message.displayHash = _booleanOptionOr(options.commit.message.displayHash, true);
// Only one color, if null message takes commit color (only message)
this.commit.message.color = options.commit.message.color || null;
this.commit.message.font = options.commit.message.font || "normal 12pt Calibri";
}
/**
* Get a default template from library
*
* @param {String} name - Template name
*
* @return {Template} [template] - Template if exist
**/
Template.prototype.get = function (name) {
var template = {};
switch (name) {
case "blackarrow":
template = {
branch: {
color: "#000000",
lineWidth: 4,
spacingX: 50,
mergeStyle: "straight",
labelRotation: 0
},
commit: {
spacingY: -60,
dot: {
size: 12,
strokeColor: "#000000",
strokeWidth: 7
},
message: {
color: "black"
}
},
arrow: {
size: 16,
offset: 2.5
}
};
break;
case "metro":
/* falls through */
default:
template = {
colors: ["#979797", "#008fb5", "#f1c109"],
branch: {
lineWidth: 10,
spacingX: 50,
labelRotation: 0
},
commit: {
spacingY: -80,
dot: {
size: 14
},
message: {
font: "normal 14pt Arial"
}
}
};
break;
}
return new Template(template);
};
// --------------------------------------------------------------------
// ----------------------- Utilities -----------------------
// --------------------------------------------------------------------
/**
* Returns the last element of given array.
*
* @param {Array} array
* @returns {*}
* @private */
function _getLast(array) {
return array.slice(-1)[0];
}
/**
* Extend given commit with proper attributes for tag from options.
*
* @param {Commit} commit
* @param {Object} [options]
* @param {String} [options.tag] - Tag of the commit
* @param {String} [options.tagColor = commit.messageColor] - Color of the tag
* @param {String} [options.tagFont = commit.template.commit.tag.font] - Font of the tag
* @param {String} [options.displayTagBox = true] - If true, display a box around the tag
* @private
*/
function _assignTagOptionsToCommit(commit, options) {
commit.tag = options.tag || null;
commit.tagColor = options.tagColor || commit.messageColor;
commit.tagFont = options.tagFont || commit.template.commit.tag.font;
commit.displayTagBox = _booleanOptionOr(options.displayTagBox, true);
}
/**
* Returns the parent commit of current HEAD from given branch.
*
* @param {Branch} branch
* @returns {Commit}
* @private
* */
function _getParentCommitFromBranch(branch) {
if (_getLast(branch.commits)) {
return _getLast(branch.commits);
} else if (branch.parentBranch) {
return _getParentCommitFromBranch(branch.parentBranch);
} else {
return null;
}
}
/**
* Returns a copy of the given object.
*
* @param {Object} object
* @returns {Object}
* @private
* */
function _clone(object) {
return JSON.parse(JSON.stringify(object));
}
/**
* Returns the height of the given font when rendered.
*
* @param {String} font
* @returns {Number}
* @private
*/
function _getFontHeight(font) {
var body = document.getElementsByTagName("body")[0];
var dummy = document.createElement("div");
var dummyText = document.createTextNode("Mg");
dummy.appendChild(dummyText);
dummy.setAttribute("style", "font: " + font + ";");
body.appendChild(dummy);
var fontHeight = dummy.offsetHeight;
body.removeChild(dummy);
return fontHeight;
}
/**
* Returns the `booleanOptions` if it's actually a boolean, returns `defaultOptions` otherwise.
*
* @param {*} booleanOption
* @param {Boolean} defaultOptions
* @returns {Boolean}
* @private
*/
function _booleanOptionOr(booleanOption, defaultOption) {
return (typeof booleanOption === "boolean") ? booleanOption : defaultOption;
}
/**
* Draw text background.
*
* @param {CanvasRenderingContext2D} context - Canvas 2D context in which to render text.
* @param {Number} x - Horizontal offset of the text.
* @param {Number} y - Vertical offset of the text.
* @param {String} text - Text content.
* @param {String} color - Text Colors.
* @param {String} font - Text font.
* @param {Number} angle - Angle of the text for rotation.
* @param {Boolean} useStroke - Name of the triggered event.
* @private
*/
function _drawTextBG(context, x, y, text, color, font, angle, useStroke) {
context.save();
context.translate(x, y);
context.rotate(angle * (Math.PI / 180));
context.textAlign = "center";
context.font = font;
var width = context.measureText(text).width;
var height = _getFontHeight(font);
if (useStroke) {
context.beginPath();
context.rect(-(width / 2) - 4, -(height / 2) + 2, width + 8, height + 2);
context.fillStyle = color;
context.fill();
context.lineWidth = 2;
context.strokeStyle = "black";
context.stroke();
context.fillStyle = "black";
} else {
context.fillStyle = color;
}
context.fillText(text, 0, height / 2);
context.restore();
}
/**
* Emit an event on the given element.
*
* @param {HTMLElement} element - DOM element to trigger the event on.
* @param {String} eventName - Name of the triggered event.
* @param {Object} [data = {}] - Custom data to attach to the event.
* @private
*/
function _emitEvent(element, eventName, data) {
var event;
if (document.createEvent) {
event = document.createEvent("HTMLEvents");
event.initEvent(eventName, true, true);
} else {
event = document.createEventObject();
event.eventType = eventName;
}
event.eventName = eventName;
event.data = data || {};
if (document.createEvent) {
element.dispatchEvent(event);
} else {
element.fireEvent("on" + event.eventType, event);
}
}
/**
* Returns the scaling factor of given canvas `context`.
* Handles high-resolution displays.
*
* @param {Object} context
* @returns {Number}
* @private
*/
function _getScale(context) {
var backingStorePixelRatio;
var scalingFactor;
// Account for high-resolution displays
scalingFactor = 1;
if (window.devicePixelRatio) {
backingStorePixelRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
scalingFactor *= window.devicePixelRatio / backingStorePixelRatio;
}
return scalingFactor;
}
/**
* Returns `true` if `graph` has a vertical orientation.
*
* @param {GitGraph} graph
* @returns {Boolean}
* @private
*/
function _isVertical(graph) {
return (graph.orientation === "vertical" || graph.orientation === "vertical-reverse");
}
/**
* Returns `true` if `graph` has an horizontal orientation.
*
* @param {GitGraph} graph
* @returns {Boolean}
* @private
*/
function _isHorizontal(graph) {
return (graph.orientation === "horizontal" || graph.orientation === "horizontal-reverse");
}
/**
* Returns `true` if `object` is an object.
*
* @param {*} object
* @returns {Boolean}
* @private
*/
function _isObject(object) {
return (typeof object === "object");
}
/**
* Returns `true` if any of the properties (nested or single) of `obj` specified by `key` are undefined or set to a value of null.
* Modified from original source: http://stackoverflow.com/a/23809123.
*
* @param {*} obj - The object whose properties are to be tested as being undefined or equal to null.
* @param {String} key - The property hierarchy of `obj` to be tested, specified using 'dot notation' (e.g. property1.property2.property3 etc).
* @returns {Boolean} - True if ANY of the properties specified by `key` is undefined or equal to null, otherwise False.
* @private
*/
function _isNullOrUndefined(obj, key) {
/* We invert the result of '.every()' in order to meet the expected return value for the condition test of the function.
* We have to do this, given that '.every()' will return immediately upon capturing a falsey value from the callback.
*
* See: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/every for more information.
*/
return !(key.split(".").every(function (x) {
if (typeof obj !== "object" || obj === null || !(x in obj)) {
return false;
}
obj = obj[x];
return true;
}));
}
/* Polyfill for ECMA-252 5th edition Array.prototype.every()
* See: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/every
* for more information.
* */
if (!Array.prototype.every) {
Array.prototype.every = function (callbackFn, thisArg) {
var T, k;
if (this === null) {
throw new TypeError("this is null or not defined");
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callbackFn !== "function") {
throw new TypeError();
}
if (arguments.length > 1) {
T = thisArg;
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
var testResult = callbackFn.call(T, kValue, k, O);
if (!testResult) {
return false;
}
}
k++;
}
return true;
};
}
// Expose GitGraph object
window.GitGraph = GitGraph;
window.GitGraph.Branch = Branch;
window.GitGraph.Commit = Commit;
window.GitGraph.Template = Template;