angular-gantt
Version:
Gantt chart component for AngularJS
491 lines (411 loc) • 16.1 kB
text/typescript
import angular, {IAugmentedJQuery, IDocumentService, IScope, ITimeoutService} from 'angular'
import moment from 'moment'
import {GanttApi} from './api/api.factory'
import {GanttOptions} from './api/options.factory'
import {GanttCalendar} from './calendar/calendar.factory'
import {GanttCurrentDateManager} from './calendar/currentDateManager.factory'
import {GanttObjectModel} from './model/objectModel.factory'
import {GanttRowsManager} from './row/rowsManager.factory'
import {GanttColumnsManager} from './column/columnsManager.factory'
import {GanttTimespansManager} from './timespan/timespansManager.factory'
import GanttArrays from './util/arrays.service'
import {GanttScroll} from './template/scroll.factory'
import {GanttBody} from './template/body.factory'
import {GanttHeader} from './template/header.factory'
import {GanttSide} from './template/side.factory'
import {GanttRowModel} from './row/row.factory'
// Gantt logic. Manages the columns, rows and sorting functionality.
export class Gantt {
static $document: IDocumentService
static ganttArrays: GanttArrays
static $timeout: ITimeoutService
options: GanttOptions
$scope: IScope
$element: IAugmentedJQuery
api: GanttApi
calendar: GanttCalendar
objectModel: GanttObjectModel
columnMagnetValue: number
columnMagnetUnit: string
shiftColumnMagnetValue: number
shiftColumnMagnetUnit: string
shiftKey: boolean
scroll: GanttScroll
body: GanttBody
header: GanttHeader
side: GanttSide
rowsManager: GanttRowsManager
columnsManager: GanttColumnsManager
timespansManager: GanttTimespansManager
currentDateManager: GanttCurrentDateManager
originalWidth: number
width: number
rendered = false
isRefreshingColumns = false
constructor ($scope: IScope, $element: IAugmentedJQuery) {
this.$scope = $scope
this.$element = $element
this.options = new GanttOptions($scope, {
// tslint:disable:no-empty
'api': function () {},
'data': [],
'timespans': [],
'viewScale': 'day',
'columnMagnet': '15 minutes',
'timeFramesMagnet': true,
'showSide': true,
'allowSideResizing': true,
'currentDate': 'line',
'currentDateValue': moment,
'autoExpand': 'none',
'taskOutOfRange': 'truncate',
'taskContent': '{{task.model.name}}',
'rowContent': '{{row.model.name}}',
'maxHeight': 0,
'timeFrames': [],
'dateFrames': [],
'timeFramesWorkingMode': 'hidden',
'timeFramesNonWorkingMode': 'visible',
'taskLimitThreshold': 100,
'columnLimitThreshold': 500
})
this.api = new GanttApi(this)
this.api.registerEvent('core', 'ready')
this.api.registerEvent('core', 'rendered')
this.api.registerEvent('directives', 'controller')
this.api.registerEvent('directives', 'preLink')
this.api.registerEvent('directives', 'postLink')
this.api.registerEvent('directives', 'new')
this.api.registerEvent('directives', 'destroy')
this.api.registerEvent('data', 'change')
this.api.registerEvent('data', 'load')
this.api.registerEvent('data', 'remove')
this.api.registerEvent('data', 'clear')
this.api.registerMethod('core', 'getDateByPosition', this.getDateByPosition, this)
this.api.registerMethod('core', 'getPositionByDate', this.getPositionByDate, this)
this.api.registerMethod('data', 'load', this.loadData, this)
this.api.registerMethod('data', 'remove', this.removeData, this)
this.api.registerMethod('data', 'clear', this.clearData, this)
this.api.registerMethod('data', 'get', this.getData, this)
this.calendar = new GanttCalendar()
this.calendar.registerTimeFrames(this.options.value('timeFrames'))
this.calendar.registerDateFrames(this.options.value('dateFrames'))
this.api.registerMethod('timeframes', 'registerTimeFrames', this.calendar.registerTimeFrames, this.calendar)
this.api.registerMethod('timeframes', 'clearTimeframes', this.calendar.clearTimeFrames, this.calendar)
this.api.registerMethod('timeframes', 'registerDateFrames', this.calendar.registerDateFrames, this.calendar)
this.api.registerMethod('timeframes', 'clearDateFrames', this.calendar.clearDateFrames, this.calendar)
this.api.registerMethod('timeframes', 'registerTimeFrameMappings', this.calendar.registerTimeFrameMappings, this.calendar)
this.api.registerMethod('timeframes', 'clearTimeFrameMappings', this.calendar.clearTimeFrameMappings, this.calendar)
$scope.$watchGroup(['timeFrames', 'dateFrames'], (newValues, oldValues) => {
if (newValues !== oldValues) {
let timeFrames = newValues[0]
let dateFrames = newValues[1]
let oldTimeFrames = oldValues[0]
let oldDateFrames = oldValues[1]
let framesChanged = false
if (!angular.equals(timeFrames, oldTimeFrames)) {
this.calendar.clearTimeFrames()
this.calendar.registerTimeFrames(timeFrames)
framesChanged = true
}
if (!angular.equals(dateFrames, oldDateFrames)) {
this.calendar.clearDateFrames()
this.calendar.registerDateFrames(dateFrames)
framesChanged = true
}
if (framesChanged) {
this.columnsManager.generateColumns()
}
}
})
$scope.$watch('columnMagnet', () => {
let splittedColumnMagnet
let columnMagnet = this.options.value('columnMagnet')
if (columnMagnet) {
splittedColumnMagnet = columnMagnet.trim().split(' ')
}
if (splittedColumnMagnet && splittedColumnMagnet.length > 1) {
this.columnMagnetValue = parseFloat(splittedColumnMagnet[0])
this.columnMagnetUnit = moment.normalizeUnits(splittedColumnMagnet[splittedColumnMagnet.length - 1])
} else {
this.columnMagnetValue = 1
this.columnMagnetUnit = moment.normalizeUnits(columnMagnet)
}
})
$scope.$watchGroup(['shiftColumnMagnet', 'viewScale'], () => {
let splittedColumnMagnet
let shiftColumnMagnet = this.options.value('shiftColumnMagnet')
if (shiftColumnMagnet) {
splittedColumnMagnet = shiftColumnMagnet.trim().split(' ')
}
if (splittedColumnMagnet !== undefined && splittedColumnMagnet.length > 1) {
this.shiftColumnMagnetValue = parseFloat(splittedColumnMagnet[0])
this.shiftColumnMagnetUnit = moment.normalizeUnits(splittedColumnMagnet[splittedColumnMagnet.length - 1])
} else {
this.shiftColumnMagnetValue = 1
this.shiftColumnMagnetUnit = moment.normalizeUnits(shiftColumnMagnet)
}
})
Gantt.$document.on('keyup keydown', this.keyHandler)
$scope.$on('$destroy', () => {
Gantt.$document.off('keyup keydown', this.keyHandler)
})
this.scroll = new GanttScroll(this)
this.body = new GanttBody(this)
this.header = new GanttHeader(this)
this.side = new GanttSide(this)
this.objectModel = new GanttObjectModel(this.api)
this.rowsManager = new GanttRowsManager(this)
this.columnsManager = new GanttColumnsManager(this)
this.timespansManager = new GanttTimespansManager(this)
this.currentDateManager = new GanttCurrentDateManager(this)
this.originalWidth = 0
this.width = 0
if (typeof this.$scope.api === 'function') {
this.$scope.api(this.api)
}
let hasRowModelOrderChanged = (data1, data2) => {
if (data2 === undefined || data1.length !== data2.length) {
return true
}
// tslint:disable:one-variable-per-declaration
for (let i = 0, l = data1.length; i < l; i++) {
if (data1[i].id !== data2[i].id) {
return true
}
}
return false
}
$scope.$watchCollection('data', (newData: GanttRowModel[], oldData: GanttRowModel[]) => {
if (oldData !== undefined) {
let toRemoveIds = Gantt.ganttArrays.getRemovedIds(newData, oldData)
if (toRemoveIds.length === oldData.length) {
this.rowsManager.removeAll();
// DEPRECATED
(this.api as any).data.raise.clear()
} else {
for (let i = 0, l = toRemoveIds.length; i < l; i++) {
let toRemoveId = toRemoveIds[i]
this.rowsManager.removeRow(toRemoveId)
}
// DEPRECATED
let removedRows = []
for (let i = 0, l = oldData.length; i < l; i++) {
if (toRemoveIds.indexOf(oldData[i].id) > -1) {
removedRows.push(oldData[i])
}
}
(this.api as any).data.raise.remove(removedRows)
}
}
if (newData !== undefined) {
let modelOrderChanged = hasRowModelOrderChanged(newData, oldData)
if (modelOrderChanged) {
this.rowsManager.resetNonModelLists()
}
for (let j = 0, k = newData.length; j < k; j++) {
let rowData = newData[j]
this.rowsManager.addRow(rowData, modelOrderChanged)
}
(this.api as any).data.raise.change(newData, oldData);
// DEPRECATED
(this.api as any).data.raise.load(newData)
}
})
}
private keyHandler (e) {
this.shiftKey = e.shiftKey
return true
}
/**
* Get the magnet value and unit considering the current gantt state.
*
* @returns {[*,*]}
*/
getMagnetValueAndUnit () {
if (this.shiftKey) {
if (this.shiftColumnMagnetValue !== undefined && this.shiftColumnMagnetUnit !== undefined) {
return [this.shiftColumnMagnetValue, this.shiftColumnMagnetUnit]
} else {
let viewScale = this.options.value('viewScale')
viewScale = viewScale.trim()
let viewScaleValue
let viewScaleUnit
let splittedViewScale
if (viewScale) {
splittedViewScale = viewScale.split(' ')
}
if (splittedViewScale && splittedViewScale.length > 1) {
viewScaleValue = parseFloat(splittedViewScale[0])
viewScaleUnit = moment.normalizeUnits(splittedViewScale[splittedViewScale.length - 1])
} else {
viewScaleValue = 1
viewScaleUnit = moment.normalizeUnits(viewScale)
}
return [viewScaleValue * 0.25, viewScaleUnit]
}
} else {
return [this.columnMagnetValue, this.columnMagnetUnit]
}
}
// Get the date transformed by magnet feature.
getMagnetDate (date, disableExpand) {
if (date === undefined) {
return undefined
}
if (!moment.isMoment(moment)) {
date = moment(date)
}
let column = this.columnsManager.getColumnByDate(date, disableExpand)
let magnetValueAndUnit = this.getMagnetValueAndUnit()
let magnetValue = magnetValueAndUnit[0]
let magnetUnit = magnetValueAndUnit[1]
return column.getMagnetDate(date, magnetValue, magnetUnit, this.options.value('timeFramesMagnet'))
}
// Returns the exact column date at the given position x (in em)
getDateByPosition (x: number, magnet?: boolean, disableExpand?: boolean) {
let column = this.columnsManager.getColumnByPosition(x, disableExpand)
if (column !== undefined) {
let magnetValue
let magnetUnit
if (magnet) {
let magnetValueAndUnit = this.getMagnetValueAndUnit()
magnetValue = magnetValueAndUnit[0]
magnetUnit = magnetValueAndUnit[1]
}
return column.getDateByPosition(x - column.left, magnetValue, magnetUnit, this.options.value('timeFramesMagnet'))
} else {
return undefined
}
}
getBodyAvailableWidth () {
let scrollWidth = this.getWidth() - this.side.getWidth()
let borderWidth = this.scroll.getBordersWidth()
let availableWidth = scrollWidth - (borderWidth !== undefined ? this.scroll.getBordersWidth() : 0)
// Remove 1 pixel because of rounding issue in some cases.
availableWidth = availableWidth - 1
return availableWidth
}
// Returns the position inside the Gantt calculated by the given date
getPositionByDate (date, disableExpand?: boolean) {
if (date === undefined) {
return undefined
}
if (!moment.isMoment(moment)) {
date = moment(date)
}
let column = this.columnsManager.getColumnByDate(date, disableExpand)
if (column !== undefined) {
return column.getPositionByDate(date)
} else {
return undefined
}
}
// DEPRECATED - Use $data instead.
loadData (data: GanttRowModel | GanttRowModel[]) {
if (!Array.isArray(data)) {
data = data !== undefined ? [data] : []
}
if (this.$scope.data === undefined) {
this.$scope.data = data
} else {
for (let i = 0, l = data.length; i < l; i++) {
let row = data[i]
let j = Gantt.ganttArrays.indexOfId(this.$scope.data, row.id)
if (j > -1) {
this.$scope.data[j] = row
} else {
this.$scope.data.push(row)
}
}
}
let w = this.side.getWidth()
if (w > 0) {
this.options.set('sideWidth', w)
}
}
getData () {
return this.$scope.data
}
// DEPRECATED - Use $data instead.
removeData (data) {
if (!Array.isArray(data)) {
data = data !== undefined ? [data] : []
}
if (this.$scope.data !== undefined) {
for (let i = 0, l = data.length; i < l; i++) {
let rowToRemove = data[i]
let j = Gantt.ganttArrays.indexOfId(this.$scope.data, rowToRemove.id)
if (j > -1) {
if (rowToRemove.tasks === undefined || rowToRemove.tasks.length === 0) {
// Remove complete row
this.$scope.data.splice(j, 1)
} else {
// Remove single tasks
let row = this.$scope.data[j]
for (let ti = 0, tl = rowToRemove.tasks.length; ti < tl; ti++) {
let taskToRemove = rowToRemove.tasks[ti]
let tj = Gantt.ganttArrays.indexOfId(row.tasks, taskToRemove.id)
if (tj > -1) {
row.tasks.splice(tj, 1)
}
}
}
}
}
}
}
// DEPRECATED - Use $data instead.
clearData () {
this.$scope.data = undefined
}
getWidth () {
return this.$scope.ganttElementWidth
}
getHeight () {
return this.$scope.ganttElementHeight
}
getContainerWidth () {
return this.$scope.ganttContainerWidth
}
getContainerHeight () {
return this.$scope.ganttContainerHeight
}
initialized () {
// Gantt is initialized. Signal that the Gantt is ready.
(this.api as any).core.raise.ready(this.api)
this.rendered = true
this.columnsManager.generateColumns()
Gantt.$timeout(() => {
let w = this.side.getWidth()
if (w > 0) {
this.options.set('sideWidth', w)
}
(this.api as any).core.raise.rendered(this.api)
})
}
}
export default function (GanttApi: { new(gantt: Gantt): GanttApi },
GanttOptions: {
new(values: { [option: string]: any; },
defaultValues: { [option: string]: any; }): GanttOptions
},
GanttCalendar: { new(): GanttCalendar },
GanttScroll: { new(gantt: Gantt): GanttScroll },
GanttBody: { new(gantt: Gantt): GanttBody },
GanttHeader: { new(gantt: Gantt): GanttHeader },
GanttSide: { new(gantt: Gantt): GanttSide },
GanttObjectModel: { new(gantt: Gantt): GanttObjectModel },
GanttRowsManager: { new(gantt: Gantt): GanttRowsManager },
GanttColumnsManager: { new(gantt: Gantt): GanttColumnsManager },
GanttTimespansManager: { new(gantt: Gantt): GanttTimespansManager },
GanttCurrentDateManager: { new(gantt: Gantt): GanttCurrentDateManager },
ganttArrays: GanttArrays,
$document: IDocumentService,
$timeout: ITimeoutService) {
'ngInject'
Gantt.ganttArrays = ganttArrays
Gantt.$document = $document
Gantt.$timeout = $timeout
return Gantt
}