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; })();