angular-gantt
Version:
Gantt chart component for AngularJS
503 lines (431 loc) • 15.9 kB
text/typescript
import angular from 'angular'
import moment from 'moment'
export default function ($scope, $timeout, $log, ganttUtils, GanttObjectModel, DemoService, ganttMouseOffset, ganttDebounce) {
'ngInject'
let objectModel
let dataToRemove
// Event handler
let logScrollEvent = function (left, date, direction) {
if (date !== undefined) {
$log.info('[Event] api.on.scroll: ' + left + ', ' + (date === undefined ? 'undefined' : date.format()) + ', ' + direction)
}
}
// Event handler
let logDataEvent = function (eventName) {
$log.info('[Event] ' + eventName)
}
// Event handler
let logTaskEvent = function (eventName, task) {
$log.info('[Event] ' + eventName + ': ' + task.model.name)
}
// Event handler
let logRowEvent = function (eventName, row) {
$log.info('[Event] ' + eventName + ': ' + row.model.name)
}
// Event handler
let logTimespanEvent = function (eventName, timespan) {
$log.info('[Event] ' + eventName + ': ' + timespan.model.name)
}
// Event handler
let logLabelsEvent = function (eventName, width) {
$log.info('[Event] ' + eventName + ': ' + width)
}
// Event handler
let logColumnsGenerateEvent = function (columns, headers) {
$log.info('[Event] ' + 'columns.on.generate' + ': ' + columns.length + ' column(s), ' + headers.length + ' header(s)')
}
// Event handler
let logRowsFilterEvent = function (rows, filteredRows) {
$log.info('[Event] rows.on.filter: ' + filteredRows.length + '/' + rows.length + ' rows displayed.')
}
// Event handler
let logTasksFilterEvent = function (tasks, filteredTasks) {
$log.info('[Event] tasks.on.filter: ' + filteredTasks.length + '/' + tasks.length + ' tasks displayed.')
}
// Event handler
let logReadyEvent = function () {
$log.info('[Event] core.on.ready')
}
// Event utility function
let addEventName = function (eventName, func) {
return function (data) {
return func(eventName, data)
}
}
// angular-gantt options
$scope.options = {
mode: 'custom',
scale: 'day',
sortMode: undefined,
sideMode: 'TreeTable',
daily: false,
maxHeight: false,
width: false,
zoom: 1,
columns: ['model.name', 'from', 'to'],
treeTableColumns: ['from', 'to'],
columnsHeaders: {'model.name': 'Name', 'from': 'From', 'to': 'To'},
columnsClasses: {'model.name': 'gantt-column-name', 'from': 'gantt-column-from', 'to': 'gantt-column-to'},
columnsFormatters: {
'from': function (from) {
return from !== undefined ? from.format('lll') : undefined
},
'to': function (to) {
return to !== undefined ? to.format('lll') : undefined
}
},
treeHeaderContent: '<i class="fa fa-align-justify"></i> {{getHeader()}}',
columnsHeaderContents: {
'model.name': '<i class="fa fa-align-justify"></i> {{getHeader()}}',
'from': '<i class="fa fa-calendar"></i> {{getHeader()}}',
'to': '<i class="fa fa-calendar"></i> {{getHeader()}}'
},
autoExpand: 'none',
taskOutOfRange: 'truncate',
fromDate: moment(null),
toDate: undefined,
rowContent: '<i class="fa fa-align-justify"></i> {{row.model.name}}',
taskContent: '<i class="fa fa-tasks"></i> {{task.model.name}}',
allowSideResizing: true,
labelsEnabled: true,
currentDate: 'line',
currentDateValue: new Date(2013, 9, 23, 11, 20, 0),
draw: false,
readOnly: false,
groupDisplayMode: 'group',
filterTask: '',
filterRow: '',
timeFrames: {
'day': {
start: moment('8:00', 'HH:mm'),
end: moment('20:00', 'HH:mm'),
color: '#ACFFA3',
working: true,
default: true
},
'noon': {
start: moment('12:00', 'HH:mm'),
end: moment('13:30', 'HH:mm'),
working: false,
default: true
},
'closed': {
working: false,
default: true
},
'weekend': {
working: false
},
'holiday': {
working: false,
color: 'red',
classes: ['gantt-timeframe-holiday']
}
},
dateFrames: {
'weekend': {
evaluator: function (date) {
return date.isoWeekday() === 6 || date.isoWeekday() === 7
},
targets: ['weekend']
},
'11-november': {
evaluator: function (date) {
return date.month() === 10 && date.date() === 11
},
targets: ['holiday']
}
},
timeFramesWorkingMode: 'hidden',
timeFramesNonWorkingMode: 'visible',
columnMagnet: '15 minutes',
timeFramesMagnet: true,
dependencies: {
enabled: true,
conflictChecker: true
},
movable: {
allowRowSwitching: function (task, targetRow) {
return task.row.model.name !== 'Milestones' && targetRow.model.name !== 'Milestones'
}
},
corner: {
headersLabels: function (key) {
return key.charAt(0).toUpperCase() + key.slice(1)
},
headersLabelsTemplates: '{{getLabel(header)}} <i class="fa fa-calendar"></i>'
},
targetDataAddRowIndex: undefined,
canDraw: function (event) {
let isLeftMouseButton = event.button === 0 || event.button === 1
return $scope.options.draw && !$scope.options.readOnly && isLeftMouseButton
},
drawTaskFactory: function () {
return {
id: ganttUtils.randomUuid(), // Unique id of the task.
name: 'Drawn task', // Name shown on top of each task.
color: '#AA8833' // Color of the task in HEX format (Optional).
}
},
api: function (api) {
// API Object is used to control methods and events from angular-gantt.
$scope.api = api
api.core.on.ready($scope, function () {
// Log various events to console
api.scroll.on.scroll($scope, logScrollEvent)
api.core.on.ready($scope, logReadyEvent)
api.data.on.remove($scope, addEventName('data.on.remove', logDataEvent))
api.data.on.load($scope, addEventName('data.on.load', logDataEvent))
api.data.on.clear($scope, addEventName('data.on.clear', logDataEvent))
api.data.on.change($scope, addEventName('data.on.change', logDataEvent))
api.tasks.on.add($scope, addEventName('tasks.on.add', logTaskEvent))
api.tasks.on.change($scope, addEventName('tasks.on.change', logTaskEvent))
api.tasks.on.rowChange($scope, addEventName('tasks.on.rowChange', logTaskEvent))
api.tasks.on.remove($scope, addEventName('tasks.on.remove', logTaskEvent))
if (api.tasks.on.moveBegin) {
api.tasks.on.moveBegin($scope, addEventName('tasks.on.moveBegin', logTaskEvent))
// api.tasks.on.move($scope, addEventName('tasks.on.move', logTaskEvent));
api.tasks.on.moveEnd($scope, addEventName('tasks.on.moveEnd', logTaskEvent))
api.tasks.on.resizeBegin($scope, addEventName('tasks.on.resizeBegin', logTaskEvent))
// api.tasks.on.resize($scope, addEventName('tasks.on.resize', logTaskEvent));
api.tasks.on.resizeEnd($scope, addEventName('tasks.on.resizeEnd', logTaskEvent))
}
if (api.tasks.on.drawBegin) {
api.tasks.on.drawBegin($scope, addEventName('tasks.on.drawBegin', logTaskEvent))
// api.tasks.on.draw($scope, addEventName('tasks.on.draw', logTaskEvent));
api.tasks.on.drawEnd($scope, addEventName('tasks.on.drawEnd', logTaskEvent))
}
api.rows.on.add($scope, addEventName('rows.on.add', logRowEvent))
api.rows.on.change($scope, addEventName('rows.on.change', logRowEvent))
api.rows.on.move($scope, addEventName('rows.on.move', logRowEvent))
api.rows.on.remove($scope, addEventName('rows.on.remove', logRowEvent))
api.side.on.resizeBegin($scope, addEventName('labels.on.resizeBegin', logLabelsEvent))
// api.side.on.resize($scope, addEventName('labels.on.resize', logLabelsEvent));
api.side.on.resizeEnd($scope, addEventName('labels.on.resizeEnd', logLabelsEvent))
api.timespans.on.add($scope, addEventName('timespans.on.add', logTimespanEvent))
api.columns.on.generate($scope, logColumnsGenerateEvent)
api.rows.on.filter($scope, logRowsFilterEvent)
api.tasks.on.filter($scope, logTasksFilterEvent)
api.data.on.change($scope, function (newData) {
if (dataToRemove === undefined) {
dataToRemove = [
{'id': newData[2].id}, // Remove Kickoff row
{
'id': newData[0].id, 'tasks': [
{'id': newData[0].tasks[0].id},
{'id': newData[0].tasks[3].id}
]
}, // Remove some Milestones
{
'id': newData[7].id, 'tasks': [
{'id': newData[7].tasks[0].id}
]
} // Remove order basket from Sprint 2
]
}
})
// When gantt is ready, load data.
// `data` attribute could have been used too.
$scope.load()
// Add some DOM events
api.directives.on.new($scope, function (directiveName, directiveScope, element) {
if (directiveName === 'ganttTask') {
element.bind('click', function (event) {
event.stopPropagation()
logTaskEvent('task-click', directiveScope.task)
})
element.bind('mousedown touchstart', function (event) {
event.stopPropagation()
$scope.live.row = directiveScope.task.row.model
if (directiveScope.task.originalModel !== undefined) {
$scope.live.task = directiveScope.task.originalModel
} else {
$scope.live.task = directiveScope.task.model
}
$scope.$digest()
})
} else if (directiveName === 'ganttRow') {
element.bind('click', function (event) {
event.stopPropagation()
logRowEvent('row-click', directiveScope.row)
})
element.bind('mousedown touchstart', function (event) {
event.stopPropagation()
$scope.live.row = directiveScope.row.model
$scope.$digest()
})
} else if (directiveName === 'ganttRowLabel') {
element.bind('click', function () {
logRowEvent('row-label-click', directiveScope.row)
})
element.bind('mousedown touchstart', function () {
$scope.live.row = directiveScope.row.model
$scope.$digest()
})
}
})
api.tasks.on.rowChange($scope, function (task) {
$scope.live.row = task.row.model
})
objectModel = new GanttObjectModel(api)
})
}
}
$scope.handleTaskIconClick = function (taskModel) {
alert('Icon from ' + taskModel.name + ' task has been clicked.')
}
$scope.handleRowIconClick = function (rowModel) {
alert('Icon from ' + rowModel.name + ' row has been clicked.')
}
$scope.expandAll = function () {
$scope.api.tree.expandAll()
}
$scope.collapseAll = function () {
$scope.api.tree.collapseAll()
}
$scope.$watch('options.sideMode', function (newValue, oldValue) {
if (newValue !== oldValue) {
$scope.api.side.setWidth(undefined)
$timeout(function () {
$scope.api.columns.refresh()
})
}
})
$scope.canAutoWidth = function (scale) {
if (scale.match(/.*?hour.*?/) || scale.match(/.*?minute.*?/)) {
return false
}
return true
}
$scope.getColumnWidth = function (widthEnabled, scale, zoom) {
if (!widthEnabled && $scope.canAutoWidth(scale)) {
return undefined
}
if (scale.match(/.*?week.*?/)) {
return 150 * zoom
}
if (scale.match(/.*?month.*?/)) {
return 300 * zoom
}
if (scale.match(/.*?quarter.*?/)) {
return 500 * zoom
}
if (scale.match(/.*?year.*?/)) {
return 800 * zoom
}
return 40 * zoom
}
// Reload data action
$scope.load = function () {
$scope.data = DemoService.getSampleData()
dataToRemove = undefined
$scope.timespans = DemoService.getSampleTimespans()
}
$scope.reload = function () {
$scope.load()
}
// Remove data action
$scope.remove = function () {
$scope.api.data.remove(dataToRemove)
$scope.api.dependencies.refresh()
}
// Clear data action
$scope.clear = function () {
$scope.data = []
}
// Add data to target row index
$scope.addOverlapTaskToTargetRowIndex = function () {
let targetDataAddRowIndex = parseInt($scope.options.targetDataAddRowIndex, 10)
if (targetDataAddRowIndex) {
let targetRow = $scope.data[$scope.options.targetDataAddRowIndex]
if (targetRow && targetRow.tasks && targetRow.tasks.length > 0) {
let firstTaskInRow = targetRow.tasks[0]
let copiedColor = firstTaskInRow.color
let firstTaskEndDate = firstTaskInRow.to.toDate()
let overlappingFromDate = new Date(firstTaskEndDate)
overlappingFromDate.setDate(overlappingFromDate.getDate() - 1)
let overlappingToDate = new Date(overlappingFromDate)
overlappingToDate.setDate(overlappingToDate.getDate() + 7)
targetRow.tasks.push({
'name': 'Overlapping',
'from': overlappingFromDate,
'to': overlappingToDate,
'color': copiedColor
})
}
}
}
// Visual two way binding.
$scope.live = {}
let debounceValue = 1000
let listenTaskJson = ganttDebounce(function (taskJson) {
if (taskJson !== undefined) {
let task = angular.fromJson(taskJson)
objectModel.cleanTask(task)
let model = $scope.live.task
angular.merge(model, task)
}
}, debounceValue)
$scope.$watch('live.taskJson', listenTaskJson)
let listenRowJson = ganttDebounce(function (rowJson) {
if (rowJson !== undefined) {
let row = angular.fromJson(rowJson)
objectModel.cleanRow(row)
let tasks = row.tasks
delete row.tasks
delete row.drawTask
let rowModel = $scope.live.row
angular.merge(rowModel, row)
let newTasks = {}
let i
let l
if (tasks !== undefined) {
for (i = 0, l = tasks.length; i < l; i++) {
objectModel.cleanTask(tasks[i])
}
for (i = 0, l = tasks.length; i < l; i++) {
newTasks[tasks[i].id] = tasks[i]
}
if (rowModel.tasks === undefined) {
rowModel.tasks = []
}
for (i = rowModel.tasks.length - 1; i >= 0; i--) {
let existingTask = rowModel.tasks[i]
let newTask = newTasks[existingTask.id]
if (newTask === undefined) {
rowModel.tasks.splice(i, 1)
} else {
objectModel.cleanTask(newTask)
angular.merge(existingTask, newTask)
delete newTasks[existingTask.id]
}
}
} else {
delete rowModel.tasks
}
angular.forEach(newTasks, function (newTask) {
rowModel.tasks.push(newTask)
})
}
}, debounceValue)
$scope.$watch('live.rowJson', listenRowJson)
$scope.$watchCollection('live.task', function (task) {
$timeout(function () {
$scope.live.taskJson = angular.toJson(task, true)
$scope.live.rowJson = angular.toJson($scope.live.row, true)
})
})
$scope.$watchCollection('live.row', function (row) {
$timeout(function () {
$scope.live.rowJson = angular.toJson(row, true)
if (row !== undefined && row.tasks !== undefined && row.tasks.indexOf($scope.live.task) < 0) {
$scope.live.task = row.tasks[0]
}
})
})
$scope.$watchCollection('live.row.tasks', function () {
$timeout(function () {
$scope.live.rowJson = angular.toJson($scope.live.row, true)
})
})
}