Files
Redmine/app/javascript/controllers/gantt/chart_controller.js

354 lines
10 KiB
JavaScript
Raw Permalink Normal View History

import { Controller } from "@hotwired/stimulus"
const RELATION_STROKE_WIDTH = 2
export default class extends Controller {
static targets = ["ganttArea", "drawArea", "subjectsContainer"]
static values = {
issueRelationTypes: Object,
showSelectedColumns: Boolean,
showRelations: Boolean,
showProgress: Boolean
}
#drawTop = 0
#drawRight = 0
#drawLeft = 0
#drawPaper = null
initialize() {
this.$ = window.jQuery
this.Raphael = window.Raphael
}
connect() {
this.#drawTop = 0
this.#drawRight = 0
this.#drawLeft = 0
this.#drawProgressLineAndRelations()
this.#drawSelectedColumns()
}
disconnect() {
if (this.#drawPaper) {
this.#drawPaper.remove()
this.#drawPaper = null
}
}
showSelectedColumnsValueChanged() {
this.#drawSelectedColumns()
}
showRelationsValueChanged() {
this.#drawProgressLineAndRelations()
}
showProgressValueChanged() {
this.#drawProgressLineAndRelations()
}
handleWindowResize() {
this.#drawProgressLineAndRelations()
this.#drawSelectedColumns()
}
handleSubjectTreeChanged() {
this.#drawProgressLineAndRelations()
this.#drawSelectedColumns()
}
handleOptionsDisplay(event) {
this.showSelectedColumnsValue = !!(event.detail && event.detail.enabled)
}
handleOptionsRelations(event) {
this.showRelationsValue = !!(event.detail && event.detail.enabled)
}
handleOptionsProgress(event) {
this.showProgressValue = !!(event.detail && event.detail.enabled)
}
#drawProgressLineAndRelations() {
if (this.#drawPaper) {
this.#drawPaper.clear()
} else {
this.#drawPaper = this.Raphael(this.drawAreaTarget)
}
this.#setupDrawArea()
if (this.showProgressValue) {
this.#drawGanttProgressLines()
}
if (this.showRelationsValue) {
this.#drawRelations()
}
}
#setupDrawArea() {
const $drawArea = this.$(this.drawAreaTarget)
const $ganttArea = this.hasGanttAreaTarget ? this.$(this.ganttAreaTarget) : null
this.#drawTop = $drawArea.position().top
this.#drawRight = $drawArea.width()
this.#drawLeft = $ganttArea ? $ganttArea.scrollLeft() : 0
}
#drawSelectedColumns() {
const $selectedColumns = this.$("td.gantt_selected_column")
const $subjectsContainer = this.$(".gantt_subjects_container")
const isMobileDevice = typeof window.isMobile === "function" && window.isMobile()
if (this.showSelectedColumnsValue) {
if (isMobileDevice) {
$selectedColumns.each((_, element) => {
this.$(element).hide()
})
} else {
$subjectsContainer.addClass("draw_selected_columns")
$selectedColumns.show()
}
} else {
$selectedColumns.each((_, element) => {
this.$(element).hide()
})
$subjectsContainer.removeClass("draw_selected_columns")
}
}
get #relationsArray() {
const relations = []
this.$("div.task_todo[data-rels]").each((_, element) => {
const $element = this.$(element)
if (!$element.is(":visible")) return
const elementId = $element.attr("id")
if (!elementId) return
const issueId = elementId.replace("task-todo-issue-", "")
const dataRels = $element.data("rels") || {}
Object.keys(dataRels).forEach((relTypeKey) => {
this.$.each(dataRels[relTypeKey], (_, relatedIssue) => {
relations.push({ issue_from: issueId, issue_to: relatedIssue, rel_type: relTypeKey })
})
})
})
return relations
}
#drawRelations() {
const relations = this.#relationsArray
relations.forEach((relation) => {
const issueFrom = this.$(`#task-todo-issue-${relation.issue_from}`)
const issueTo = this.$(`#task-todo-issue-${relation.issue_to}`)
if (issueFrom.length === 0 || issueTo.length === 0) return
const issueHeight = issueFrom.height()
const issueFromTop = issueFrom.position().top + issueHeight / 2 - this.#drawTop
const issueFromRight = issueFrom.position().left + issueFrom.width()
const issueToTop = issueTo.position().top + issueHeight / 2 - this.#drawTop
const issueToLeft = issueTo.position().left
const relationConfig = this.issueRelationTypesValue[relation.rel_type] || {}
const color = relationConfig.color || "#000"
const landscapeMargin = relationConfig.landscape_margin || 0
const issueFromRightRel = issueFromRight + landscapeMargin
const issueToLeftRel = issueToLeft - landscapeMargin
this.#drawPaper
.path([
"M",
issueFromRight + this.#drawLeft,
issueFromTop,
"L",
issueFromRightRel + this.#drawLeft,
issueFromTop
])
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
if (issueFromRightRel < issueToLeftRel) {
this.#drawPaper
.path([
"M",
issueFromRightRel + this.#drawLeft,
issueFromTop,
"L",
issueFromRightRel + this.#drawLeft,
issueToTop
])
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
this.#drawPaper
.path([
"M",
issueFromRightRel + this.#drawLeft,
issueToTop,
"L",
issueToLeft + this.#drawLeft,
issueToTop
])
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
} else {
const issueMiddleTop = issueToTop + issueHeight * (issueFromTop > issueToTop ? 1 : -1)
this.#drawPaper
.path([
"M",
issueFromRightRel + this.#drawLeft,
issueFromTop,
"L",
issueFromRightRel + this.#drawLeft,
issueMiddleTop
])
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
this.#drawPaper
.path([
"M",
issueFromRightRel + this.#drawLeft,
issueMiddleTop,
"L",
issueToLeftRel + this.#drawLeft,
issueMiddleTop
])
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
this.#drawPaper
.path([
"M",
issueToLeftRel + this.#drawLeft,
issueMiddleTop,
"L",
issueToLeftRel + this.#drawLeft,
issueToTop
])
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
this.#drawPaper
.path([
"M",
issueToLeftRel + this.#drawLeft,
issueToTop,
"L",
issueToLeft + this.#drawLeft,
issueToTop
])
.attr({ stroke: color, "stroke-width": RELATION_STROKE_WIDTH })
}
this.#drawPaper
.path([
"M",
issueToLeft + this.#drawLeft,
issueToTop,
"l",
-4 * RELATION_STROKE_WIDTH,
-2 * RELATION_STROKE_WIDTH,
"l",
0,
4 * RELATION_STROKE_WIDTH,
"z"
])
.attr({
stroke: "none",
fill: color,
"stroke-linecap": "butt",
"stroke-linejoin": "miter"
})
})
}
get #progressLinesArray() {
const lines = []
const todayLeft = this.$("#today_line").position().left
lines.push({ left: todayLeft, top: 0 })
this.$("div.issue-subject, div.version-name").each((_, element) => {
const $element = this.$(element)
if (!$element.is(":visible")) return true
const topPosition = $element.position().top - this.#drawTop
const elementHeight = $element.height() / 9
const elementTopUpper = topPosition - elementHeight
const elementTopCenter = topPosition + elementHeight * 3
const elementTopLower = topPosition + elementHeight * 8
const issueClosed = $element.children("span").hasClass("issue-closed")
const versionClosed = $element.children("span").hasClass("version-closed")
if (issueClosed || versionClosed) {
lines.push({ left: todayLeft, top: elementTopCenter })
} else {
const issueDone = this.$(`#task-done-${$element.attr("id")}`)
const isBehindStart = $element.children("span").hasClass("behind-start-date")
const isOverEnd = $element.children("span").hasClass("over-end-date")
if (isOverEnd) {
lines.push({ left: this.#drawRight, top: elementTopUpper, is_right_edge: true })
lines.push({
left: this.#drawRight,
top: elementTopLower,
is_right_edge: true,
none_stroke: true
})
} else if (issueDone.length > 0) {
const doneLeft = issueDone.first().position().left + issueDone.first().width()
lines.push({ left: doneLeft, top: elementTopCenter })
} else if (isBehindStart) {
lines.push({ left: 0, top: elementTopUpper, is_left_edge: true })
lines.push({
left: 0,
top: elementTopLower,
is_left_edge: true,
none_stroke: true
})
} else {
let todoLeft = todayLeft
const issueTodo = this.$(`#task-todo-${$element.attr("id")}`)
if (issueTodo.length > 0) {
todoLeft = issueTodo.first().position().left
}
lines.push({ left: Math.min(todayLeft, todoLeft), top: elementTopCenter })
}
}
})
return lines
}
#drawGanttProgressLines() {
if (this.$("#today_line").length === 0) return
const progressLines = this.#progressLinesArray
const color = this.$("#today_line").css("border-left-color") || "#ff0000"
for (let index = 1; index < progressLines.length; index += 1) {
const current = progressLines[index]
const previous = progressLines[index - 1]
if (
!current.none_stroke &&
!(
(previous.is_right_edge && current.is_right_edge) ||
(previous.is_left_edge && current.is_left_edge)
)
) {
const x1 = previous.left === 0 ? 0 : previous.left + this.#drawLeft
const x2 = current.left === 0 ? 0 : current.left + this.#drawLeft
this.#drawPaper
.path(["M", x1, previous.top, "L", x2, current.top])
.attr({ stroke: color, "stroke-width": 2 })
}
}
}
}