diff options
| author | shu <nvcsadjkojf@outlook.com> | 2026-07-26 22:42:45 +0800 |
|---|---|---|
| committer | shu <nvcsadjkojf@outlook.com> | 2026-07-26 22:42:45 +0800 |
| commit | 338cc101f127e4841689c1f81227ee033e57ad28 (patch) | |
| tree | 490cfc2ca29dc28e116a979f5c19b0851c3ea32e /tiddlers/plugin-task-board-widget.tid | |
| parent | 1888ff59743d83bd7776b0f16ef0bc20d68e3e10 (diff) | |
Add task board and organize local plugins
Diffstat (limited to 'tiddlers/plugin-task-board-widget.tid')
| -rw-r--r-- | tiddlers/plugin-task-board-widget.tid | 729 |
1 files changed, 729 insertions, 0 deletions
diff --git a/tiddlers/plugin-task-board-widget.tid b/tiddlers/plugin-task-board-widget.tid new file mode 100644 index 0000000..397811b --- /dev/null +++ b/tiddlers/plugin-task-board-widget.tid @@ -0,0 +1,729 @@ +title: $:/plugins/local/task-board/widget.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/task-board/widget.js +type: application/javascript +module-type: widget + +Task board with kanban, list, calendar, and Gantt views. +\*/ +(function() { +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget, + VIEW_STATE_TITLE = "$:/state/task-board/view", + MONTH_STATE_TITLE = "$:/state/task-board/month", + DAY_MS = 24 * 60 * 60 * 1000, + DAY_WIDTH = 30, + STATUSES = ["待处理","进行中","等待","完成"], + PRIORITIES = ["P1","P2","P3"], + VIEW_LABELS = { + board: "看板", + list: "列表", + calendar: "日历", + gantt: "甘特图" + }; + +var TaskBoardWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +TaskBoardWidget.prototype = new Widget(); +TaskBoardWidget.prototype.constructor = TaskBoardWidget; + +function pad(value) { + return value < 10 ? "0" + value : "" + value; +} + +function todayKey() { + var now = new Date(); + return now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()); +} + +function monthKey(date) { + return date.getUTCFullYear() + "-" + pad(date.getUTCMonth() + 1); +} + +function parseDate(value) { + var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || ""), + date; + if(!match) { + return null; + } + date = new Date(0); + date.setUTCHours(0,0,0,0); + date.setUTCFullYear(Number(match[1]),Number(match[2]) - 1,Number(match[3])); + if(date.getUTCFullYear() !== Number(match[1]) || date.getUTCMonth() !== Number(match[2]) - 1 || date.getUTCDate() !== Number(match[3])) { + return null; + } + return date; +} + +function dateKey(date) { + return date.getUTCFullYear() + "-" + pad(date.getUTCMonth() + 1) + "-" + pad(date.getUTCDate()); +} + +function dayNumber(date) { + return Math.floor(date.getTime() / DAY_MS); +} + +function addDays(date,amount) { + return new Date(date.getTime() + amount * DAY_MS); +} + +function isValidMonth(value) { + var match = /^(\d{4})-(\d{2})$/.exec(value || ""); + return !!match && Number(match[2]) >= 1 && Number(match[2]) <= 12; +} + +function priorityRank(priority) { + var rank = PRIORITIES.indexOf(priority); + return rank === -1 ? PRIORITIES.length : rank; +} + +function compareTasks(left,right) { + var priorityDifference = priorityRank(left.effectivePriority) - priorityRank(right.effectivePriority); + if(priorityDifference) { + return priorityDifference; + } + var leftDue = left.dueDate ? dayNumber(left.dueDate) : Infinity, + rightDue = right.dueDate ? dayNumber(right.dueDate) : Infinity; + if(leftDue !== rightDue) { + return leftDue - rightDue; + } + return left.title.localeCompare(right.title); +} + +TaskBoardWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + this.containerNode = this.document.createElement("div"); + this.containerNode.className = "tc-task-board"; + parent.insertBefore(this.containerNode,nextSibling); + this.domNodes.push(this.containerNode); + this.renderBoard(); +}; + +TaskBoardWidget.prototype.execute = function() { + this.tag = this.getAttribute("tag","事情"); +}; + +TaskBoardWidget.prototype.createElement = function(tagName,className,text) { + var node = this.document.createElement(tagName); + if(className) { + node.className = className; + } + if(text !== undefined && text !== null) { + node.textContent = text; + } + return node; +}; + +TaskBoardWidget.prototype.createButton = function(label,className,handler) { + var button = this.createElement("button",className,label); + button.setAttribute("type","button"); + button.addEventListener("click",handler); + return button; +}; + +TaskBoardWidget.prototype.setState = function(title,value) { + this.wiki.addTiddler({title: title,text: value}); +}; + +TaskBoardWidget.prototype.getView = function() { + var view = this.wiki.getTiddlerText(VIEW_STATE_TITLE,"board"); + return VIEW_LABELS[view] ? view : "board"; +}; + +TaskBoardWidget.prototype.getMonth = function() { + var value = this.wiki.getTiddlerText(MONTH_STATE_TITLE,""); + if(isValidMonth(value)) { + return value; + } + var today = parseDate(todayKey()); + return monthKey(today); +}; + +TaskBoardWidget.prototype.getTasks = function() { + var self = this, + tasks = []; + + this.wiki.getTiddlersWithTag(this.tag).forEach(function(title) { + var tiddler = self.wiki.getTiddler(title); + if(!tiddler || tiddler.fields["draft.of"]) { + return; + } + var fields = tiddler.fields, + start = fields.start || "", + end = fields.end || "", + due = fields.due || "", + startDate = parseDate(start), + endDate = parseDate(end), + dueDate = parseDate(due), + issues = [], + rangeError, + dateError, + calendarDates = []; + + if(start && !startDate) { + issues.push("开始日期无效"); + } + if(end && !endDate) { + issues.push("结束日期无效"); + } + if(due && !dueDate) { + issues.push("截止日期无效"); + } + if(startDate && endDate && endDate < startDate) { + issues.push("结束日期早于开始日期"); + } + dateError = !!((start && !startDate) || (end && !endDate) || (due && !dueDate)); + rangeError = !!((start && !startDate) || (end && !endDate) || (startDate && endDate && endDate < startDate)); + [ + {type: "start",label: "开始",date: startDate}, + {type: "end",label: "结束",date: endDate}, + {type: "due",label: "截止",date: dueDate} + ].forEach(function(item) { + if(item.date) { + calendarDates.push(item); + } + }); + calendarDates.sort(function(left,right) { + return dayNumber(left.date) - dayNumber(right.date); + }); + if(STATUSES.indexOf(fields.status) === -1) { + issues.push("状态无效"); + } + if(PRIORITIES.indexOf(fields.priority) === -1) { + issues.push("优先级无效"); + } + + tasks.push({ + title: title, + status: fields.status || "", + effectiveStatus: STATUSES.indexOf(fields.status) === -1 ? STATUSES[0] : fields.status, + priority: fields.priority || "", + effectivePriority: PRIORITIES.indexOf(fields.priority) === -1 ? "P2" : fields.priority, + start: start, + end: end, + due: due, + startDate: startDate, + endDate: endDate, + dueDate: dueDate, + rangeValid: !!startDate && !rangeError, + rangeError: rangeError, + dateError: dateError, + calendarDates: calendarDates, + tags: (fields.tags || []).filter(function(tag) {return tag !== self.tag;}), + issues: issues + }); + }); + + return tasks.sort(compareTasks); +}; + +TaskBoardWidget.prototype.navigateTo = function(title,node,event) { + var bounds = node.getBoundingClientRect(); + this.dispatchEvent({ + type: "tm-navigate", + navigateTo: title, + navigateFromTitle: this.getVariable("storyTiddler"), + navigateFromNode: this, + navigateFromClientRect: { + top: bounds.top, + left: bounds.left, + width: bounds.width, + right: bounds.right, + bottom: bounds.bottom, + height: bounds.height + }, + navigateSuppressNavigation: event && (event.metaKey || event.ctrlKey || event.button === 1) + }); +}; + +TaskBoardWidget.prototype.createTaskLink = function(task,className,label) { + var self = this, + button = this.createButton(label !== undefined ? label : task.title,"tc-btn-invisible " + className,function(event) { + self.navigateTo(task.title,event.currentTarget,event); + }); + button.title = "打开事情:" + task.title; + button.setAttribute("aria-label","打开事情:" + task.title); + return button; +}; + +TaskBoardWidget.prototype.renderBoard = function() { + var tasks = this.getTasks(), + view = this.getView(); + this.renderToolbar(view); + if(view === "list") { + this.renderList(tasks); + } else if(view === "calendar") { + this.renderCalendar(tasks); + } else if(view === "gantt") { + this.renderGantt(tasks); + } else { + this.renderKanban(tasks); + } +}; + +TaskBoardWidget.prototype.renderToolbar = function(currentView) { + var self = this, + toolbar = this.createElement("div","tc-task-board-toolbar"), + switcher = this.createElement("div","tc-task-board-switcher"); + + toolbar.appendChild(this.createButton("+ 新建事情","tc-btn-invisible tc-task-board-new",function() { + self.dispatchEvent({ + type: "tm-new-tiddler", + paramObject: { + tags: $tw.utils.stringifyList([self.tag]), + priority: "P2", + status: "待处理", + start: todayKey() + }, + navigateFromTitle: self.getVariable("storyTiddler") + }); + })); + + Object.keys(VIEW_LABELS).forEach(function(view) { + var button = self.createButton(VIEW_LABELS[view],"tc-btn-invisible tc-task-board-view-button" + (view === currentView ? " tc-selected" : ""),function() { + self.setState(VIEW_STATE_TITLE,view); + }); + button.setAttribute("aria-pressed",view === currentView ? "true" : "false"); + switcher.appendChild(button); + }); + toolbar.appendChild(switcher); + this.containerNode.appendChild(toolbar); +}; + +TaskBoardWidget.prototype.renderPriority = function(task) { + var badge = this.createElement("span","tc-task-priority tc-task-priority-" + task.effectivePriority.toLowerCase(),task.priority || "未设置"); + badge.title = "优先级"; + return badge; +}; + +TaskBoardWidget.prototype.renderIssues = function(task,parent) { + if(task.issues.length) { + var issue = this.createElement("span","tc-task-issues","⚠ " + task.issues.join(";")); + issue.title = task.issues.join(";"); + parent.appendChild(issue); + } +}; + +TaskBoardWidget.prototype.renderTaskDates = function(task,parent) { + var values = []; + if(task.start) {values.push("开始 " + task.start);} + if(task.end) {values.push("结束 " + task.end);} + if(task.due) {values.push("截止 " + task.due);} + parent.appendChild(this.createElement("div","tc-task-card-dates",values.length ? values.join(" · ") : "未排期")); +}; + +TaskBoardWidget.prototype.updateTaskStatus = function(title,status) { + var tiddler = this.wiki.getTiddler(title); + if(tiddler && !tiddler.fields["draft.of"] && (tiddler.fields.tags || []).indexOf(this.tag) !== -1 && STATUSES.indexOf(status) !== -1) { + this.wiki.addTiddler(new $tw.Tiddler(tiddler,{status: status},this.wiki.getModificationFields())); + } +}; + +TaskBoardWidget.prototype.renderKanban = function(tasks) { + var self = this, + board = this.createElement("div","tc-task-kanban"); + + STATUSES.forEach(function(status) { + var column = self.createElement("section","tc-task-kanban-column tc-task-status-" + STATUSES.indexOf(status)), + columnTasks = tasks.filter(function(task) {return task.effectiveStatus === status;}), + heading = self.createElement("h2","tc-task-kanban-heading",status + " · " + columnTasks.length), + cards = self.createElement("div","tc-task-kanban-cards"); + + column.setAttribute("data-task-status",status); + column.addEventListener("dragover",function(event) { + event.preventDefault(); + column.classList.add("tc-drag-over"); + }); + column.addEventListener("dragleave",function() { + column.classList.remove("tc-drag-over"); + }); + column.addEventListener("drop",function(event) { + event.preventDefault(); + column.classList.remove("tc-drag-over"); + var title = self.draggedTaskTitle; + if(title) { + self.updateTaskStatus(title,status); + } + self.draggedTaskTitle = null; + }); + + column.appendChild(heading); + columnTasks.forEach(function(task) { + var card = self.createElement("article","tc-task-card"); + card.setAttribute("draggable","true"); + card.setAttribute("data-task-title",task.title); + card.addEventListener("dragstart",function(event) { + self.draggedTaskTitle = task.title; + if(event.dataTransfer) { + event.dataTransfer.setData("application/x-tiddlywiki-task",task.title); + event.dataTransfer.effectAllowed = "move"; + } + card.classList.add("tc-dragging"); + }); + card.addEventListener("dragend",function() { + self.draggedTaskTitle = null; + card.classList.remove("tc-dragging"); + }); + var cardTop = self.createElement("div","tc-task-card-top"); + cardTop.appendChild(self.createTaskLink(task,"tc-task-card-title")); + cardTop.appendChild(self.renderPriority(task)); + card.appendChild(cardTop); + self.renderTaskDates(task,card); + if(task.tags.length) { + card.appendChild(self.createElement("div","tc-task-card-tags",task.tags.join(" · "))); + } + self.renderIssues(task,card); + cards.appendChild(card); + }); + if(!columnTasks.length) { + cards.appendChild(self.createElement("div","tc-task-empty-column","暂无事情")); + } + column.appendChild(cards); + board.appendChild(column); + }); + this.containerNode.appendChild(board); +}; + +TaskBoardWidget.prototype.renderList = function(tasks) { + var self = this, + wrapper = this.createElement("div","tc-task-list-wrapper"), + table = this.createElement("table","tc-task-list"), + thead = this.createElement("thead"), + headerRow = this.createElement("tr"), + tbody = this.createElement("tbody"); + + ["事情","优先级","状态","开始","结束","截止"].forEach(function(label) { + headerRow.appendChild(self.createElement("th","",label)); + }); + thead.appendChild(headerRow); + table.appendChild(thead); + + tasks.forEach(function(task) { + var row = self.createElement("tr","tc-task-list-row" + (task.issues.length ? " tc-task-invalid" : "")), + titleCell = self.createElement("td","tc-task-list-title"); + titleCell.setAttribute("data-label","事情"); + titleCell.appendChild(self.createTaskLink(task,"tc-task-list-link")); + self.renderIssues(task,titleCell); + row.appendChild(titleCell); + [ + ["优先级",task.priority || "-"], + ["状态",task.status || "-"], + ["开始",task.start || "-"], + ["结束",task.end || "-"], + ["截止",task.due || "-"] + ].forEach(function(item) { + var cell = self.createElement("td","",item[1]); + cell.setAttribute("data-label",item[0]); + row.appendChild(cell); + }); + tbody.appendChild(row); + }); + + if(!tasks.length) { + var emptyRow = this.createElement("tr"), + emptyCell = this.createElement("td","tc-task-empty","还没有事情"); + emptyCell.setAttribute("colspan","6"); + emptyRow.appendChild(emptyCell); + tbody.appendChild(emptyRow); + } + + table.appendChild(tbody); + wrapper.appendChild(table); + this.containerNode.appendChild(wrapper); +}; + +TaskBoardWidget.prototype.renderMonthControls = function(value) { + var self = this, + controls = this.createElement("div","tc-task-calendar-controls"), + parts = value.split("-"), + year = Number(parts[0]), + month = Number(parts[1]); + + function move(offset) { + self.setState(MONTH_STATE_TITLE,monthKey(new Date(Date.UTC(year,month - 1 + offset,1)))); + } + + controls.appendChild(this.createButton("上个月","tc-btn-invisible tc-task-month-button",function() {move(-1);})); + controls.appendChild(this.createElement("h2","tc-task-calendar-title",year + "年" + month + "月")); + controls.appendChild(this.createButton("本月","tc-btn-invisible tc-task-month-button",function() { + self.setState(MONTH_STATE_TITLE,monthKey(parseDate(todayKey()))); + })); + controls.appendChild(this.createButton("下个月","tc-btn-invisible tc-task-month-button",function() {move(1);})); + this.containerNode.appendChild(controls); +}; + +TaskBoardWidget.prototype.renderCalendar = function(tasks) { + var self = this, + value = this.getMonth(), + parts = value.split("-"), + year = Number(parts[0]), + month = Number(parts[1]), + firstDate = new Date(Date.UTC(year,month - 1,1)), + firstWeekday = (firstDate.getUTCDay() + 6) % 7, + daysInMonth = new Date(Date.UTC(year,month,0)).getUTCDate(), + calendar = this.createElement("div","tc-task-calendar-grid"), + header = this.createElement("div","tc-task-calendar-header"), + weeks = this.createElement("div","tc-task-calendar-weeks"), + weekdays = ["一","二","三","四","五","六","日"], + unscheduled = tasks.filter(function(task) { + return !task.calendarDates.length || task.dateError || task.rangeError; + }), + today = todayKey(), + monthStart = firstDate, + monthEnd = new Date(Date.UTC(year,month - 1,daysInMonth)), + calendarStart = addDays(firstDate,-firstWeekday), + weekCount = Math.ceil((firstWeekday + daysInMonth) / 7); + + this.renderMonthControls(value); + calendar.setAttribute("role","grid"); + header.setAttribute("role","row"); + weeks.setAttribute("role","rowgroup"); + weekdays.forEach(function(weekday) { + var heading = self.createElement("div","tc-task-calendar-weekday",weekday); + heading.setAttribute("role","columnheader"); + header.appendChild(heading); + }); + calendar.appendChild(header); + + for(var weekIndex = 0; weekIndex < weekCount; weekIndex++) { + var weekStart = addDays(calendarStart,weekIndex * 7), + weekEnd = addDays(weekStart,6), + week = this.createElement("div","tc-task-calendar-week"), + items = [], + lanes = []; + week.setAttribute("role","row"); + + tasks.forEach(function(task) { + if(task.calendarDates.length) { + var firstCalendarDate = task.calendarDates[0].date, + lastCalendarDate = task.calendarDates[task.calendarDates.length - 1].date, + segmentStart = new Date(Math.max(firstCalendarDate.getTime(),monthStart.getTime(),weekStart.getTime())), + segmentEnd = new Date(Math.min(lastCalendarDate.getTime(),monthEnd.getTime(),weekEnd.getTime())); + if(segmentStart <= segmentEnd) { + items.push({ + task: task, + type: task.calendarDates.length === 1 ? "point" : "range", + start: dayNumber(segmentStart) - dayNumber(weekStart) + 1, + end: dayNumber(segmentEnd) - dayNumber(weekStart) + 1, + starts: dateKey(segmentStart) === dateKey(firstCalendarDate), + ends: dateKey(segmentEnd) === dateKey(lastCalendarDate), + dateLabel: task.calendarDates[0].label + }); + } + if(task.calendarDates.length === 3) { + var middle = task.calendarDates[1]; + if(middle.date >= monthStart && middle.date <= monthEnd && middle.date >= weekStart && middle.date <= weekEnd) { + var middleColumn = dayNumber(middle.date) - dayNumber(weekStart) + 1; + items.push({task: task,type: "milestone",start: middleColumn,end: middleColumn,dateLabel: middle.label}); + } + } + } + }); + + items.forEach(function(item) { + var lane = 0; + while(lanes[lane] && lanes[lane].some(function(occupied) { + return item.start <= occupied.end && item.end >= occupied.start; + })) { + lane++; + } + lanes[lane] = lanes[lane] || []; + lanes[lane].push(item); + item.lane = lane; + }); + + week.style.gridTemplateRows = "minmax(2.25rem,1fr) repeat(" + lanes.length + ",1.5rem)"; + for(var column = 0; column < 7; column++) { + var cellDate = addDays(weekStart,column), + inMonth = cellDate >= monthStart && cellDate <= monthEnd, + key = dateKey(cellDate), + cell = self.createElement("div","tc-task-calendar-day" + (!inMonth ? " tc-task-calendar-blank" : "") + (key === today ? " tc-task-calendar-today" : "")); + cell.style.gridColumn = (column + 1) + ""; + cell.style.gridRow = "1 / span " + (lanes.length + 1); + cell.setAttribute("role","gridcell"); + cell.setAttribute("aria-label",key); + if(inMonth) { + cell.appendChild(self.createElement("div","tc-task-calendar-day-number",cellDate.getUTCDate())); + } + week.appendChild(cell); + } + + items.forEach(function(item) { + var className = item.type === "range" ? "tc-task-calendar-range" + (item.starts ? " tc-range-start" : "") + (item.ends ? " tc-range-end" : "") : item.type === "point" ? "tc-task-calendar-point" : "tc-task-calendar-milestone", + label = item.type === "point" || item.type === "milestone" ? item.dateLabel + " · " + item.task.title : item.task.title, + entry = self.createTaskLink(item.task,className,label); + entry.style.gridColumn = item.start + " / " + (item.end + 1); + entry.style.gridRow = (item.lane + 2) + ""; + entry.setAttribute("data-priority",item.task.effectivePriority); + week.appendChild(entry); + }); + weeks.appendChild(week); + } + + calendar.appendChild(weeks); + this.containerNode.appendChild(calendar); + this.renderUnscheduled(unscheduled,"未排期或日期异常"); +}; + +TaskBoardWidget.prototype.renderUnscheduled = function(tasks,label) { + var self = this; + if(!tasks.length) { + return; + } + var section = this.createElement("section","tc-task-unscheduled"), + list = this.createElement("div","tc-task-unscheduled-list"); + section.appendChild(this.createElement("h3","",label + " · " + tasks.length)); + tasks.forEach(function(task) { + list.appendChild(self.createTaskLink(task,"tc-task-unscheduled-link")); + }); + section.appendChild(list); + this.containerNode.appendChild(section); +}; + +TaskBoardWidget.prototype.renderGantt = function(tasks) { + var self = this, + datedTasks = tasks.filter(function(task) {return task.rangeValid || (!task.start && task.endDate) || task.dueDate;}), + unscheduled = tasks.filter(function(task) {return task.rangeError || (!task.rangeValid && !(!task.start && task.endDate) && !task.dueDate);}), + dates = []; + + datedTasks.forEach(function(task) { + var renderedDates = []; + if(task.rangeValid) { + renderedDates.push(task.startDate,task.endDate || task.startDate); + } else if(!task.start && task.endDate) { + renderedDates.push(task.endDate); + } + if(task.dueDate) { + renderedDates.push(task.dueDate); + } + renderedDates.forEach(function(date) { + if(date) {dates.push(dayNumber(date));} + }); + }); + if(!dates.length) { + this.containerNode.appendChild(this.createElement("div","tc-task-empty","没有可绘制的日期")); + this.renderUnscheduled(unscheduled,"未排期事情"); + return; + } + + var minimum = Math.min.apply(Math,dates) - 2, + maximum = Math.max.apply(Math,dates) + 2, + span = maximum - minimum + 1, + step = Math.max(1,Math.ceil(span / 366)), + units = Math.ceil(span / step), + labelReserve = Math.max.apply(Math,datedTasks.map(function(task) { + return task.title.length * 14 + 32; + }).concat([180])), + startDate = new Date(minimum * DAY_MS), + wrapper = this.createElement("div","tc-task-gantt-scroll"), + gantt = this.createElement("div","tc-task-gantt"), + header = this.createElement("div","tc-task-gantt-row tc-task-gantt-header"), + headerLabel = this.createElement("div","tc-task-gantt-label","事情"), + headerTimeline = this.createElement("div","tc-task-gantt-timeline tc-task-gantt-header-timeline"), + monthTrack = this.createElement("div","tc-task-gantt-months"), + dayTrack = this.createElement("div","tc-task-gantt-days"), + monthGroups = [], + today = parseDate(todayKey()), + todayOffset = (dayNumber(today) - minimum) / step; + + gantt.style.setProperty("--task-gantt-width",units * DAY_WIDTH + labelReserve + "px"); + gantt.style.setProperty("--task-gantt-date-width",units * DAY_WIDTH + "px"); + header.appendChild(headerLabel); + for(var index = 0; index < units; index++) { + var date = addDays(startDate,index * step), + key = monthKey(date), + group = monthGroups[monthGroups.length - 1], + day = this.createElement("div","tc-task-gantt-day",pad(date.getUTCDate())); + if(!group || group.key !== key) { + group = {key: key,date: date,count: 0}; + monthGroups.push(group); + } + group.count++; + day.style.width = DAY_WIDTH + "px"; + day.setAttribute("title",dateKey(date) + (step > 1 ? " 起 " + step + " 天" : "")); + day.setAttribute("aria-label",dateKey(date) + (step > 1 ? " 起 " + step + " 天" : "")); + dayTrack.appendChild(day); + } + monthGroups.forEach(function(group) { + var segment = self.createElement("div","tc-task-gantt-month-segment"), + label = self.createElement("span","tc-task-gantt-month-label",group.date.getUTCFullYear() + "年" + (group.date.getUTCMonth() + 1) + "月"); + segment.style.width = group.count * DAY_WIDTH + "px"; + segment.setAttribute("title",label.textContent); + segment.appendChild(label); + monthTrack.appendChild(segment); + }); + headerTimeline.appendChild(monthTrack); + headerTimeline.appendChild(dayTrack); + header.appendChild(headerTimeline); + gantt.appendChild(header); + + datedTasks.forEach(function(task) { + var row = self.createElement("div","tc-task-gantt-row"), + label = self.createElement("div","tc-task-gantt-label"), + timeline = self.createElement("div","tc-task-gantt-timeline"), + barStart = task.startDate, + barEnd = task.rangeValid ? (task.endDate || task.startDate) : null; + label.appendChild(self.createTaskLink(task,"tc-task-gantt-task-link")); + label.appendChild(self.renderPriority(task)); + self.renderIssues(task,label); + + if(barStart && barEnd) { + var left = (dayNumber(barStart) - minimum) / step * DAY_WIDTH, + width = (dayNumber(barEnd) - dayNumber(barStart) + 1) / step * DAY_WIDTH, + barWidth = Math.max(width,DAY_WIDTH), + bar = self.createTaskLink(task,"tc-task-gantt-bar",""), + barLabel = self.createTaskLink(task,"tc-task-gantt-bar-label"); + bar.style.left = left + "px"; + bar.style.width = barWidth + "px"; + bar.setAttribute("data-priority",task.effectivePriority); + barLabel.style.left = (left + barWidth + 8) + "px"; + timeline.appendChild(bar); + timeline.appendChild(barLabel); + } else if(!task.start && task.endDate) { + var endMarker = self.createTaskLink(task,"tc-task-gantt-end","◆"); + endMarker.style.left = ((dayNumber(task.endDate) - minimum) / step * DAY_WIDTH + DAY_WIDTH / 2) + "px"; + endMarker.title = "计划结束:" + task.end; + timeline.appendChild(endMarker); + } + if(task.dueDate) { + var dueMarker = self.createTaskLink(task,"tc-task-gantt-due","◆"); + dueMarker.style.left = ((dayNumber(task.dueDate) - minimum) / step * DAY_WIDTH + DAY_WIDTH / 2) + "px"; + dueMarker.title = "截止:" + task.due; + timeline.appendChild(dueMarker); + } + if(todayOffset >= 0 && todayOffset < units) { + var todayLine = self.createElement("span","tc-task-gantt-today-line"); + todayLine.style.left = (todayOffset * DAY_WIDTH + DAY_WIDTH / 2) + "px"; + timeline.appendChild(todayLine); + } + row.appendChild(label); + row.appendChild(timeline); + gantt.appendChild(row); + }); + + wrapper.appendChild(gantt); + this.containerNode.appendChild(wrapper); + this.renderUnscheduled(unscheduled,"未排期事情"); +}; + +TaskBoardWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(Object.keys(changedAttributes).length || Object.keys(changedTiddlers).length) { + this.refreshSelf(); + return true; + } + return false; +}; + +exports["task-board"] = TaskBoardWidget; + +})(); |
