"use strict";
/**
* 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");
}
/**
* 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
*
* @this GitGraph
**/
function GitGraph ( options ) {
// Options
options = (typeof options === "object") ? 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")
|| (typeof options.template === "object") ) {
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 = 0;
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 = -90;
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 = 90;
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 = 0;
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 = 0;
this.commitOffsetY = 0;
// Bindings
var mouseMoveOptions = {
handleEvent: this.hover,
gitgraph: this
};
this.canvas.addEventListener( "mousemove", mouseMoveOptions, false );
var mouseDownOptions = {
handleEvent: this.click,
gitgraph: this
};
this.canvas.addEventListener( "mousedown", mouseDownOptions, false );
// Render on window resize
window.onresize = this.render.bind( this );
}
/**
* 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 = (typeof options === "object") ? 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 = (typeof options === "object") ? 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;
};
/**
* 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
*/
/**
* 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 ) {
for ( var i = 0, commit; !!(commit = this.commits[ i ]); i++ ) {
var distanceX = (commit.x + (this.offsetX + this.marginX) / this.scalingFactor - event.offsetX);
var distanceY = (commit.y + (this.offsetY + this.marginY) / this.scalingFactor - event.offsetY);
var distanceBetweenCommitCenterAndMouse = Math.sqrt( Math.pow( distanceX, 2 ) + Math.pow( distanceY, 2 ) );
var isOverCommit = distanceBetweenCommitCenterAndMouse < this.template.commit.dot.size;
callbackFn( commit, isOverCommit );
}
};
/**
* 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 ) {
// 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 ) {
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 ) {
if ( !isOverCommit ) {
return;
}
if ( commit.onClick !== null ) {
commit.onClick( commit, true );
}
} );
};
// --------------------------------------------------------------------
// ----------------------- Branch ------------------------
// --------------------------------------------------------------------
/**
* Branch
*
* @constructor
*
* @param {Object} options - Options of branch
* @param {GitGraph} options.parent - GitGraph constructor
* @param {Branch} [options.parentBranch] - Parent branch
* @param {String} [options.name = "no-name"] - Branch name
*
* @this Branch
**/
function Branch ( options ) {
// Check integrity
if ( options.parent instanceof GitGraph === false ) {
return;
}
// Options
options = (typeof options === "object") ? options : {};
this.parent = options.parent;
this.parentBranch = options.parentBranch;
this.name = (typeof options.name === "string") ? options.name : "no-name";
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;
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 = (typeof options === "object") ? 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.template.commit.color || this.template.colors[ columnIndex ];
options.parent = this.parent;
options.parentCommit = options.parentCommit || this.commits.slice( -1 )[ 0 ];
// Special compact mode
if ( this.parent.mode === "compact"
&& this.parent.commits.slice( -1 )[ 0 ]
&& this.parent.commits.slice( -1 )[ 0 ].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.template.commit.message.color || options.color || null;
options.labelColor = options.labelColor || this.template.branch.labelColor || options.color || null;
options.tagColor = options.tagColor || this.template.commit.tag.color || options.color || null;
options.dotColor = options.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 = options.branch.commits.slice( -1 )[ 0 ] || {};
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.parentBranch.commits.slice( -1 )[ 0 ];
}
// 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 ) {
var parent = {
x: commit.parentCommit.branch.offsetX - this.parent.commitOffsetX + this.template.commit.spacingX,
y: commit.parentCommit.branch.offsetY - this.parent.commitOffsetY + this.template.commit.spacingY,
type: "start"
};
this.path.push( JSON.parse( JSON.stringify( parent ) ) ); // Elegant way for cloning an object
parent.type = "join";
this.parentBranch.path.push( parent );
} else if ( isPathBeginning ) {
point.type = "start";
}
// Increment commitOffset for next commit position
this.path.push( point );
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;
};
/**
* Checkout onto this branch
*
* @this Branch
**/
Branch.prototype.checkout = function () {
this.parent.HEAD = this;
};
/**
* Delete this branch
*
* @this Branch
**/
Branch.prototype.delete = function () {
this.isfinish = true;
};
/**
* Merge branch
*
* @param {Branch} [target = this.parent.HEAD]
* @param {(String | Object)} [commitOptions] - Message | Options of commit
*
* @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 = this.commits.slice( -1 )[ 0 ];
targetBranch.commit( commitOptions );
// Add points to path
var targetCommit = targetBranch.commits.slice( -1 )[ 0 ];
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.path.push( JSON.parse( JSON.stringify( endOfBranch ) ) ); // Elegant way for cloning an object
var mergeCommit = {
x: targetCommit.x,
y: targetCommit.y,
type: "end"
};
this.path.push( mergeCommit );
endOfBranch.type = "start";
this.path.push( 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.isfinish ) {
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;
}
}
};
// --------------------------------------------------------------------
// ----------------------- 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] - Specific tag color
* @param {String} [options.tagFont = this.template.commit.tag.font] - Font of 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 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 {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 = (typeof options === "object") ? 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.tag = options.tag || null;
this.tagColor = options.tagColor || options.color;
this.tagFont = options.tagFont || this.template.commit.tag.font;
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.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;
this.parent.commits.push( this );
}
/**
* Render the commit
*
* @this Commit
**/
Commit.prototype.render = function () {
// Label
if ( this.showLabel ) {
drawTextBG( this.context, this.x + this.template.commit.spacingX, this.y + this.template.commit.spacingY, this.branch.name, "black", this.labelColor, this.labelFont, this.template.branch.labelRotation );
}
// 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
var tagWidth = this.template.commit.tag.spacingX;
if ( this.tag !== null ) {
this.parent.tagNum++;
this.context.font = this.tagFont;
var textWidth = this.context.measureText( this.tag ).width;
if ( this.template.branch.labelRotation !== 0 ) {
var textHeight = getFontHeight( this.tagFont );
drawTextBG( this.context,
this.x - this.dotSize / 2,
((this.parent.columnMax + 1) * this.template.commit.tag.spacingY) - this.template.commit.tag.spacingY / 2 + (this.parent.tagNum % 2) * textHeight * 1.5,
this.tag, "black", this.tagColor, this.tagFont, 0 );
} else {
drawTextBG( this.context,
((this.parent.columnMax + 1) * this.template.commit.tag.spacingX) - this.template.commit.tag.spacingX / 2 + textWidth / 2,
this.y - this.dotSize / 2,
this.tag, "black", this.tagColor, this.tagFont, 0 );
}
tagWidth = (tagWidth < textWidth) ? textWidth : tagWidth;
}
this.context.font = this.messageFont;
var commitOffsetLeft = (this.parent.columnMax + 1) * this.template.branch.spacingX + tagWidth;
// 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.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
if ( this.type === "mergeCommit" || this === this.branch.commits[ 0 ] /* First commit */ ) {
var deltaColumn = (this.parentCommit.branch.column - this.branch.column);
var commitSpaceDelta = (this.showLabel ? 2 : 1);
var isArrowVertical = (
isReversed
&& _isVertical( this.parent )
&& Math.abs( this.y - this.parentCommit.y ) > Math.abs( this.template.commit.spacingY )
);
var alphaX = (isArrowVertical)
? 0
: this.template.branch.spacingX * deltaColumn + this.template.commit.spacingX * commitSpaceDelta;
var isArrowHorizontal = (
isReversed
&& _isHorizontal( this.parent )
&& Math.abs( this.x - this.parentCommit.x ) > Math.abs( this.template.commit.spacingX )
);
var alphaY = (isArrowHorizontal)
? 0
: this.template.branch.spacingY * deltaColumn + this.template.commit.spacingY * commitSpaceDelta;
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();
};
// --------------------------------------------------------------------
// ----------------------- 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.offser] - 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 {commitCallback} [options.commit.tooltipHTMLFormatter=true] - Formatter for the tooltip contents.
*
* @this Template
**/
function Template ( options ) {
// Options
options = (typeof options === "object") ? 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";
this.branch.labelRotation = options.branch.labelRotation || 0;
// 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"
},
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
},
commit: {
spacingY: -80,
dot: {
size: 14
},
message: {
font: "normal 14pt Arial"
}
}
};
break;
}
return new Template( template );
};
// --------------------------------------------------------------------
// ----------------------- Utilities -----------------------
// --------------------------------------------------------------------
var getFontHeight = function ( 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 result = dummy.offsetHeight;
body.removeChild( dummy );
return result;
};
function booleanOptionOr ( booleanOption, defaultOption ) {
return (typeof booleanOption === "boolean") ? booleanOption : defaultOption;
}
function drawTextBG ( context, x, y, text, fgcolor, bgcolor, font, angle ) {
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 );
context.beginPath();
context.rect( -(width / 2) - 4, -(height / 2) + 2, width + 8, height + 2 );
context.fillStyle = bgcolor;
context.fill();
context.lineWidth = 2;
context.strokeStyle = "black";
context.stroke();
context.fillStyle = fgcolor;
context.fillText( text, 0, height / 2 );
context.restore();
}
// Expose GitGraph object
window.GitGraph = GitGraph;
window.GitGraph.Branch = Branch;
window.GitGraph.Commit = Commit;
window.GitGraph.Template = Template;