angular-gantt
Version:
Gantt chart component for AngularJS
551 lines (461 loc) • 18.1 kB
text/typescript
import moment from 'moment'
import GanttBinarySearch from '../util/binarySearch.service'
import GanttColumnGenerator from './columnGenerator.service'
import {GanttColumnBuilder} from './columnBuilder.factory'
import {GanttColumn} from './column.factory'
import {GanttColumnHeader} from './columnHeader.factory'
import GanttHeadersGenerator from './headersGenerator.service'
import GanttLayout from '../../ui/util/layout.service'
import {Gantt} from '../gantt.factory'
import {IGanttFilterService} from '../../../index'
export class GanttColumnsManager {
static GanttColumnGenerator: GanttColumnGenerator
static GanttHeadersGenerator: GanttHeadersGenerator
static GanttColumnBuilder: { new(columnsManager: GanttColumnsManager): GanttColumnBuilder }
static ganttBinarySearch: GanttBinarySearch
static ganttLayout: GanttLayout
static $filter: IGanttFilterService
gantt: Gantt
from: moment.Moment
to: moment.Moment
columns: GanttColumn[]
visibleColumns: GanttColumn[]
previousColumns: GanttColumn[]
nextColumns: GanttColumn[]
headers: GanttColumnHeader[][]
visibleHeaders: GanttColumnHeader[][]
columnBuilder: GanttColumnBuilder
scrollAnchor: moment.Moment
defaultHeadersFormats = {
year: 'YYYY',
quarter: '[Q]Q YYYY',
month: 'MMMM YYYY',
week: 'w',
day: 'D',
hour: 'H',
minute: 'H:mm',
second: 'H:mm:ss',
millisecond: 'H:mm:ss:SSS'
}
defaultDayHeadersFormats = {day: 'LL', hour: 'H', minute: 'H:mm', second: 'H:mm:ss', millisecond: 'H:mm:ss:SSS'}
defaultYearHeadersFormats = {'year': 'YYYY', 'quarter': '[Q]Q', month: 'MMMM'}
constructor (gantt) {
this.gantt = gantt
this.from = undefined
this.to = undefined
this.columns = []
this.visibleColumns = []
this.previousColumns = []
this.nextColumns = []
this.headers = []
this.visibleHeaders = []
this.scrollAnchor = undefined
this.columnBuilder = new GanttColumnsManager.GanttColumnBuilder(this)
// Add a watcher if a view related setting changed from outside of the Gantt. Update the gantt accordingly if so.
// All those changes need a recalculation of the header columns
this.gantt.$scope.$watchGroup(['viewScale', 'columnWidth', 'timeFramesWorkingMode', 'timeFramesNonWorkingMode', 'fromDate', 'toDate', 'autoExpand', 'taskOutOfRange'], (newValues, oldValues) => {
if (newValues !== oldValues && this.gantt.rendered) {
this.generateColumns()
}
})
this.gantt.$scope.$watchCollection('headers', (newValues, oldValues) => {
if (newValues !== oldValues && this.gantt.rendered) {
this.generateColumns()
}
})
this.gantt.$scope.$watchCollection('headersFormats', (newValues, oldValues) => {
if (newValues !== oldValues && this.gantt.rendered) {
this.generateColumns()
}
})
this.gantt.$scope.$watchGroup(['ganttElementWidth', 'showSide', 'sideWidth', 'maxHeight', 'daily'], (newValues, oldValues) => {
if (newValues !== oldValues && this.gantt.rendered) {
this.updateColumnsMeta()
}
});
(this.gantt.api as any).data.on.load(this.gantt.$scope, () => {
if ((this.from === undefined || this.to === undefined ||
this.from > this.gantt.rowsManager.getDefaultFrom() ||
this.to < this.gantt.rowsManager.getDefaultTo()) && this.gantt.rendered) {
this.generateColumns()
}
this.gantt.rowsManager.sortRows()
});
(this.gantt.api as any).data.on.remove(this.gantt.$scope, () => {
this.gantt.rowsManager.sortRows()
})
this.gantt.api.registerMethod('columns', 'clear', this.clearColumns, this)
this.gantt.api.registerMethod('columns', 'generate', this.generateColumns, this)
this.gantt.api.registerMethod('columns', 'refresh', this.updateColumnsMeta, this)
this.gantt.api.registerMethod('columns', 'getColumnsWidth', this.getColumnsWidth, this)
this.gantt.api.registerMethod('columns', 'getColumnsWidthToFit', this.getColumnsWidthToFit, this)
this.gantt.api.registerMethod('columns', 'getDateRange', this.getDateRange, this)
this.gantt.api.registerEvent('columns', 'clear')
this.gantt.api.registerEvent('columns', 'generate')
this.gantt.api.registerEvent('columns', 'refresh')
}
setScrollAnchor () {
if (this.gantt.scroll.$element && this.columns.length > 0) {
let el = this.gantt.scroll.$element[0]
let center = el.scrollLeft + el.offsetWidth / 2
this.scrollAnchor = this.gantt.getDateByPosition(center)
}
}
scrollToScrollAnchor () {
if (this.columns.length > 0 && this.scrollAnchor !== undefined) {
// Ugly but prevents screen flickering (unlike $timeout)
this.gantt.$scope.$$postDigest(() => {
(this.gantt.api as any).scroll.toDate(this.scrollAnchor)
})
}
}
clearColumns () {
this.setScrollAnchor()
this.from = undefined
this.to = undefined
this.columns = []
this.visibleColumns = []
this.previousColumns = []
this.nextColumns = []
this.headers = []
this.visibleHeaders = [];
(this.gantt.api as any).columns.raise.clear()
}
generateColumns (from?: moment.Moment | Date, to?: moment.Moment | Date) {
if (!from) {
from = this.gantt.options.value('fromDate')
}
if (!to) {
to = this.gantt.options.value('toDate')
}
if (!from || (moment.isMoment(from) && !from.isValid())) {
from = this.gantt.rowsManager.getDefaultFrom()
if (!from) {
return false
}
}
if (!to || (moment.isMoment(to) && !to.isValid())) {
to = this.gantt.rowsManager.getDefaultTo()
if (!to) {
return false
}
}
if (from !== undefined && !moment.isMoment(from)) {
from = moment(from)
}
if (to !== undefined && !moment.isMoment(to)) {
to = moment(to)
}
if (this.gantt.options.value('taskOutOfRange') === 'expand') {
from = this.gantt.rowsManager.getExpandedFrom(from as moment.Moment)
to = this.gantt.rowsManager.getExpandedTo(to as moment.Moment)
}
this.setScrollAnchor()
this.from = from as moment.Moment
this.to = to as moment.Moment
this.previousColumns = []
this.nextColumns = []
this.columns = GanttColumnsManager.GanttColumnGenerator.generate(this.columnBuilder, this.from, this.to, this.gantt.options.value('viewScale'), this.getColumnsWidth())
this.headers = GanttColumnsManager.GanttHeadersGenerator.generate(this)
this.updateColumnsMeta()
this.scrollToScrollAnchor();
(this.gantt.api as any).columns.raise.generate(this.columns, this.headers)
}
updateColumnsMeta () {
this.gantt.isRefreshingColumns = true
let lastColumn = this.getLastColumn()
this.gantt.originalWidth = lastColumn !== undefined ? lastColumn.originalSize.left + lastColumn.originalSize.width : 0
let columnsWidthChanged = this.updateColumnsWidths(this.columns, this.headers, this.previousColumns, this.nextColumns)
this.gantt.width = lastColumn !== undefined ? lastColumn.left + lastColumn.width : 0
let showSide = this.gantt.options.value('showSide')
let sideShown = this.gantt.side.isShown()
let sideVisibilityChanged = showSide !== sideShown
if (sideVisibilityChanged && !showSide) {
// Prevent unnecessary v-scrollbar if side is hidden here
this.gantt.side.show(false)
}
this.gantt.rowsManager.updateTasksPosAndSize()
this.gantt.timespansManager.updateTimespansPosAndSize()
this.updateVisibleColumns(columnsWidthChanged)
this.gantt.rowsManager.updateVisibleObjects()
let currentDateValue = this.gantt.options.value('currentDateValue')
this.gantt.currentDateManager.setCurrentDate(currentDateValue)
if (sideVisibilityChanged && showSide) {
// Prevent unnecessary v-scrollbar if side is shown here
this.gantt.side.show(true)
}
this.gantt.isRefreshingColumns = false;
(this.gantt.api as any).columns.raise.refresh(this.columns, this.headers)
}
/**
* Returns the last Gantt column or undefined
* @param extended
* @returns {any}
*/
getLastColumn (extended = false) {
let columns = this.columns
if (extended) {
columns = this.nextColumns
}
if (columns && columns.length > 0) {
return columns[columns.length - 1]
} else {
return undefined
}
}
/**
* Returns the first Gantt column or undefined
*
* @param extended
* @returns {any}
*/
getFirstColumn (extended = false) {
let columns = this.columns
if (extended) {
columns = this.previousColumns
}
if (columns && columns.length > 0) {
return columns[0]
} else {
return undefined
}
}
/**
* Returns the column at the given or next possible date
*
* @param date
* @param disableExpand
* @returns {GanttColumn}
*/
getColumnByDate (date: moment.Moment, disableExpand?: boolean) {
if (!disableExpand) {
this.expandExtendedColumnsForDate(date)
}
let extendedColumns = this.previousColumns.concat(this.columns, this.nextColumns)
let columns = GanttColumnsManager.ganttBinarySearch.get(extendedColumns, date, function (c) {
return c.date
}, true)
return columns[0] === undefined ? columns[1] : columns[0]
}
/**
* Returns the column at the given position x (in em)
*
* @param x
* @param disableExpand
* @returns {GanttColumn}
*/
getColumnByPosition (x: number, disableExpand?: boolean) {
if (!disableExpand) {
this.expandExtendedColumnsForPosition(x)
}
let extendedColumns = this.previousColumns.concat(this.columns, this.nextColumns)
let columns = GanttColumnsManager.ganttBinarySearch.get(extendedColumns, x, function (c) {
return c.left
}, true)
return columns[0] === undefined ? columns[1] : columns[0]
}
updateColumnsWidths (columns, headers, previousColumns, nextColumns) {
let columnWidth: number = this.gantt.options.value('columnWidth')
let expandToFit: boolean = this.gantt.options.value('expandToFit')
let shrinkToFit: boolean = this.gantt.options.value('shrinkToFit')
if (columnWidth === undefined || expandToFit || shrinkToFit) {
let newWidth = this.gantt.getBodyAvailableWidth()
let lastColumn = this.gantt.columnsManager.getLastColumn(false)
if (lastColumn !== undefined) {
let currentWidth = lastColumn.originalSize.left + lastColumn.originalSize.width
if (expandToFit && currentWidth < newWidth ||
shrinkToFit && currentWidth > newWidth ||
columnWidth === undefined
) {
let widthFactor = newWidth / currentWidth
GanttColumnsManager.ganttLayout.setColumnsWidthFactor(columns, widthFactor)
for (let header of headers) {
GanttColumnsManager.ganttLayout.setColumnsWidthFactor(header, widthFactor)
}
// previous and next columns will be generated again on need.
previousColumns.splice(0, this.previousColumns.length)
nextColumns.splice(0, this.nextColumns.length)
return true
}
}
}
return false
}
getColumnsWidth () {
let columnWidth: number = this.gantt.options.value('columnWidth')
if (columnWidth === undefined) {
if (!this.gantt.width || this.gantt.width <= 0) {
columnWidth = 20
} else {
columnWidth = this.gantt.width / this.columns.length
}
}
return columnWidth
}
getColumnsWidthToFit () {
return this.gantt.getBodyAvailableWidth() / this.columns.length
}
expandExtendedColumnsForPosition (x) {
let viewScale
if (x < 0) {
let firstColumn = this.getFirstColumn()
let from = firstColumn.date
let firstExtendedColumn = this.getFirstColumn(true)
if (!firstExtendedColumn || firstExtendedColumn.left > x) {
viewScale = this.gantt.options.value('viewScale')
this.previousColumns = GanttColumnsManager.GanttColumnGenerator.generate(this.columnBuilder, from, undefined, viewScale, this.getColumnsWidth(), -x, 0, true)
}
return true
} else if (x > this.gantt.width) {
let lastColumn = this.getLastColumn()
let endDate = lastColumn.getDateByPosition(lastColumn.width)
let lastExtendedColumn = this.getLastColumn(true)
if (!lastExtendedColumn || lastExtendedColumn.left + lastExtendedColumn.width < x) {
viewScale = this.gantt.options.value('viewScale')
this.nextColumns = GanttColumnsManager.GanttColumnGenerator.generate(this.columnBuilder, endDate, undefined, viewScale, this.getColumnsWidth(), x - this.gantt.width, this.gantt.width, false)
}
return true
}
return false
}
expandExtendedColumnsForDate (date) {
let firstColumn = this.getFirstColumn()
let from
if (firstColumn) {
from = firstColumn.date
}
let lastColumn = this.getLastColumn()
let endDate
if (lastColumn) {
endDate = lastColumn.endDate
}
let viewScale
if (from && date < from) {
let firstExtendedColumn = this.getFirstColumn(true)
if (!firstExtendedColumn || firstExtendedColumn.date > date) {
viewScale = this.gantt.options.value('viewScale')
this.previousColumns = GanttColumnsManager.GanttColumnGenerator.generate(this.columnBuilder, from, date, viewScale, this.getColumnsWidth(), undefined, 0, true)
}
return true
} else if (endDate && date >= endDate) {
let lastExtendedColumn = this.getLastColumn(true)
if (!lastExtendedColumn || lastExtendedColumn.date < endDate) {
viewScale = this.gantt.options.value('viewScale')
this.nextColumns = GanttColumnsManager.GanttColumnGenerator.generate(this.columnBuilder, endDate, date, viewScale, this.getColumnsWidth(), undefined, this.gantt.width, false)
}
return true
}
return false
}
/**
* Returns the number of active headers
*
* @returns {number}
*/
getActiveHeadersCount () {
return this.headers.length
}
updateVisibleColumns (includeViews) {
let limitThreshold = this.gantt.options.value('columnLimitThreshold')
let i
if (limitThreshold === undefined || limitThreshold > 0 && this.columns.length >= limitThreshold) {
this.visibleColumns = GanttColumnsManager.$filter('ganttColumnLimit')(this.columns, this.gantt)
this.visibleHeaders = []
for (i = 0; i < this.headers.length; i++) {
this.visibleHeaders.push.apply(this.visibleHeaders, GanttColumnsManager.$filter('ganttColumnLimit')(this.headers[i], this.gantt))
}
} else {
this.visibleColumns = this.columns
this.visibleHeaders = this.headers
}
if (includeViews) {
for (i = 0; i < this.visibleColumns.length; i++) {
this.visibleColumns[i].updateView()
}
for (i = 0; i < this.visibleHeaders.length; i++) {
let headerRow = this.visibleHeaders[i]
for (let headerRowItem of headerRow) {
headerRowItem.updateView()
}
}
}
let currentDateValue = this.gantt.options.value('currentDateValue')
this.gantt.currentDateManager.setCurrentDate(currentDateValue)
}
getHeaderFormat (unit) {
let format
let headersFormats = this.gantt.options.value('headersFormats')
if (headersFormats !== undefined) {
format = headersFormats[unit]
}
if (format === undefined) {
let viewScale = this.gantt.options.value('viewScale')
viewScale = viewScale.trim()
if (viewScale.charAt(viewScale.length - 1) === 's') {
viewScale = viewScale.substring(0, viewScale.length - 1)
}
let viewScaleUnit
let splittedViewScale
if (viewScale) {
splittedViewScale = viewScale.split(' ')
}
if (splittedViewScale && splittedViewScale.length > 1) {
viewScaleUnit = splittedViewScale[splittedViewScale.length - 1]
} else {
viewScaleUnit = viewScale
}
if (['millisecond', 'second', 'minute', 'hour'].indexOf(viewScaleUnit) > -1) {
format = this.defaultDayHeadersFormats[unit]
} else if (['month', 'quarter', 'year'].indexOf(viewScaleUnit) > -1) {
format = this.defaultYearHeadersFormats[unit]
}
if (format === undefined) {
format = this.defaultHeadersFormats[unit]
}
}
return format
}
getHeaderScale (header) {
let scale
let headersScales = this.gantt.options.value('headersScales')
if (headersScales !== undefined) {
scale = headersScales[header]
}
if (scale === undefined) {
scale = header
}
if (['millisecond', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'].indexOf(scale) === -1) {
scale = 'day'
}
return scale
}
getDateRange (visibleOnly) {
let firstColumn
let lastColumn
if (visibleOnly) {
if (this.visibleColumns && this.visibleColumns.length > 0) {
firstColumn = this.visibleColumns[0]
lastColumn = this.visibleColumns[this.visibleColumns.length - 1]
}
} else {
firstColumn = this.getFirstColumn()
lastColumn = this.getLastColumn()
}
return firstColumn && lastColumn ? [firstColumn.date, lastColumn.endDate] : undefined
}
}
export default function (GanttColumnGenerator: GanttColumnGenerator,
GanttColumnBuilder: { new(columnsManager: GanttColumnsManager): GanttColumnBuilder },
GanttHeadersGenerator: GanttHeadersGenerator,
$filter: IGanttFilterService,
ganttLayout: GanttLayout,
ganttBinarySearch: GanttBinarySearch) {
'ngInject'
GanttColumnsManager.GanttColumnGenerator = GanttColumnGenerator
GanttColumnsManager.GanttHeadersGenerator = GanttHeadersGenerator
GanttColumnsManager.ganttBinarySearch = ganttBinarySearch
GanttColumnsManager.GanttColumnBuilder = GanttColumnBuilder
GanttColumnsManager.ganttLayout = ganttLayout
GanttColumnsManager.$filter = $filter
return GanttColumnsManager
}