aboutsummaryrefslogtreecommitdiffstats
path: root/tiddlers/plugin-task-board-widget.tid
diff options
context:
space:
mode:
Diffstat (limited to 'tiddlers/plugin-task-board-widget.tid')
-rw-r--r--tiddlers/plugin-task-board-widget.tid729
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;
+
+})();