angular-gantt
Version:
Gantt chart component for AngularJS
537 lines (464 loc) • 15.2 kB
text/typescript
import {IAugmentedJQuery, IFilterService} from 'angular'
import moment, {MomentInput} from 'moment'
export type TimeFramesDisplayMode = 'visible' | 'cropped' | 'hidden'
export interface TimeFrameOptions {
start?: moment.Moment
end?: moment.Moment
working?: boolean
magnet?: boolean
default?: boolean
color?: string
classes?: string[]
internal?: boolean
}
/**
* 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
*/
export class TimeFrame implements TimeFrameOptions {
start?: moment.Moment
end?: moment.Moment
working?: boolean
magnet?: boolean
default?: boolean
color?: string
classes?: string[]
internal?: boolean
left: number
width: number
hidden: boolean
originalSize: { left: number; width: number }
cropped: boolean
$element: IAugmentedJQuery
constructor (options: TimeFrameOptions) {
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
}
updateView () {
if (this.$element) {
let 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)
let classes = ['gantt-timeframe' + (this.working ? '' : '-non') + '-working']
if (this.classes) {
classes = classes.concat(this.classes)
}
// tslint:disable:one-variable-per-declaration
for (let i = 0, l = classes.length; i < l; i++) {
this.$element.toggleClass(classes[i], true)
}
}
}
getDuration () {
if (this.end !== undefined && this.start !== undefined) {
return this.end.diff(this.start, 'milliseconds')
}
}
clone () {
return new TimeFrame(this)
}
}
export interface TimeFrameMappingFunction {(date: moment.Moment): string[]}
/**
* 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
*/
export class TimeFrameMapping {
private func: TimeFrameMappingFunction
constructor (func: TimeFrameMappingFunction) {
this.func = func
}
getTimeFrames (date: moment.Moment): string[] {
let ret: string[] = this.func(date)
if (!(ret instanceof Array)) {
ret = [ret]
}
return ret
}
clone () {
return new TimeFrameMapping(this.func)
}
}
interface DateFrameOptions {
evaluator: { (date: moment.Moment): boolean }
date?: MomentInput
start?: moment.Moment
end?: moment.Moment
default?: boolean
targets: any
}
/**
* 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
*/
export class DateFrame {
evaluator: { (date: moment.Moment): boolean }
start: moment.Moment
end: moment.Moment
default: boolean
targets: string[]
constructor (options: DateFrameOptions) {
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
}
dateMatch (date: moment.Moment) {
if (this.evaluator) {
return this.evaluator(date)
} else if (this.start && this.end) {
return date >= this.start && date <= this.end
} else {
return false
}
}
clone () {
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
*/
export class GanttCalendar {
static $filter: IFilterService
timeFrames: {[name: string]: TimeFrame} = {}
timeFrameMappings: {[name: string]: TimeFrameMapping} = {}
dateFrames: {[name: string]: DateFrame} = {}
/**
* Remove all objects.
*/
clear () {
this.timeFrames = {}
this.timeFrameMappings = {}
this.dateFrames = {}
}
/**
* Register TimeFrame objects.
*
* @param {object} timeFrames with names of timeFrames for keys and TimeFrame objects for values.
*/
registerTimeFrames (timeFrames: {[name: string]: TimeFrameOptions}) {
for (let name in timeFrames) {
let timeFrame = timeFrames[name]
this.timeFrames[name] = new TimeFrame(timeFrame)
}
}
/**
* Removes TimeFrame objects.
*
* @param {array} timeFrames names of timeFrames to remove.
*/
removeTimeFrames (timeFrames: string[]) {
for (let name in timeFrames) {
delete this.timeFrames[name]
}
}
/**
* Remove all TimeFrame objects.
*/
clearTimeFrames () {
this.timeFrames = {}
}
/**
* Register TimeFrameMapping objects.
*
* @param {object} mappings object with names of timeFrames mappings for keys and TimeFrameMapping objects for values.
*/
registerTimeFrameMappings (mappings: {(date: moment.Moment): TimeFrameMappingFunction}) {
for (let name in mappings) {
let timeFrameMapping = mappings[name]
this.timeFrameMappings[name] = new TimeFrameMapping(timeFrameMapping)
}
}
/**
* Removes TimeFrameMapping objects.
*
* @param {array} mappings names of timeFrame mappings to remove.
*/
removeTimeFrameMappings (mappings: string[]) {
for (let name in mappings) {
delete this.timeFrameMappings[name]
}
}
/**
* Removes all TimeFrameMapping objects.
*/
clearTimeFrameMappings () {
this.timeFrameMappings = {}
}
/**
* Register DateFrame objects.
*
* @param {object} dateFrames object with names of dateFrames for keys and DateFrame objects for values.
*/
registerDateFrames (dateFrames: {[name: string]: DateFrameOptions}) {
for (let name in dateFrames) {
let dateFrame = dateFrames[name]
this.dateFrames[name] = new DateFrame(dateFrame)
}
}
/**
* Remove DateFrame objects.
*
* @param {array} dateFrames names of date frames to remove.
*/
removeDateFrames (dateFrames: string[]) {
for (let name in dateFrames) {
delete this.dateFrames[name]
}
}
/**
* Removes all DateFrame objects.
*/
clearDateFrames () {
this.dateFrames = {}
}
filterDateFrames (inputDateFrames: {[name: string]: DateFrame}, date: moment.Moment): DateFrame[] {
let dateFrames = []
for (let name in inputDateFrames) {
let dateFrame = inputDateFrames[name]
if (dateFrame.dateMatch(date)) {
dateFrames.push(dateFrame)
}
}
if (dateFrames.length === 0) {
for (let name in inputDateFrames) {
let dateFrame = inputDateFrames[name]
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.
*/
getTimeFrames (date: moment.Moment): TimeFrame[] {
let timeFrames: TimeFrame[] = []
let dateFrames = this.filterDateFrames(this.dateFrames, date)
for (let dateFrame of dateFrames) {
if (dateFrame !== undefined) {
let targets = dateFrame.targets
for (let target of targets) {
let timeFrameMapping = this.timeFrameMappings[target]
if (timeFrameMapping !== undefined) {
// If a timeFrame mapping is found
let names = timeFrameMapping.getTimeFrames(date)
for (let name of names) {
let timeFrame = this.timeFrames[name]
timeFrames.push(timeFrame)
}
} else {
// If no timeFrame mapping is found, try using direct timeFrame
let timeFrame = this.timeFrames[target]
if (timeFrame !== undefined) {
timeFrames.push(timeFrame)
}
}
}
}
}
let dateYear = date.year()
let dateMonth = date.month()
let dateDate = date.date()
let validatedTimeFrames = []
if (timeFrames.length === 0) {
for (let name in this.timeFrames) {
let timeFrame = this.timeFrames[name]
if (timeFrame.default) {
timeFrames.push(timeFrame)
}
}
}
for (let name in timeFrames) {
let timeFrame = timeFrames[name]
let cTimeFrame = timeFrame.clone()
if (cTimeFrame.start !== undefined) {
cTimeFrame.start.year(dateYear)
cTimeFrame.start.month(dateMonth)
cTimeFrame.start.date(dateDate)
}
if (cTimeFrame.end !== undefined) {
cTimeFrame.end.year(dateYear)
cTimeFrame.end.month(dateMonth)
cTimeFrame.end.date(dateDate)
if (moment(cTimeFrame.end).startOf('day') === cTimeFrame.end) {
cTimeFrame.end.add(1, 'day')
}
}
validatedTimeFrames.push(cTimeFrame)
}
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
*/
solve (timeFrames: TimeFrame[], startDate, endDate) {
let color: string
let classes: string[]
let minDate: moment.Moment
let maxDate: moment.Moment
for (let timeFrame of timeFrames) {
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
}
let solvedTimeFrames: TimeFrame[] = [new TimeFrame({start: startDate, end: endDate, internal: true})]
timeFrames = GanttCalendar.$filter('filter')(timeFrames, (timeFrame) => {
return (timeFrame.start === undefined || timeFrame.start < endDate) && (timeFrame.end === undefined || timeFrame.end > startDate)
})
for (let timeFrame of timeFrames) {
if (!timeFrame.start) {
timeFrame.start = startDate
}
if (!timeFrame.end) {
timeFrame.end = endDate
}
}
let orderedTimeFrames = GanttCalendar.$filter('orderBy')(timeFrames, (timeFrame) => {
return -timeFrame.getDuration()
})
let k
for (let oTimeFrame of orderedTimeFrames) {
let tmpSolvedTimeFrames = solvedTimeFrames.slice()
k = 0
let dispatched = false
let treated = false
for (let sTimeFrame of solvedTimeFrames) {
if (!treated) {
if (!oTimeFrame.end && !oTimeFrame.start) {
// timeFrame is infinite.
tmpSolvedTimeFrames.splice(k, 0, oTimeFrame)
treated = true
dispatched = false
} else if (oTimeFrame.end > sTimeFrame.start && oTimeFrame.start < sTimeFrame.end) {
// timeFrame is included in this solvedTimeFrame.
// solvedTimeFrame:|ssssssssssssssssssssssssssssssssss|
// timeFrame: |tttttt|
// result:|sssssssss|tttttt|sssssssssssssssss|
let newSolvedTimeFrame = sTimeFrame.clone()
sTimeFrame.end = moment(oTimeFrame.start)
newSolvedTimeFrame.start = moment(oTimeFrame.end)
tmpSolvedTimeFrames.splice(k + 1, 0, oTimeFrame.clone(), newSolvedTimeFrame)
treated = true
dispatched = false
} else if (!dispatched && oTimeFrame.start < sTimeFrame.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|
sTimeFrame.end = moment(oTimeFrame.start)
tmpSolvedTimeFrames.splice(k + 1, 0, oTimeFrame.clone())
dispatched = true
} else if (dispatched && oTimeFrame.end > sTimeFrame.start) {
// timeFrame is dispatched on two solvedTimeFrame.
// Second part
sTimeFrame.start = moment(oTimeFrame.end)
dispatched = false
treated = true
}
k++
}
}
solvedTimeFrames = tmpSolvedTimeFrames
}
solvedTimeFrames = GanttCalendar.$filter('filter')(solvedTimeFrames, (timeFrame) => {
return !timeFrame.internal &&
(timeFrame.start === undefined || timeFrame.start < endDate) &&
(timeFrame.end === undefined || timeFrame.end > startDate)
})
return solvedTimeFrames
}
}
export default function ($filter: IFilterService) {
'ngInject'
GanttCalendar.$filter = $filter
return GanttCalendar
}