mermaid/src/diagrams/gantt/ganttDb.js

576 lines
14 KiB
JavaScript

import moment from 'moment-mini'
import { sanitizeUrl } from '@braintree/sanitize-url'
import { logger } from '../../logger'
import { getConfig } from '../../config'
const config = getConfig()
let dateFormat = ''
let axisFormat = ''
let excludes = []
let title = ''
let sections = []
let tasks = []
let currentSection = ''
const tags = ['active', 'done', 'crit', 'milestone']
let funs = []
let inclusiveEndDates = false
export const clear = function () {
sections = []
tasks = []
currentSection = ''
funs = []
title = ''
taskCnt = 0
lastTask = undefined
lastTaskID = undefined
rawTasks = []
dateFormat = ''
axisFormat = ''
excludes = []
inclusiveEndDates = false
}
export const setAxisFormat = function (txt) {
axisFormat = txt
}
export const getAxisFormat = function () {
return axisFormat
}
export const setDateFormat = function (txt) {
dateFormat = txt
}
export const enableInclusiveEndDates = function () {
inclusiveEndDates = true
}
export const endDatesAreInclusive = function () {
return inclusiveEndDates
}
export const getDateFormat = function () {
return dateFormat
}
export const setExcludes = function (txt) {
excludes = txt.toLowerCase().split(/[\s,]+/)
}
export const getExcludes = function () {
return excludes
}
export const setTitle = function (txt) {
title = txt
}
export const getTitle = function () {
return title
}
export const addSection = function (txt) {
currentSection = txt
sections.push(txt)
}
export const getSections = function () {
return sections
}
export const getTasks = function () {
let allItemsPricessed = compileTasks()
const maxDepth = 10
let iterationCount = 0
while (!allItemsPricessed && (iterationCount < maxDepth)) {
allItemsPricessed = compileTasks()
iterationCount++
}
tasks = rawTasks
return tasks
}
const isInvalidDate = function (date, dateFormat, excludes) {
if (date.isoWeekday() >= 6 && excludes.indexOf('weekends') >= 0) {
return true
}
if (excludes.indexOf(date.format('dddd').toLowerCase()) >= 0) {
return true
}
return excludes.indexOf(date.format(dateFormat.trim())) >= 0
}
const checkTaskDates = function (task, dateFormat, excludes) {
if (!excludes.length || task.manualEndTime) return
let startTime = moment(task.startTime, dateFormat, true)
startTime.add(1, 'd')
let endTime = moment(task.endTime, dateFormat, true)
let renderEndTime = fixTaskDates(startTime, endTime, dateFormat, excludes)
task.endTime = endTime.toDate()
task.renderEndTime = renderEndTime
}
const fixTaskDates = function (startTime, endTime, dateFormat, excludes) {
let invalid = false
let renderEndTime = null
while (startTime.date() <= endTime.date()) {
if (!invalid) {
renderEndTime = endTime.toDate()
}
invalid = isInvalidDate(startTime, dateFormat, excludes)
if (invalid) {
endTime.add(1, 'd')
}
startTime.add(1, 'd')
}
return renderEndTime
}
const getStartDate = function (prevTime, dateFormat, str) {
str = str.trim()
// Test for after
const re = /^after\s+([\d\w-]+)/
const afterStatement = re.exec(str.trim())
if (afterStatement !== null) {
const task = findTaskById(afterStatement[1])
if (typeof task === 'undefined') {
const dt = new Date()
dt.setHours(0, 0, 0, 0)
return dt
}
return task.endTime
}
// Check for actual date set
let mDate = moment(str, dateFormat.trim(), true)
if (mDate.isValid()) {
return mDate.toDate()
} else {
logger.debug('Invalid date:' + str)
logger.debug('With date format:' + dateFormat.trim())
}
// Default date - now
return new Date()
}
const durationToDate = function (durationStatement, relativeTime) {
if (durationStatement !== null) {
switch (durationStatement[2]) {
case 's':
relativeTime.add(durationStatement[1], 'seconds')
break
case 'm':
relativeTime.add(durationStatement[1], 'minutes')
break
case 'h':
relativeTime.add(durationStatement[1], 'hours')
break
case 'd':
relativeTime.add(durationStatement[1], 'days')
break
case 'w':
relativeTime.add(durationStatement[1], 'weeks')
break
}
}
// Default date - now
return relativeTime.toDate()
}
const getEndDate = function (prevTime, dateFormat, str, inclusive) {
inclusive = inclusive || false
str = str.trim()
// Check for actual date
let mDate = moment(str, dateFormat.trim(), true)
if (mDate.isValid()) {
if (inclusive) {
mDate.add(1, 'd')
}
return mDate.toDate()
}
return durationToDate(
/^([\d]+)([wdhms])/.exec(str.trim()),
moment(prevTime)
)
}
let taskCnt = 0
const parseId = function (idStr) {
if (typeof idStr === 'undefined') {
taskCnt = taskCnt + 1
return 'task' + taskCnt
}
return idStr
}
// id, startDate, endDate
// id, startDate, length
// id, after x, endDate
// id, after x, length
// startDate, endDate
// startDate, length
// after x, endDate
// after x, length
// endDate
// length
const compileData = function (prevTask, dataStr) {
let ds
if (dataStr.substr(0, 1) === ':') {
ds = dataStr.substr(1, dataStr.length)
} else {
ds = dataStr
}
const data = ds.split(',')
const task = {}
// Get tags like active, done, crit and milestone
getTaskTags(data, task, tags)
for (let i = 0; i < data.length; i++) {
data[i] = data[i].trim()
}
let endTimeData = ''
switch (data.length) {
case 1:
task.id = parseId()
task.startTime = prevTask.endTime
endTimeData = data[0]
break
case 2:
task.id = parseId()
task.startTime = getStartDate(undefined, dateFormat, data[0])
endTimeData = data[1]
break
case 3:
task.id = parseId(data[0])
task.startTime = getStartDate(undefined, dateFormat, data[1])
endTimeData = data[2]
break
default:
}
if (endTimeData) {
task.endTime = getEndDate(task.startTime, dateFormat, endTimeData, inclusiveEndDates)
task.manualEndTime = moment(endTimeData, 'YYYY-MM-DD', true).isValid()
checkTaskDates(task, dateFormat, excludes)
}
return task
}
const parseData = function (prevTaskId, dataStr) {
let ds
if (dataStr.substr(0, 1) === ':') {
ds = dataStr.substr(1, dataStr.length)
} else {
ds = dataStr
}
const data = ds.split(',')
const task = {}
// Get tags like active, done, crit and milestone
getTaskTags(data, task, tags)
for (let i = 0; i < data.length; i++) {
data[i] = data[i].trim()
}
switch (data.length) {
case 1:
task.id = parseId()
task.startTime = {
type: 'prevTaskEnd',
id: prevTaskId
}
task.endTime = {
data: data[0]
}
break
case 2:
task.id = parseId()
task.startTime = {
type: 'getStartDate',
startData: data[0]
}
task.endTime = {
data: data[1]
}
break
case 3:
task.id = parseId(data[0])
task.startTime = {
type: 'getStartDate',
startData: data[1]
}
task.endTime = {
data: data[2]
}
break
default:
}
return task
}
let lastTask
let lastTaskID
let rawTasks = []
const taskDb = {}
export const addTask = function (descr, data) {
const rawTask = {
section: currentSection,
type: currentSection,
processed: false,
manualEndTime: false,
renderEndTime: null,
raw: { data: data },
task: descr,
classes: []
}
const taskInfo = parseData(lastTaskID, data)
rawTask.raw.startTime = taskInfo.startTime
rawTask.raw.endTime = taskInfo.endTime
rawTask.id = taskInfo.id
rawTask.prevTaskId = lastTaskID
rawTask.active = taskInfo.active
rawTask.done = taskInfo.done
rawTask.crit = taskInfo.crit
rawTask.milestone = taskInfo.milestone
const pos = rawTasks.push(rawTask)
lastTaskID = rawTask.id
// Store cross ref
taskDb[rawTask.id] = pos - 1
}
export const findTaskById = function (id) {
const pos = taskDb[id]
return rawTasks[pos]
}
export const addTaskOrg = function (descr, data) {
const newTask = {
section: currentSection,
type: currentSection,
description: descr,
task: descr,
classes: []
}
const taskInfo = compileData(lastTask, data)
newTask.startTime = taskInfo.startTime
newTask.endTime = taskInfo.endTime
newTask.id = taskInfo.id
newTask.active = taskInfo.active
newTask.done = taskInfo.done
newTask.crit = taskInfo.crit
newTask.milestone = taskInfo.milestone
lastTask = newTask
tasks.push(newTask)
}
const compileTasks = function () {
const compileTask = function (pos) {
const task = rawTasks[pos]
let startTime = ''
switch (rawTasks[pos].raw.startTime.type) {
case 'prevTaskEnd':
const prevTask = findTaskById(task.prevTaskId)
task.startTime = prevTask.endTime
break
case 'getStartDate':
startTime = getStartDate(undefined, dateFormat, rawTasks[pos].raw.startTime.startData)
if (startTime) {
rawTasks[pos].startTime = startTime
}
break
}
if (rawTasks[pos].startTime) {
rawTasks[pos].endTime = getEndDate(rawTasks[pos].startTime, dateFormat, rawTasks[pos].raw.endTime.data, inclusiveEndDates)
if (rawTasks[pos].endTime) {
rawTasks[pos].processed = true
rawTasks[pos].manualEndTime = moment(rawTasks[pos].raw.endTime.data, 'YYYY-MM-DD', true).isValid()
checkTaskDates(rawTasks[pos], dateFormat, excludes)
}
}
return rawTasks[pos].processed
}
let allProcessed = true
for (let i = 0; i < rawTasks.length; i++) {
compileTask(i)
allProcessed = allProcessed && rawTasks[i].processed
}
return allProcessed
}
/**
* Called by parser when a link is found. Adds the URL to the vertex data.
* @param ids Comma separated list of ids
* @param linkStr URL to create a link for
*/
export const setLink = function (ids, _linkStr) {
let linkStr = _linkStr
if (config.securityLevel !== 'loose') {
linkStr = sanitizeUrl(_linkStr)
}
ids.split(',').forEach(function (id) {
let rawTask = findTaskById(id)
if (typeof rawTask !== 'undefined') {
pushFun(id, () => { window.open(linkStr, '_self') })
}
})
setClass(ids, 'clickable')
}
/**
* Called by parser when a special node is found, e.g. a clickable element.
* @param ids Comma separated list of ids
* @param className Class to add
*/
export const setClass = function (ids, className) {
ids.split(',').forEach(function (id) {
let rawTask = findTaskById(id)
if (typeof rawTask !== 'undefined') {
rawTask.classes.push(className)
}
})
}
const setClickFun = function (id, functionName, functionArgs) {
if (config.securityLevel !== 'loose') {
return
}
if (typeof functionName === 'undefined') {
return
}
let argList = []
if (typeof functionArgs === 'string') {
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/)
for (let i = 0; i < argList.length; i++) {
let item = argList[i].trim()
/* Removes all double quotes at the start and end of an argument */
/* This preserves all starting and ending whitespace inside */
if (item.charAt(0) === '"' && item.charAt(item.length - 1) === '"') {
item = item.substr(1, item.length - 2)
}
argList[i] = item
}
}
let rawTask = findTaskById(id)
if (typeof rawTask !== 'undefined') {
pushFun(id, () => { window[functionName](...argList) })
}
}
/**
* The callbackFunction is executed in a click event bound to the task with the specified id or the task's assigned text
* @param id The task's id
* @param callbackFunction A function to be executed when clicked on the task or the task's text
*/
const pushFun = function (id, callbackFunction) {
funs.push(function (element) {
// const elem = d3.select(element).select(`[id="${id}"]`)
const elem = document.querySelector(`[id="${id}"]`)
if (elem !== null) {
elem.addEventListener('click', function () {
callbackFunction()
})
}
})
funs.push(function (element) {
// const elem = d3.select(element).select(`[id="${id}-text"]`)
const elem = document.querySelector(`[id="${id}-text"]`)
if (elem !== null) {
elem.addEventListener('click', function () {
callbackFunction()
})
}
})
}
/**
* Called by parser when a click definition is found. Registers an event handler.
* @param ids Comma separated list of ids
* @param functionName Function to be called on click
* @param functionArgs Function args the function should be called with
*/
export const setClickEvent = function (ids, functionName, functionArgs) {
ids.split(',').forEach(function (id) {
setClickFun(id, functionName, functionArgs)
})
setClass(ids, 'clickable')
}
/**
* Binds all functions previously added to fun (specified through click) to the element
* @param element
*/
export const bindFunctions = function (element) {
funs.forEach(function (fun) {
fun(element)
})
}
export default {
clear,
setDateFormat,
getDateFormat,
enableInclusiveEndDates,
endDatesAreInclusive,
setAxisFormat,
getAxisFormat,
setTitle,
getTitle,
addSection,
getSections,
getTasks,
addTask,
findTaskById,
addTaskOrg,
setExcludes,
getExcludes,
setClickEvent,
setLink,
bindFunctions,
durationToDate
}
function getTaskTags (data, task, tags) {
let matchFound = true
while (matchFound) {
matchFound = false
tags.forEach(function (t) {
const pattern = '^\\s*' + t + '\\s*$'
const regex = new RegExp(pattern)
if (data[0].match(regex)) {
task[t] = true
data.shift(1)
matchFound = true
}
})
}
}