mirror of
https://github.com/redmine/redmine.git
synced 2026-03-07 04:51:35 +01:00
Extract Gantt view structure and wire Stimulus controllers (#43397).
Patch by Katsuya HIDAKA (user:hidakatsuya). git-svn-id: https://svn.redmine.org/redmine/trunk@24085 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
357
app/javascript/controllers/gantt/chart_controller.js
Normal file
357
app/javascript/controllers/gantt/chart_controller.js
Normal file
@@ -0,0 +1,357 @@
|
||||
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()
|
||||
}
|
||||
|
||||
const content = document.getElementById("content")
|
||||
if (content) {
|
||||
content.classList.add("gantt_content")
|
||||
}
|
||||
}
|
||||
|
||||
#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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user