UNPKG

angular-gantt

Version:

Gantt chart component for AngularJS

1,229 lines (1,063 loc) 212 kB
/* 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 >=