angular-gantt
Version:
Gantt chart component for AngularJS
1,229 lines (1,063 loc) • 212 kB
JavaScript
/*
Project: angular-gantt v1.2.9 - Gantt chart component for AngularJS
Authors: Marco Schweighauser, Rémi Alvergnat
License: MIT
Homepage: https://www.angular-gantt.com
Github: https://github.com/angular-gantt/angular-gantt.git
*/
(function(){
'use strict';
angular.module('gantt', ['gantt.templates', 'angularMoment'])
.directive('gantt', ['Gantt', 'ganttEnableNgAnimate', '$timeout', '$templateCache', function(Gantt, enableNgAnimate, $timeout, $templateCache) {
return {
restrict: 'A',
transclude: true,
templateUrl: function(tElement, tAttrs) {
var templateUrl;
if (tAttrs.templateUrl === undefined) {
templateUrl = 'template/gantt.tmpl.html';
} else {
templateUrl = tAttrs.templateUrl;
}
if (tAttrs.template !== undefined) {
$templateCache.put(templateUrl, tAttrs.template);
}
return templateUrl;
},
scope: {
sortMode: '=?',
filterTask: '=?',
filterTaskComparator: '=?',
filterRow: '=?',
filterRowComparator: '=?',
viewScale: '=?',
columnWidth: '=?',
expandToFit: '=?',
shrinkToFit: '=?',
showSide: '=?',
allowSideResizing: '=?',
fromDate: '=?',
toDate: '=?',
currentDateValue: '=?',
currentDate: '=?',
daily: '=?',
autoExpand: '=?',
taskOutOfRange: '=?',
taskContent: '=?',
rowContent: '=?',
maxHeight: '=?',
sideWidth: '=?',
headers: '=?',
headersFormats: '=?',
timeFrames: '=?',
dateFrames: '=?',
timeFramesWorkingMode: '=?',
timeFramesNonWorkingMode: '=?',
timespans: '=?',
columnMagnet: '=?',
shiftColumnMagnet: '=?',
timeFramesMagnet: '=?',
data: '=?',
api: '=?',
options: '=?'
},
controller: ['$scope', '$element', function($scope, $element) {
for (var option in $scope.options) {
$scope[option] = $scope.options[option];
}
// Disable animation if ngAnimate is present, as it drops down performance.
enableNgAnimate($element, false);
$scope.gantt = new Gantt($scope, $element);
this.gantt = $scope.gantt;
}],
link: function(scope, element) {
scope.gantt.api.directives.raise.new('gantt', scope, element);
scope.$on('$destroy', function() {
scope.gantt.api.directives.raise.destroy('gantt', scope, element);
});
$timeout(function() {
scope.gantt.initialized();
});
}
};
}]);
}());
// This file is adapted from Angular UI ngGrid project
// MIT License
// https://github.com/angular-ui/ng-grid/blob/v3.0.0-rc.20/src/js/core/factories/GridApi.js
(function() {
'use strict';
angular.module('gantt')
.factory('GanttApi', ['$q', '$rootScope', 'ganttUtils',
function($q, $rootScope, utils) {
/**
* @ngdoc function
* @name gantt.class:GanttApi
* @description GanttApi provides the ability to register public methods events inside the gantt and allow
* for other components to use the api via featureName.raise.methodName and featureName.on.eventName(function(args){}.
* @param {object} gantt gantt that owns api
*/
var GanttApi = function GanttApi(gantt) {
this.gantt = gantt;
this.listeners = [];
this.apiId = utils.newId();
};
/**
* @ngdoc function
* @name gantt.class:suppressEvents
* @methodOf gantt.class:GanttApi
* @description Used to execute a function while disabling the specified event listeners.
* Disables the listenerFunctions, executes the callbackFn, and then enables
* the listenerFunctions again
* @param {object} listenerFuncs listenerFunc or array of listenerFuncs to suppress. These must be the same
* functions that were used in the .on.eventName method
* @param {object} callBackFn function to execute
* @example
* <pre>
* var navigate = function (newRowCol, oldRowCol){
* //do something on navigate
* }
*
* ganttApi.cellNav.on.navigate(scope,navigate);
*
*
* //call the scrollTo event and suppress our navigate listener
* //scrollTo will still raise the event for other listeners
* ganttApi.suppressEvents(navigate, function(){
* ganttApi.cellNav.scrollTo(aRow, aCol);
* });
*
* </pre>
*/
GanttApi.prototype.suppressEvents = function(listenerFuncs, callBackFn) {
var self = this;
var listeners = angular.isArray(listenerFuncs) ? listenerFuncs : [listenerFuncs];
//find all registered listeners
var foundListeners = [];
listeners.forEach(function(l) {
foundListeners = self.listeners.filter(function(lstnr) {
return l === lstnr.handler;
});
});
//deregister all the listeners
foundListeners.forEach(function(l) {
l.dereg();
});
callBackFn();
//reregister all the listeners
foundListeners.forEach(function(l) {
l.dereg = registerEventWithAngular(l.eventId, l.handler, self.gantt, l._this);
});
};
/**
* @ngdoc function
* @name registerEvent
* @methodOf gantt.class:GanttApi
* @description Registers a new event for the given feature. The event will get a
* .raise and .on prepended to it
* <br>
* .raise.eventName() - takes no arguments
* <br/>
* <br/>
* .on.eventName(scope, callBackFn, _this)
* <br/>
* scope - a scope reference to add a deregister call to the scopes .$on('destroy')
* <br/>
* callBackFn - The function to call
* <br/>
* _this - optional this context variable for callbackFn. If omitted, gantt.api will be used for the context
* <br/>
* .on.eventName returns a dereg funtion that will remove the listener. It's not necessary to use it as the listener
* will be removed when the scope is destroyed.
* @param {string} featureName name of the feature that raises the event
* @param {string} eventName name of the event
*/
GanttApi.prototype.registerEvent = function(featureName, eventName) {
var self = this;
if (!self[featureName]) {
self[featureName] = {};
}
var feature = self[featureName];
if (!feature.on) {
feature.on = {};
feature.raise = {};
}
var eventId = 'event:gantt:' + this.apiId + ':' + featureName + ':' + eventName;
// Creating raise event method featureName.raise.eventName
feature.raise[eventName] = function() {
$rootScope.$emit.apply($rootScope, [eventId].concat(Array.prototype.slice.call(arguments)));
};
// Creating on event method featureName.oneventName
feature.on[eventName] = function(scope, handler, _this) {
var deregAngularOn = registerEventWithAngular(eventId, handler, self.gantt, _this);
//track our listener so we can turn off and on
var listener = {
handler: handler,
dereg: deregAngularOn,
eventId: eventId,
scope: scope,
_this: _this
};
self.listeners.push(listener);
var removeListener = function() {
listener.dereg();
var index = self.listeners.indexOf(listener);
self.listeners.splice(index, 1);
};
//destroy tracking when scope is destroyed
scope.$on('$destroy', function() {
removeListener();
});
return removeListener;
};
};
function registerEventWithAngular(eventId, handler, gantt, _this) {
return $rootScope.$on(eventId, function() {
var args = Array.prototype.slice.call(arguments);
args.splice(0, 1); //remove evt argument
handler.apply(_this ? _this : gantt.api, args);
});
}
/**
* @ngdoc function
* @name registerEventsFromObject
* @methodOf gantt.class:GanttApi
* @description Registers features and events from a simple objectMap.
* eventObjectMap must be in this format (multiple features allowed)
* <pre>
* {featureName:
* {
* eventNameOne:function(args){},
* eventNameTwo:function(args){}
* }
* }
* </pre>
* @param {object} eventObjectMap map of feature/event names
*/
GanttApi.prototype.registerEventsFromObject = function(eventObjectMap) {
var self = this;
var features = [];
angular.forEach(eventObjectMap, function(featProp, featPropName) {
var feature = {name: featPropName, events: []};
angular.forEach(featProp, function(prop, propName) {
feature.events.push(propName);
});
features.push(feature);
});
features.forEach(function(feature) {
feature.events.forEach(function(event) {
self.registerEvent(feature.name, event);
});
});
};
/**
* @ngdoc function
* @name registerMethod
* @methodOf gantt.class:GanttApi
* @description Registers a new event for the given feature
* @param {string} featureName name of the feature
* @param {string} methodName name of the method
* @param {object} callBackFn function to execute
* @param {object} _this binds callBackFn 'this' to _this. Defaults to ganttApi.gantt
*/
GanttApi.prototype.registerMethod = function(featureName, methodName, callBackFn, _this) {
if (!this[featureName]) {
this[featureName] = {};
}
var feature = this[featureName];
feature[methodName] = utils.createBoundedWrapper(_this || this.gantt, callBackFn);
};
/**
* @ngdoc function
* @name registerMethodsFromObject
* @methodOf gantt.class:GanttApi
* @description Registers features and methods from a simple objectMap.
* eventObjectMap must be in this format (multiple features allowed)
* <br>
* {featureName:
* {
* methodNameOne:function(args){},
* methodNameTwo:function(args){}
* }
* @param {object} eventObjectMap map of feature/event names
* @param {object} _this binds this to _this for all functions. Defaults to ganttApi.gantt
*/
GanttApi.prototype.registerMethodsFromObject = function(methodMap, _this) {
var self = this;
var features = [];
angular.forEach(methodMap, function(featProp, featPropName) {
var feature = {name: featPropName, methods: []};
angular.forEach(featProp, function(prop, propName) {
feature.methods.push({name: propName, fn: prop});
});
features.push(feature);
});
features.forEach(function(feature) {
feature.methods.forEach(function(method) {
self.registerMethod(feature.name, method.name, method.fn, _this);
});
});
};
return GanttApi;
}]);
})();
(function() {
'use strict';
angular.module('gantt').factory('GanttOptions', [function() {
var GanttOptions = function(values, defaultValues) {
this.defaultValues = defaultValues;
this.values = values;
this.defaultValue = function(optionName) {
var defaultValue = this.defaultValues[optionName];
if (angular.isFunction(defaultValue)) {
defaultValue = defaultValue();
}
return defaultValue;
};
this.sanitize = function(optionName, optionValue) {
if (!optionValue) {
var defaultValue = this.defaultValue(optionName);
if (defaultValue !== undefined) {
if (optionValue !== undefined && typeof defaultValue === 'boolean') {
return optionValue;
}
return defaultValue;
}
}
return optionValue;
};
this.value = function(optionName) {
return this.sanitize(optionName, this.values[optionName]);
};
this.set = function(optionName, optionValue) {
this.values[optionName] = optionValue;
};
this.initialize = function() {
for (var optionName in this.values) {
var optionValue = this.values[optionName];
if (this.values.hasOwnProperty(optionName)) {
this.values[optionName] = this.value(optionName, optionValue);
}
}
return this.values;
};
};
return GanttOptions;
}]);
}());
(function(){
'use strict';
/**
* Calendar factory is used to define working periods, non working periods, and other specific period of time,
* and retrieve effective timeFrames for each day of the gantt.
*/
angular.module('gantt').factory('GanttCalendar', ['$filter', 'moment', function($filter, moment) {
/**
* TimeFrame represents time frame in any day. parameters are given using options object.
*
* @param {moment|string} start start of timeFrame. If a string is given, it will be parsed as a moment.
* @param {moment|string} end end of timeFrame. If a string is given, it will be parsed as a moment.
* @param {boolean} working is this timeFrame flagged as working.
* @param {boolean} magnet is this timeFrame will magnet.
* @param {boolean} default is this timeFrame will be used as default.
* @param {color} css color attached to this timeFrame.
* @param {string} classes css classes attached to this timeFrame.
*
* @constructor
*/
var TimeFrame = function(options) {
if (options === undefined) {
options = {};
}
this.start = options.start;
this.end = options.end;
this.working = options.working;
this.magnet = options.magnet !== undefined ? options.magnet : true;
this.default = options.default;
this.color = options.color;
this.classes = options.classes;
this.internal = options.internal;
};
TimeFrame.prototype.updateView = function() {
if (this.$element) {
var cssStyles = {};
if (this.left !== undefined) {
cssStyles.left = this.left + 'px';
} else {
cssStyles.left = '';
}
if (this.width !== undefined) {
cssStyles.width = this.width + 'px';
} else {
cssStyles.width = '';
}
if (this.color !== undefined) {
cssStyles['background-color'] = this.color;
} else {
cssStyles['background-color'] = '';
}
this.$element.css(cssStyles);
var classes = ['gantt-timeframe' + (this.working ? '' : '-non') + '-working'];
if (this.classes) {
classes = classes.concat(this.classes);
}
for (var i = 0, l = classes.length; i < l; i++) {
this.$element.toggleClass(classes[i], true);
}
}
};
TimeFrame.prototype.getDuration = function() {
if (this.end !== undefined && this.start !== undefined) {
return this.end.diff(this.start, 'milliseconds');
}
};
TimeFrame.prototype.clone = function() {
return new TimeFrame(this);
};
/**
* TimeFrameMapping defines how timeFrames will be placed for each days. parameters are given using options object.
*
* @param {function} func a function with date parameter, that will be evaluated for each distinct day of the gantt.
* this function must return an array of timeFrame names to apply.
* @constructor
*/
var TimeFrameMapping = function(func) {
this.func = func;
};
TimeFrameMapping.prototype.getTimeFrames = function(date) {
var ret = this.func(date);
if (!(ret instanceof Array)) {
ret = [ret];
}
return ret;
};
TimeFrameMapping.prototype.clone = function() {
return new TimeFrameMapping(this.func);
};
/**
* A DateFrame is date range that will use a specific TimeFrameMapping, configured using a function (evaluator),
* a date (date) or a date range (start, end). parameters are given using options object.
*
* @param {function} evaluator a function with date parameter, that will be evaluated for each distinct day of the gantt.
* this function must return a boolean representing matching of this dateFrame or not.
* @param {moment} date date of dateFrame.
* @param {moment} start start of date frame.
* @param {moment} end end of date frame.
* @param {array} targets array of TimeFrameMappings/TimeFrames names to use for this date frame.
* @param {boolean} default is this dateFrame will be used as default.
* @constructor
*/
var DateFrame = function(options) {
this.evaluator = options.evaluator;
if (options.date) {
this.start = moment(options.date).startOf('day');
this.end = moment(options.date).endOf('day');
} else {
this.start = options.start;
this.end = options.end;
}
if (options.targets instanceof Array) {
this.targets = options.targets;
} else {
this.targets = [options.targets];
}
this.default = options.default;
};
DateFrame.prototype.dateMatch = function(date) {
if (this.evaluator) {
return this.evaluator(date);
} else if (this.start && this.end) {
return date >= this.start && date <= this.end;
} else {
return false;
}
};
DateFrame.prototype.clone = function() {
return new DateFrame(this);
};
/**
* Register TimeFrame, TimeFrameMapping and DateMapping objects into Calendar object,
* and use Calendar#getTimeFrames(date) function to retrieve effective timeFrames for a specific day.
*
* @constructor
*/
var Calendar = function() {
this.timeFrames = {};
this.timeFrameMappings = {};
this.dateFrames = {};
};
/**
* Remove all objects.
*/
Calendar.prototype.clear = function() {
this.timeFrames = {};
this.timeFrameMappings = {};
this.dateFrames = {};
};
/**
* Register TimeFrame objects.
*
* @param {object} timeFrames with names of timeFrames for keys and TimeFrame objects for values.
*/
Calendar.prototype.registerTimeFrames = function(timeFrames) {
angular.forEach(timeFrames, function(timeFrame, name) {
this.timeFrames[name] = new TimeFrame(timeFrame);
}, this);
};
/**
* Removes TimeFrame objects.
*
* @param {array} timeFrames names of timeFrames to remove.
*/
Calendar.prototype.removeTimeFrames = function(timeFrames) {
angular.forEach(timeFrames, function(name) {
delete this.timeFrames[name];
}, this);
};
/**
* Remove all TimeFrame objects.
*/
Calendar.prototype.clearTimeFrames = function() {
this.timeFrames = {};
};
/**
* Register TimeFrameMapping objects.
*
* @param {object} mappings object with names of timeFrames mappings for keys and TimeFrameMapping objects for values.
*/
Calendar.prototype.registerTimeFrameMappings = function(mappings) {
angular.forEach(mappings, function(timeFrameMapping, name) {
this.timeFrameMappings[name] = new TimeFrameMapping(timeFrameMapping);
}, this);
};
/**
* Removes TimeFrameMapping objects.
*
* @param {array} mappings names of timeFrame mappings to remove.
*/
Calendar.prototype.removeTimeFrameMappings = function(mappings) {
angular.forEach(mappings, function(name) {
delete this.timeFrameMappings[name];
}, this);
};
/**
* Removes all TimeFrameMapping objects.
*/
Calendar.prototype.clearTimeFrameMappings = function() {
this.timeFrameMappings = {};
};
/**
* Register DateFrame objects.
*
* @param {object} dateFrames object with names of dateFrames for keys and DateFrame objects for values.
*/
Calendar.prototype.registerDateFrames = function(dateFrames) {
angular.forEach(dateFrames, function(dateFrame, name) {
this.dateFrames[name] = new DateFrame(dateFrame);
}, this);
};
/**
* Remove DateFrame objects.
*
* @param {array} mappings names of date frames to remove.
*/
Calendar.prototype.removeDateFrames = function(dateFrames) {
angular.forEach(dateFrames, function(name) {
delete this.dateFrames[name];
}, this);
};
/**
* Removes all DateFrame objects.
*/
Calendar.prototype.clearDateFrames = function() {
this.dateFrames = {};
};
var filterDateFrames = function(inputDateFrames, date) {
var dateFrames = [];
angular.forEach(inputDateFrames, function(dateFrame) {
if (dateFrame.dateMatch(date)) {
dateFrames.push(dateFrame);
}
});
if (dateFrames.length === 0) {
angular.forEach(inputDateFrames, function(dateFrame) {
if (dateFrame.default) {
dateFrames.push(dateFrame);
}
});
}
return dateFrames;
};
/**
* Retrieves TimeFrame objects for a given date, using whole configuration for this Calendar object.
*
* @param {moment} date
*
* @return {array} an array of TimeFrame objects.
*/
Calendar.prototype.getTimeFrames = function(date) {
var timeFrames = [];
var dateFrames = filterDateFrames(this.dateFrames, date);
angular.forEach(dateFrames, function(dateFrame) {
if (dateFrame !== undefined) {
angular.forEach(dateFrame.targets, function(timeFrameMappingName) {
var timeFrameMapping = this.timeFrameMappings[timeFrameMappingName];
if (timeFrameMapping !== undefined) {
// If a timeFrame mapping is found
timeFrames.push(timeFrameMapping.getTimeFrames());
} else {
// If no timeFrame mapping is found, try using direct timeFrame
var timeFrame = this.timeFrames[timeFrameMappingName];
if (timeFrame !== undefined) {
timeFrames.push(timeFrame);
}
}
}, this);
}
}, this);
var dateYear = date.year();
var dateMonth = date.month();
var dateDate = date.date();
var validatedTimeFrames = [];
if (timeFrames.length === 0) {
angular.forEach(this.timeFrames, function(timeFrame) {
if (timeFrame.default) {
timeFrames.push(timeFrame);
}
});
}
angular.forEach(timeFrames, function(timeFrame) {
timeFrame = timeFrame.clone();
if (timeFrame.start !== undefined) {
timeFrame.start.year(dateYear);
timeFrame.start.month(dateMonth);
timeFrame.start.date(dateDate);
}
if (timeFrame.end !== undefined) {
timeFrame.end.year(dateYear);
timeFrame.end.month(dateMonth);
timeFrame.end.date(dateDate);
if (moment(timeFrame.end).startOf('day') === timeFrame.end) {
timeFrame.end.add(1, 'day');
}
}
validatedTimeFrames.push(timeFrame);
});
return validatedTimeFrames;
};
/**
* Solve timeFrames.
*
* Smaller timeFrames have priority over larger one.
*
* @param {array} timeFrames Array of timeFrames to solve
* @param {moment} startDate
* @param {moment} endDate
*/
Calendar.prototype.solve = function(timeFrames, startDate, endDate) {
var color;
var classes;
var minDate;
var maxDate;
angular.forEach(timeFrames, function(timeFrame) {
if (minDate === undefined || minDate > timeFrame.start) {
minDate = timeFrame.start;
}
if (maxDate === undefined || maxDate < timeFrame.end) {
maxDate = timeFrame.end;
}
if (color === undefined && timeFrame.color) {
color = timeFrame.color;
}
if (timeFrame.classes !== undefined) {
if (classes === undefined) {
classes = [];
}
classes = classes.concat(timeFrame.classes);
}
});
if (startDate === undefined) {
startDate = minDate;
}
if (endDate === undefined) {
endDate = maxDate;
}
var solvedTimeFrames = [new TimeFrame({start: startDate, end: endDate, internal: true})];
timeFrames = $filter('filter')(timeFrames, function(timeFrame) {
return (timeFrame.start === undefined || timeFrame.start < endDate) && (timeFrame.end === undefined || timeFrame.end > startDate);
});
angular.forEach(timeFrames, function(timeFrame) {
if (!timeFrame.start) {
timeFrame.start = startDate;
}
if (!timeFrame.end) {
timeFrame.end = endDate;
}
});
var orderedTimeFrames = $filter('orderBy')(timeFrames, function(timeFrame) {
return -timeFrame.getDuration();
});
angular.forEach(orderedTimeFrames, function(timeFrame) {
var tmpSolvedTimeFrames = solvedTimeFrames.slice();
var i=0;
var dispatched = false;
var treated = false;
angular.forEach(solvedTimeFrames, function(solvedTimeFrame) {
if (!treated) {
if (!timeFrame.end && !timeFrame.start) {
// timeFrame is infinite.
tmpSolvedTimeFrames.splice(i, 0, timeFrame);
treated = true;
dispatched = false;
} else if (timeFrame.end > solvedTimeFrame.start && timeFrame.start < solvedTimeFrame.end) {
// timeFrame is included in this solvedTimeFrame.
// solvedTimeFrame:|ssssssssssssssssssssssssssssssssss|
// timeFrame: |tttttt|
// result:|sssssssss|tttttt|sssssssssssssssss|
var newSolvedTimeFrame = solvedTimeFrame.clone();
solvedTimeFrame.end = moment(timeFrame.start);
newSolvedTimeFrame.start = moment(timeFrame.end);
tmpSolvedTimeFrames.splice(i + 1, 0, timeFrame.clone(), newSolvedTimeFrame);
treated = true;
dispatched = false;
} else if (!dispatched && timeFrame.start < solvedTimeFrame.end) {
// timeFrame is dispatched on two solvedTimeFrame.
// First part
// solvedTimeFrame:|sssssssssssssssssssssssssssssssssss|s+1;s+1;s+1;s+1;s+1;s+1|
// timeFrame: |tttttt|
// result:|sssssssssssssssssssssssssssssss|tttttt|;s+1;s+1;s+1;s+1;s+1|
solvedTimeFrame.end = moment(timeFrame.start);
tmpSolvedTimeFrames.splice(i + 1, 0, timeFrame.clone());
dispatched = true;
} else if (dispatched && timeFrame.end > solvedTimeFrame.start) {
// timeFrame is dispatched on two solvedTimeFrame.
// Second part
solvedTimeFrame.start = moment(timeFrame.end);
dispatched = false;
treated = true;
}
i++;
}
});
solvedTimeFrames = tmpSolvedTimeFrames;
});
solvedTimeFrames = $filter('filter')(solvedTimeFrames, function(timeFrame) {
return !timeFrame.internal &&
(timeFrame.start === undefined || timeFrame.start < endDate) &&
(timeFrame.end === undefined || timeFrame.end > startDate);
});
return solvedTimeFrames;
};
return Calendar;
}]);
}());
(function(){
'use strict';
angular.module('gantt').factory('GanttCurrentDateManager', ['moment', function(moment) {
var GanttCurrentDateManager = function(gantt) {
var self = this;
this.gantt = gantt;
this.date = undefined;
this.position = undefined;
this.currentDateColumn = undefined;
this.gantt.$scope.simplifyMoment = function(d) {
return moment.isMoment(d) ? d.unix() : d;
};
this.gantt.$scope.$watchGroup(['currentDate', 'simplifyMoment(currentDateValue)'], function(newValues, oldValues) {
if (newValues !== oldValues) {
self.setCurrentDate(self.gantt.options.value('currentDateValue'));
}
});
};
GanttCurrentDateManager.prototype.setCurrentDate = function(currentDate) {
this.date = currentDate;
var oldColumn = this.currentDateColumn;
var newColumn;
if (this.date !== undefined && this.gantt.options.value('currentDate') === 'column') {
newColumn = this.gantt.columnsManager.getColumnByDate(this.date, true);
}
this.currentDateColumn = newColumn;
if (oldColumn !== newColumn) {
if (oldColumn !== undefined) {
oldColumn.currentDate = false;
oldColumn.updateView();
}
if (newColumn !== undefined) {
newColumn.currentDate = true;
newColumn.updateView();
}
}
this.position = this.gantt.getPositionByDate(this.date, true);
};
return GanttCurrentDateManager;
}]);
}());
(function() {
'use strict';
angular.module('gantt').factory('GanttColumn', ['moment', function(moment) {
// Used to display the Gantt grid and header.
// The columns are generated by the column generator.
var Column = function(date, endDate, left, width, calendar, timeFramesWorkingMode, timeFramesNonWorkingMode) {
this.date = date;
this.endDate = endDate;
this.left = left;
this.width = width;
this.calendar = calendar;
this.duration = this.endDate.diff(this.date, 'milliseconds');
this.timeFramesWorkingMode = timeFramesWorkingMode;
this.timeFramesNonWorkingMode = timeFramesNonWorkingMode;
this.timeFrames = [];
this.currentDate = false;
this.visibleTimeFrames = [];
this.daysTimeFrames = {};
this.cropped = false;
this.originalSize = {left: this.left, width: this.width};
this.updateTimeFrames();
};
var getDateKey = function(date) {
return date.year() + '-' + date.month() + '-' + date.date();
};
Column.prototype.updateView = function() {
if (this.$element) {
if (this.currentDate) {
this.$element.addClass('gantt-foreground-col-current-date');
} else {
this.$element.removeClass('gantt-foreground-col-current-date');
}
this.$element.css({'left': this.left + 'px', 'width': this.width + 'px'});
for (var i = 0, l = this.timeFrames.length; i < l; i++) {
this.timeFrames[i].updateView();
}
}
};
Column.prototype.updateTimeFrames = function() {
var self = this;
if (self.calendar !== undefined && (self.timeFramesNonWorkingMode !== 'hidden' || self.timeFramesWorkingMode !== 'hidden')) {
var buildPushTimeFrames = function(timeFrames, startDate, endDate) {
return function(timeFrame) {
var start = timeFrame.start;
if (start === undefined) {
start = startDate;
}
var end = timeFrame.end;
if (end === undefined) {
end = endDate;
}
if (start < self.date) {
start = self.date;
}
if (end > self.endDate) {
end = self.endDate;
}
timeFrame = timeFrame.clone();
timeFrame.start = moment(start);
timeFrame.end = moment(end);
timeFrames.push(timeFrame);
};
};
var cDate = self.date;
var cDateStartOfDay = moment(cDate).startOf('day');
var cDateNextDay = cDateStartOfDay.add(1, 'day');
while (cDate < self.endDate) {
var timeFrames = self.calendar.getTimeFrames(cDate);
var nextCDate = moment.min(cDateNextDay, self.endDate);
timeFrames = self.calendar.solve(timeFrames, cDate, nextCDate);
var cTimeFrames = [];
angular.forEach(timeFrames, buildPushTimeFrames(cTimeFrames, cDate, nextCDate));
self.timeFrames = self.timeFrames.concat(cTimeFrames);
var cDateKey = getDateKey(cDate);
self.daysTimeFrames[cDateKey] = cTimeFrames;
cDate = nextCDate;
cDateStartOfDay = moment(cDate).startOf('day');
cDateNextDay = cDateStartOfDay.add(1, 'day');
}
angular.forEach(self.timeFrames, function(timeFrame) {
var positionDuration = timeFrame.start.diff(self.date, 'milliseconds');
var position = positionDuration / self.duration * self.width;
var timeFrameDuration = timeFrame.end.diff(timeFrame.start, 'milliseconds');
var timeFramePosition = timeFrameDuration / self.duration * self.width;
var hidden = false;
if (timeFrame.working && self.timeFramesWorkingMode !== 'visible') {
hidden = true;
} else if (!timeFrame.working && self.timeFramesNonWorkingMode !== 'visible') {
hidden = true;
}
if (!hidden) {
self.visibleTimeFrames.push(timeFrame);
}
timeFrame.hidden = hidden;
timeFrame.left = position;
timeFrame.width = timeFramePosition;
timeFrame.originalSize = {left: timeFrame.left, width: timeFrame.width};
});
if (self.timeFramesNonWorkingMode === 'cropped' || self.timeFramesWorkingMode === 'cropped') {
var timeFramesWidth = 0;
angular.forEach(self.timeFrames, function(timeFrame) {
if (!timeFrame.working && self.timeFramesNonWorkingMode !== 'cropped' ||
timeFrame.working && self.timeFramesWorkingMode !== 'cropped') {
timeFramesWidth += timeFrame.width;
}
});
if (timeFramesWidth !== self.width) {
var croppedRatio = self.width / timeFramesWidth;
var croppedWidth = 0;
var originalCroppedWidth = 0;
var allCropped = true;
angular.forEach(self.timeFrames, function(timeFrame) {
if (!timeFrame.working && self.timeFramesNonWorkingMode !== 'cropped' ||
timeFrame.working && self.timeFramesWorkingMode !== 'cropped') {
timeFrame.left = (timeFrame.left - croppedWidth) * croppedRatio;
timeFrame.width = timeFrame.width * croppedRatio;
timeFrame.originalSize.left = (timeFrame.originalSize.left - originalCroppedWidth) * croppedRatio;
timeFrame.originalSize.width = timeFrame.originalSize.width * croppedRatio;
timeFrame.cropped = false;
allCropped = false;
} else {
croppedWidth += timeFrame.width;
originalCroppedWidth += timeFrame.originalSize.width;
timeFrame.left = undefined;
timeFrame.width = 0;
timeFrame.originalSize = {left: undefined, width: 0};
timeFrame.cropped = true;
}
});
self.cropped = allCropped;
} else {
self.cropped = false;
}
}
}
};
Column.prototype.clone = function() {
return new Column(moment(this.date), moment(this.endDate), this.left, this.width, this.calendar);
};
Column.prototype.containsDate = function(date) {
return date > this.date && date <= this.endDate;
};
Column.prototype.equals = function(other) {
return this.date === other.date;
};
Column.prototype.roundTo = function(date, unit, offset, midpoint) {
// Waiting merge of https://github.com/moment/moment/pull/1794
if (unit === 'day') {
// Inconsistency in units in momentJS.
unit = 'date';
}
offset = offset || 1;
var value = date.get(unit);
switch (midpoint) {
case 'up':
value = Math.ceil(value / offset);
break;
case 'down':
value = Math.floor(value / offset);
break;
default:
value = Math.round(value / offset);
break;
}
var units = ['millisecond', 'second', 'minute', 'hour', 'date', 'month', 'year'];
date.set(unit, value * offset);
var indexOf = units.indexOf(unit);
for (var i = 0; i < indexOf; i++) {
date.set(units[i], 0);
}
return date;
};
Column.prototype.getMagnetDate = function(date, magnetValue, magnetUnit, timeFramesMagnet) {
if (magnetValue > 0 && magnetUnit !== undefined) {
var initialDate = date;
date = moment(date);
if (magnetUnit === 'column') {
// Snap to column borders only.
var position = this.getPositionByDate(date);
if (position < this.width / 2) {
date = moment(this.date);
} else {
date = moment(this.endDate);
}
} else {
// Round the value
date = this.roundTo(date, magnetUnit, magnetValue);
// Snap to column borders if date overflows.
if (date < this.date) {
date = moment(this.date);
} else if (date > this.endDate) {
date = moment(this.endDate);
}
}
if (timeFramesMagnet) {
var maxTimeFrameDiff = Math.abs(initialDate.diff(date, 'milliseconds'));
var currentTimeFrameDiff;
for (var i=0; i<this.timeFrames.length; i++) {
var timeFrame = this.timeFrames[i];
if (timeFrame.magnet) {
var previousTimeFrame = this.timeFrames[i-1];
var nextTimeFrame = this.timeFrames[i+1];
var timeFrameDiff;
if (previousTimeFrame === undefined || previousTimeFrame.working !== timeFrame.working) {
timeFrameDiff = Math.abs(initialDate.diff(timeFrame.start, 'milliseconds'));
if (timeFrameDiff < maxTimeFrameDiff && (currentTimeFrameDiff === undefined || timeFrameDiff < currentTimeFrameDiff)) {
currentTimeFrameDiff = timeFrameDiff;
date = timeFrame.start;
}
}
if (nextTimeFrame === undefined || nextTimeFrame.working !== timeFrame.working) {
timeFrameDiff = Math.abs(initialDate.diff(timeFrame.end, 'milliseconds'));
if (timeFrameDiff < maxTimeFrameDiff && (currentTimeFrameDiff === undefined || timeFrameDiff < currentTimeFrameDiff)) {
currentTimeFrameDiff = timeFrameDiff;
date = timeFrame.end;
}
}
}
}
}
}
return date;
};
Column.prototype.getDateByPositionUsingTimeFrames = function(position) {
for (var i = 0, l = this.timeFrames.length; i < l; i++) {
// TODO: performance optimization could be done.
var timeFrame = this.timeFrames[i];
if (!timeFrame.cropped && position >= timeFrame.left && position <= timeFrame.left + timeFrame.width) {
var positionDuration = timeFrame.getDuration() / timeFrame.width * (position - timeFrame.left);
var date = moment(timeFrame.start).add(positionDuration, 'milliseconds');
return date;
}
}
};
Column.prototype.getDateByPosition = function(position, magnetValue, magnetUnit, timeFramesMagnet) {
var positionDuration;
var date;
if (position < 0) {
position = 0;
}
if (position > this.width) {
position = this.width;
}
if (this.timeFramesNonWorkingMode === 'cropped' || this.timeFramesWorkingMode === 'cropped') {
date = this.getDateByPositionUsingTimeFrames(position);
}
if (date === undefined) {
positionDuration = this.duration / this.width * position;
date = moment(this.date).add(positionDuration, 'milliseconds');
}
date = this.getMagnetDate(date, magnetValue, magnetUnit, timeFramesMagnet);
return date;
};
Column.prototype.getDayTimeFrame = function(date) {
var dtf = this.daysTimeFrames[getDateKey(date)];
if (dtf === undefined) {
return [];
}
return dtf;
};
Column.prototype.getPositionByDate = function(date) {
var positionDuration;
var position;
if (this.timeFramesNonWorkingMode === 'cropped' || this.timeFramesWorkingMode === 'cropped') {
var croppedDate = date;
var timeFrames = this.getDayTimeFrame(croppedDate);
for (var i = 0; i < timeFrames.length; i++) {
var timeFrame = timeFrames[i];
if (croppedDate >=