From 8efba22ed34b6eeb28b5f410177e5a1545b5d798 Mon Sep 17 00:00:00 2001 From: shu Date: Tue, 28 Jul 2026 19:16:16 +0800 Subject: 用 Org GTD 工作流替换事情看板 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 注册 text/x-org 内容类型和 CodeMirror 编辑器,支持 TODO 状态流转、层级调整、子树移动与折叠。 新增 Org 文档解析、Agenda 日期分组、源任务定位、侧栏入口和响应式样式,并补充中文使用说明。 快捷键改为仅在 Org 编辑器中按配置加载,默认不绑定;无效 JSON 或键名只禁用自定义键位,不影响编辑器启动。 移除旧事情看板的字段模板、组件、样式和默认入口,统一使用 Org GTD 工作流。 --- tiddlers/$__DefaultTiddlers.tid | 3 +- tiddlers/GTD Agenda.tid | 5 + ...75\277\347\224\250\350\257\264\346\230\216.tid" | 46 ++ tiddlers/plugin-org-gtd-agenda-widget.tid | 229 +++++++ tiddlers/plugin-org-gtd-codemirror.tid | 341 ++++++++++ tiddlers/plugin-org-gtd-content-type.tid | 12 + tiddlers/plugin-org-gtd-editor-mapping.tid | 2 + tiddlers/plugin-org-gtd-library.tid | 141 ++++ tiddlers/plugin-org-gtd-parser.tid | 76 +++ tiddlers/plugin-org-gtd-sidebar.tid | 13 + tiddlers/plugin-org-gtd-styles.tid | 159 +++++ tiddlers/plugin-task-board-edit-fields.tid | 38 -- tiddlers/plugin-task-board-styles.tid | 641 ------------------ tiddlers/plugin-task-board-view-fields.tid | 14 - tiddlers/plugin-task-board-widget.tid | 729 --------------------- ...72\213\346\203\205\347\234\213\346\235\277.tid" | 6 - 16 files changed, 1025 insertions(+), 1430 deletions(-) create mode 100644 tiddlers/GTD Agenda.tid create mode 100644 "tiddlers/Org GTD \344\275\277\347\224\250\350\257\264\346\230\216.tid" create mode 100644 tiddlers/plugin-org-gtd-agenda-widget.tid create mode 100644 tiddlers/plugin-org-gtd-codemirror.tid create mode 100644 tiddlers/plugin-org-gtd-content-type.tid create mode 100644 tiddlers/plugin-org-gtd-editor-mapping.tid create mode 100644 tiddlers/plugin-org-gtd-library.tid create mode 100644 tiddlers/plugin-org-gtd-parser.tid create mode 100644 tiddlers/plugin-org-gtd-sidebar.tid create mode 100644 tiddlers/plugin-org-gtd-styles.tid delete mode 100644 tiddlers/plugin-task-board-edit-fields.tid delete mode 100644 tiddlers/plugin-task-board-styles.tid delete mode 100644 tiddlers/plugin-task-board-view-fields.tid delete mode 100644 tiddlers/plugin-task-board-widget.tid delete mode 100644 "tiddlers/\344\272\213\346\203\205\347\234\213\346\235\277.tid" (limited to 'tiddlers') diff --git a/tiddlers/$__DefaultTiddlers.tid b/tiddlers/$__DefaultTiddlers.tid index 3f59296..eef92ca 100644 --- a/tiddlers/$__DefaultTiddlers.tid +++ b/tiddlers/$__DefaultTiddlers.tid @@ -1,9 +1,8 @@ created: 20131127215321439 -modified: 20260726125214612 +modified: 20260728080112830 title: $:/DefaultTiddlers type: text/vnd.tiddlywiki -事情看板 tag 日历 tiddlywiki \ No newline at end of file diff --git a/tiddlers/GTD Agenda.tid b/tiddlers/GTD Agenda.tid new file mode 100644 index 0000000..69614ea --- /dev/null +++ b/tiddlers/GTD Agenda.tid @@ -0,0 +1,5 @@ +created: 20260728000000000 +title: GTD Agenda +type: text/vnd.tiddlywiki + +<$org-gtd-agenda days="7"/> diff --git "a/tiddlers/Org GTD \344\275\277\347\224\250\350\257\264\346\230\216.tid" "b/tiddlers/Org GTD \344\275\277\347\224\250\350\257\264\346\230\216.tid" new file mode 100644 index 0000000..2a670ee --- /dev/null +++ "b/tiddlers/Org GTD \344\275\277\347\224\250\350\257\264\346\230\216.tid" @@ -0,0 +1,46 @@ +created: 20260728000000000 +modified: 20260728090815424 +tags: 🍀 +title: Org GTD 使用说明 +type: text/vnd.tiddlywiki + +!语法 + +在 [[GTD Agenda]] 中点击“新建 Org 文档”,使用以下子集: + +```org +* TODO 收集任务 :work: + SCHEDULED: <2026-07-29 Wed> + DEADLINE: <2026-07-31 Fri> +** NEXT 下一步行动 +** WAIT 等待他人 +* DONE 已完成事项 +``` + +!CodeMirror 快捷键 + +插件默认不绑定快捷键,避免与浏览器、桌面环境和 TiddlyWiki 冲突。普通按键(包括 Tab)保持 CodeMirror 原有行为。 + +需要快捷键时,编辑 [[$:/config/org-gtd/extraKeys]],按 CodeMirror 键名到命令名的格式填写 JSON。组合键内部使用连字符,例如 `Ctrl-Left`;空格表示连续按键,例如 `F8 T`。配置只在 Org 编辑器中生效;保存后重新打开编辑器。 + +例如,将 `Ctrl-Left` 绑定为提升层级,并将 `F8` 后按 `T` 绑定为循环任务状态: + +```json +{ + "Ctrl-Left": "orgGtdPromote", + "F8 T": "orgGtdCycleState" +} +``` + +可用命令: + +|命令 |操作 |h +|orgGtdCycleState |循环 TODO → NEXT → WAIT → DONE | +|orgGtdPromote / orgGtdDemote |提升 / 降低当前子树层级 | +|orgGtdMoveUp / orgGtdMoveDown |向上 / 向下移动当前子树 | +|orgGtdInsertSibling |新建同级 TODO 标题 | +|orgGtdToggleFold |折叠 / 展开当前子树 | + +保持配置内容为 `{}` 即可禁用全部 Org GTD 快捷键。 + +Agenda 显示逾期、今天、未来 7 天和无日期的 NEXT。点击 Agenda 事项会打开源 Org 文档并定位到对应标题。 diff --git a/tiddlers/plugin-org-gtd-agenda-widget.tid b/tiddlers/plugin-org-gtd-agenda-widget.tid new file mode 100644 index 0000000..4749564 --- /dev/null +++ b/tiddlers/plugin-org-gtd-agenda-widget.tid @@ -0,0 +1,229 @@ +title: $:/plugins/local/org-gtd/agenda-widget.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/org-gtd/agenda-widget.js +type: application/javascript +module-type: widget +\*/ +(function() { +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget, + org = require("$:/plugins/local/org-gtd/library.js"), + JUMP_STATE_TITLE = "$:/state/org-gtd/jump"; + +var OrgGtdAgendaWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); + this.parseCache = Object.create(null); +}; + +OrgGtdAgendaWidget.prototype = new Widget(); +OrgGtdAgendaWidget.prototype.constructor = OrgGtdAgendaWidget; + +function startOfDay(date) { + return new Date(date.getFullYear(),date.getMonth(),date.getDate()).getTime(); +} + +function earliestDate(task) { + var values = [task.scheduled,task.deadline].filter(function(value) { + return !isNaN(org.dateToTime(value)); + }); + values.sort(); + return values[0] || ""; +} + +OrgGtdAgendaWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + var container = this.document.createElement("div"); + container.className = "tc-org-agenda"; + parent.insertBefore(container,nextSibling); + this.domNodes.push(container); + this.containerNode = container; + this.renderAgenda(); +}; + +OrgGtdAgendaWidget.prototype.execute = function() { + this.days = Math.max(1,Number(this.getAttribute("days","7")) || 7); +}; + +OrgGtdAgendaWidget.prototype.createButton = function(label,className,handler) { + var button = this.document.createElement("button"); + button.type = "button"; + button.className = "tc-btn-invisible " + className; + button.textContent = label; + button.addEventListener("click",handler); + return button; +}; + +OrgGtdAgendaWidget.prototype.getTasks = function() { + var self = this, + tasks = [], + activeTitles = Object.create(null); + this.wiki.allTitles().forEach(function(title) { + var tiddler = self.wiki.getTiddler(title), + text, + cached, + parsed; + if(!tiddler || tiddler.fields.type !== "text/x-org" || tiddler.fields["draft.of"]) { + return; + } + activeTitles[title] = true; + text = tiddler.fields.text || ""; + cached = self.parseCache[title]; + parsed = cached && cached.text === text ? cached.parsed : org.parse(text); + self.parseCache[title] = {text: text,parsed: parsed}; + parsed.headings.forEach(function(heading) { + if(heading.state && heading.state !== "DONE" && heading.title) { + tasks.push({ + tiddler: title, + title: heading.title, + state: heading.state, + level: heading.level, + line: heading.sourceLine, + scheduled: heading.scheduled, + deadline: heading.deadline, + date: earliestDate(heading) + }); + } + }); + }); + Object.keys(this.parseCache).forEach(function(title) { + if(!activeTitles[title]) { + delete self.parseCache[title]; + } + }); + return tasks; +}; + +OrgGtdAgendaWidget.prototype.openTask = function(task,event) { + this.wiki.setText(JUMP_STATE_TITLE,"text",null,JSON.stringify({title: task.tiddler,line: task.line})); + this.dispatchEvent({ + type: "tm-edit-tiddler", + param: task.tiddler, + tiddlerTitle: task.tiddler, + navigateFromTitle: this.getVariable("storyTiddler"), + event: event + }); +}; + +OrgGtdAgendaWidget.prototype.renderTask = function(parent,task) { + var self = this, + button = this.createButton(task.title,"tc-org-agenda-task",function(event) { + self.openTask(task,event); + }), + state = this.document.createElement("span"), + source = this.document.createElement("span"), + meta = this.document.createElement("span"), + row = this.document.createElement("div"), + parts = []; + row.className = "tc-org-agenda-row"; + state.className = "tc-org-state tc-org-state-" + task.state.toLowerCase(); + state.textContent = task.state; + source.className = "tc-org-agenda-source"; + source.textContent = task.tiddler; + if(task.scheduled) { + parts.push("计划 " + task.scheduled); + } + if(task.deadline) { + parts.push("截止 " + task.deadline); + } + meta.className = "tc-org-agenda-meta"; + meta.textContent = parts.join(" · "); + row.appendChild(state); + row.appendChild(button); + row.appendChild(source); + if(parts.length) { + row.appendChild(meta); + } + parent.appendChild(row); +}; + +OrgGtdAgendaWidget.prototype.renderSection = function(title,tasks,className) { + var self = this, + section = this.document.createElement("section"), + heading = this.document.createElement("h2"), + count = this.document.createElement("span"); + section.className = "tc-org-agenda-section " + className; + heading.className = "tc-org-agenda-section-title"; + heading.textContent = title; + count.className = "tc-org-agenda-count"; + count.textContent = String(tasks.length); + heading.appendChild(count); + section.appendChild(heading); + if(tasks.length) { + tasks.forEach(function(task) { + self.renderTask(section,task); + }); + } else { + var empty = this.document.createElement("p"); + empty.className = "tc-org-agenda-empty"; + empty.textContent = "没有事项"; + section.appendChild(empty); + } + this.containerNode.appendChild(section); +}; + +OrgGtdAgendaWidget.prototype.renderAgenda = function() { + var self = this, + today = startOfDay(new Date()), + horizonDate = new Date(today), + horizon, + groups = {overdue: [],today: [],upcoming: [],next: []}, + toolbar = this.document.createElement("div"), + newButton; + horizonDate.setDate(horizonDate.getDate() + this.days); + horizon = horizonDate.getTime(); + newButton = this.createButton("+ 新建 Org 文档","tc-org-agenda-new",function(event) { + var title = self.wiki.generateNewTitle("GTD Inbox"); + self.dispatchEvent({ + type: "tm-new-tiddler", + paramObject: {title: title,type: "text/x-org",text: "* TODO "}, + navigateFromTitle: self.getVariable("storyTiddler"), + event: event + }); + }); + toolbar.className = "tc-org-agenda-toolbar"; + toolbar.appendChild(newButton); + this.containerNode.appendChild(toolbar); + this.getTasks().forEach(function(task) { + var time = org.dateToTime(task.date); + if(!isNaN(time)) { + if(time < today) { + groups.overdue.push(task); + } else if(time === today) { + groups.today.push(task); + } else if(time <= horizon) { + groups.upcoming.push(task); + } + } else if(task.state === "NEXT") { + groups.next.push(task); + } + }); + Object.keys(groups).forEach(function(key) { + groups[key].sort(function(left,right) { + return (left.date || "9999").localeCompare(right.date || "9999") || left.title.localeCompare(right.title,"zh-Hans"); + }); + }); + this.renderSection("逾期",groups.overdue,"tc-org-agenda-overdue"); + this.renderSection("今天",groups.today,"tc-org-agenda-today"); + this.renderSection("未来 " + this.days + " 天",groups.upcoming,"tc-org-agenda-upcoming"); + this.renderSection("无日期的下一步",groups.next,"tc-org-agenda-next"); +}; + +OrgGtdAgendaWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(Object.keys(changedAttributes).length || Object.keys(changedTiddlers).length) { + this.refreshSelf(); + return true; + } + return false; +}; + +exports["org-gtd-agenda"] = OrgGtdAgendaWidget; + +})(); diff --git a/tiddlers/plugin-org-gtd-codemirror.tid b/tiddlers/plugin-org-gtd-codemirror.tid new file mode 100644 index 0000000..09b3e6c --- /dev/null +++ b/tiddlers/plugin-org-gtd-codemirror.tid @@ -0,0 +1,341 @@ +title: $:/plugins/local/org-gtd/codemirror.js +type: application/javascript +module-type: codemirror + +/*\ +title: $:/plugins/local/org-gtd/codemirror.js +type: application/javascript +module-type: codemirror +\*/ +(function() { +"use strict"; + +var CodeMirror = require("$:/plugins/tiddlywiki/codemirror/lib/codemirror.js"), + org = require("$:/plugins/local/org-gtd/library.js"), + JUMP_STATE_TITLE = "$:/state/org-gtd/jump", + KEY_MAP_TITLE = "$:/config/org-gtd/extraKeys"; + +function isOrgEditor(cm) { + return cm.getMode().name === "org-gtd"; +} + +function readOrgGtdKeyMap(cm) { + var widget = cm.getOption("editWidget"), + text, + keyMap; + if(!widget || !widget.wiki) { + return null; + } + text = widget.wiki.getTiddlerText(KEY_MAP_TITLE,"{}"); + try { + keyMap = JSON.parse(text); + } catch(error) { + console.error("Org GTD shortcut configuration is invalid JSON",error); + return null; + } + if(!keyMap || typeof keyMap !== "object" || Array.isArray(keyMap) || Object.keys(keyMap).length === 0) { + return null; + } + try { + return CodeMirror.normalizeKeyMap(keyMap); + } catch(error) { + console.error("Org GTD shortcut configuration contains an invalid key name",error); + return null; + } +} + +function syncOrgGtdKeyMap(cm) { + var keyMap; + if(cm.state.orgGtdKeyMap) { + cm.removeKeyMap(cm.state.orgGtdKeyMap); + cm.state.orgGtdKeyMap = null; + } + if(!isOrgEditor(cm)) { + return; + } + keyMap = readOrgGtdKeyMap(cm); + if(keyMap) { + cm.addKeyMap(keyMap); + cm.state.orgGtdKeyMap = keyMap; + } +} + +function clearFolds(cm) { + var marks = cm.state.orgGtdFoldMarks || []; + marks.forEach(function(mark) { + if(mark.find()) { + mark.clear(); + } + }); + cm.state.orgGtdFoldMarks = []; +} + +function replaceDocument(cm,lines,cursorLine,cursorCh) { + var lastLine = cm.lastLine(), + lastCh = cm.getLine(lastLine).length; + clearFolds(cm); + cm.operation(function() { + cm.replaceRange(lines.join("\n"),CodeMirror.Pos(0,0),CodeMirror.Pos(lastLine,lastCh),"+org-gtd"); + cm.setCursor(CodeMirror.Pos(cursorLine,cursorCh || 0)); + }); +} + +function currentHeading(cm) { + var parsed = org.parse(cm.getValue()), + heading = org.headingAtLine(parsed,cm.getCursor().line); + return {parsed: parsed,heading: heading}; +} + +CodeMirror.defineMode("org-gtd",function() { + return { + startState: function() { + return {heading: false,planning: false}; + }, + token: function(stream,state) { + if(stream.sol()) { + state.heading = /^\*+\s+/.test(stream.string); + state.planning = /^\s*(?:SCHEDULED|DEADLINE):/.test(stream.string); + } + if(state.heading) { + if(stream.match(/^\*+\s+/)) { + return "header"; + } + if(stream.match(/^(?:TODO|NEXT|WAIT|DONE)\b/)) { + return "keyword"; + } + if(stream.match(/^\s+:[A-Za-z0-9_@#%+.-]+(?::[A-Za-z0-9_@#%+.-]+)*:\s*$/)) { + return "tag"; + } + } + if(state.planning) { + if(stream.match(/^\s*(?:SCHEDULED|DEADLINE):/)) { + return "def"; + } + if(stream.match(/^\s*<[0-9]{4}-[0-9]{2}-[0-9]{2}(?:\s+[^>]*)?>/)) { + return "atom"; + } + } + if(stream.sol() && stream.match(/^\s*#.*$/)) { + return "comment"; + } + stream.next(); + return null; + }, + lineComment: "# " + }; +}); + +CodeMirror.defineMIME("text/x-org","org-gtd"); + +CodeMirror.commands.orgGtdCycleState = function(cm) { + var cursor, + line, + result; + if(!isOrgEditor(cm)) { + return CodeMirror.Pass; + } + cursor = cm.getCursor(); + line = cm.getLine(cursor.line); + result = org.cycleState(line); + if(!result) { + return CodeMirror.Pass; + } + cm.replaceRange(result.line,CodeMirror.Pos(cursor.line,0),CodeMirror.Pos(cursor.line,line.length),"+org-gtd"); + cm.setCursor(CodeMirror.Pos(cursor.line,Math.min(cursor.ch + (result.line.length - line.length),result.line.length))); +}; + +function changeLevel(cm,direction) { + var context, + lines; + if(!isOrgEditor(cm)) { + return CodeMirror.Pass; + } + context = currentHeading(cm); + if(!context.heading || (direction < 0 && context.heading.level === 1)) { + return CodeMirror.Pass; + } + lines = context.parsed.lines.slice(0); + for(var index = context.heading.line; index <= context.heading.subtreeEndLine; index++) { + if(/^\*+\s+/.test(lines[index])) { + lines[index] = direction > 0 ? "*" + lines[index] : lines[index].substring(1); + } + } + replaceDocument(cm,lines,context.heading.line,Math.max(0,cm.getCursor().ch + direction)); +} + +CodeMirror.commands.orgGtdPromote = function(cm) { + return changeLevel(cm,-1); +}; + +CodeMirror.commands.orgGtdDemote = function(cm) { + return changeLevel(cm,1); +}; + +function moveSubtree(cm,direction) { + var context, + heading, + headings, + index, + other, + lines, + before, + current, + after, + newLine; + if(!isOrgEditor(cm)) { + return CodeMirror.Pass; + } + context = currentHeading(cm); + heading = context.heading; + if(!heading) { + return CodeMirror.Pass; + } + headings = context.parsed.headings; + index = headings.indexOf(heading); + if(direction < 0) { + for(index = index - 1; index >= 0; index--) { + if(headings[index].level < heading.level) { + return CodeMirror.Pass; + } + if(headings[index].level === heading.level) { + other = headings[index]; + break; + } + } + if(!other) { + return CodeMirror.Pass; + } + lines = context.parsed.lines; + before = lines.slice(0,other.line); + current = lines.slice(heading.line,heading.subtreeEndLine + 1); + after = lines.slice(other.line,heading.line).concat(lines.slice(heading.subtreeEndLine + 1)); + newLine = other.line; + replaceDocument(cm,before.concat(current,after),newLine,cm.getCursor().ch); + } else { + for(index = index + 1; index < headings.length; index++) { + if(headings[index].line <= heading.subtreeEndLine) { + continue; + } + if(headings[index].level < heading.level) { + return CodeMirror.Pass; + } + if(headings[index].level === heading.level) { + other = headings[index]; + break; + } + } + if(!other) { + return CodeMirror.Pass; + } + lines = context.parsed.lines; + before = lines.slice(0,heading.line); + current = lines.slice(heading.line,heading.subtreeEndLine + 1); + after = lines.slice(other.line,other.subtreeEndLine + 1); + newLine = heading.line + after.length; + replaceDocument(cm,before.concat(after,current,lines.slice(other.subtreeEndLine + 1)),newLine,cm.getCursor().ch); + } +} + +CodeMirror.commands.orgGtdMoveUp = function(cm) { + return moveSubtree(cm,-1); +}; + +CodeMirror.commands.orgGtdMoveDown = function(cm) { + return moveSubtree(cm,1); +}; + +CodeMirror.commands.orgGtdToggleFold = function(cm) { + var context, + heading, + marks, + existing, + marker, + mark; + if(!isOrgEditor(cm)) { + return CodeMirror.Pass; + } + context = currentHeading(cm); + heading = context.heading; + if(!heading || cm.getCursor().line !== heading.line || heading.subtreeEndLine === heading.line) { + return CodeMirror.Pass; + } + marks = cm.state.orgGtdFoldMarks = cm.state.orgGtdFoldMarks || []; + existing = marks.filter(function(item) { + var range = item.find(); + return range && range.from.line === heading.line; + })[0]; + if(existing) { + existing.clear(); + cm.state.orgGtdFoldMarks = marks.filter(function(item) {return item !== existing;}); + return; + } + marker = cm.getWrapperElement().ownerDocument.createElement("span"); + marker.className = "cm-org-gtd-fold-marker"; + marker.textContent = " ..."; + mark = cm.markText( + CodeMirror.Pos(heading.line,cm.getLine(heading.line).length), + CodeMirror.Pos(heading.subtreeEndLine,cm.getLine(heading.subtreeEndLine).length), + {collapsed: true,replacedWith: marker,atomic: true} + ); + marks.push(mark); + marker.addEventListener("click",function() { + mark.clear(); + cm.state.orgGtdFoldMarks = marks.filter(function(item) {return item !== mark;}); + }); +}; + +CodeMirror.commands.orgGtdInsertSibling = function(cm) { + var context, + heading, + lines, + newLine; + if(!isOrgEditor(cm)) { + return CodeMirror.Pass; + } + context = currentHeading(cm); + heading = context.heading; + if(!heading) { + return CodeMirror.Pass; + } + lines = context.parsed.lines.slice(0); + newLine = heading.subtreeEndLine + 1; + lines.splice(newLine,0,new Array(heading.level + 1).join("*") + " TODO "); + replaceDocument(cm,lines,newLine,heading.level + 6); +}; + +CodeMirror.defineInitHook(function(cm) { + var widget = cm.getOption("editWidget"), + request, + currentTitle, + tiddler, + originalTitle; + syncOrgGtdKeyMap(cm); + cm.on("optionChange",function(instance,option) { + if(option === "mode") { + syncOrgGtdKeyMap(instance); + } + }); + if(!widget || !widget.wiki) { + return; + } + request = $tw.utils.parseJSONSafe(widget.wiki.getTiddlerText(JUMP_STATE_TITLE,""),null); + if(!request) { + return; + } + currentTitle = widget.editTitle || widget.getVariable("currentTiddler",""); + tiddler = widget.wiki.getTiddler(currentTitle); + originalTitle = tiddler && tiddler.fields["draft.of"] ? tiddler.fields["draft.of"] : currentTitle; + if(originalTitle !== request.title) { + return; + } + setTimeout(function() { + var line = Math.max(0,Math.min(Number(request.line || 1) - 1,cm.lastLine())), + position = CodeMirror.Pos(line,0); + cm.setCursor(position); + cm.scrollIntoView(position,100); + cm.focus(); + widget.wiki.deleteTiddler(JUMP_STATE_TITLE); + },0); +}); + +})(); diff --git a/tiddlers/plugin-org-gtd-content-type.tid b/tiddlers/plugin-org-gtd-content-type.tid new file mode 100644 index 0000000..64d2c76 --- /dev/null +++ b/tiddlers/plugin-org-gtd-content-type.tid @@ -0,0 +1,12 @@ +title: $:/plugins/local/org-gtd/content-type.js +type: application/javascript +module-type: config + +/*\ +title: $:/plugins/local/org-gtd/content-type.js +type: application/javascript +module-type: config +\*/ +"use strict"; + +$tw.utils.registerFileType("text/x-org","utf8",".org"); diff --git a/tiddlers/plugin-org-gtd-editor-mapping.tid b/tiddlers/plugin-org-gtd-editor-mapping.tid new file mode 100644 index 0000000..251f966 --- /dev/null +++ b/tiddlers/plugin-org-gtd-editor-mapping.tid @@ -0,0 +1,2 @@ +title: $:/config/EditorTypeMappings/text/x-org +text: codemirror diff --git a/tiddlers/plugin-org-gtd-library.tid b/tiddlers/plugin-org-gtd-library.tid new file mode 100644 index 0000000..af41028 --- /dev/null +++ b/tiddlers/plugin-org-gtd-library.tid @@ -0,0 +1,141 @@ +title: $:/plugins/local/org-gtd/library.js +type: application/javascript +module-type: library + +/*\ +title: $:/plugins/local/org-gtd/library.js +type: application/javascript +module-type: library +\*/ +(function() { +"use strict"; + +var TODO_STATES = ["TODO","NEXT","WAIT","DONE"], + TODO_LOOKUP = Object.create(null), + HEADING_PATTERN = /^(\*+)\s+(?:(TODO|NEXT|WAIT|DONE)\s+)?(.*)$/, + TAG_PATTERN = /\s+:([A-Za-z0-9_@#%+.-]+(?::[A-Za-z0-9_@#%+.-]+)*):\s*$/, + DATE_PATTERN = /\b(SCHEDULED|DEADLINE):\s*<([0-9]{4}-[0-9]{2}-[0-9]{2})(?:\s+[^>]*)?>/g; + +TODO_STATES.forEach(function(state) { + TODO_LOOKUP[state] = true; +}); + +function parseHeading(line,lineIndex) { + var match = HEADING_PATTERN.exec(line), + tagMatch, + title, + tags = []; + if(!match) { + return null; + } + title = match[3]; + tagMatch = TAG_PATTERN.exec(title); + if(tagMatch) { + tags = tagMatch[1].split(":"); + title = title.substring(0,tagMatch.index).trim(); + } + return { + level: match[1].length, + state: match[2] || "", + title: title, + tags: tags, + line: lineIndex, + sourceLine: lineIndex + 1, + endLine: lineIndex, + scheduled: "", + deadline: "" + }; +} + +function parse(text) { + var lines = (text || "").split("\n"), + headings = [], + current = null; + lines.forEach(function(line,index) { + var heading = parseHeading(line,index), + match; + if(heading) { + if(current) { + current.endLine = index - 1; + } + headings.push(heading); + current = heading; + return; + } + if(current) { + DATE_PATTERN.lastIndex = 0; + while((match = DATE_PATTERN.exec(line)) !== null) { + if(match[1] === "SCHEDULED") { + current.scheduled = match[2]; + } else { + current.deadline = match[2]; + } + } + } + }); + if(current) { + current.endLine = lines.length - 1; + } + for(var index = 0; index < headings.length; index++) { + var heading = headings[index], + nextIndex = index + 1; + while(nextIndex < headings.length && headings[nextIndex].level > heading.level) { + nextIndex++; + } + heading.subtreeEndLine = nextIndex < headings.length ? headings[nextIndex].line - 1 : lines.length - 1; + } + return {lines: lines,headings: headings}; +} + +function headingAtLine(parsed,line) { + var result = null; + parsed.headings.forEach(function(heading) { + if(heading.line <= line && heading.subtreeEndLine >= line) { + if(!result || heading.level >= result.level) { + result = heading; + } + } + }); + return result; +} + +function cycleState(line) { + var heading = parseHeading(line,0), + stars, + nextState, + rest; + if(!heading) { + return null; + } + stars = new Array(heading.level + 1).join("*"); + if(heading.state) { + nextState = TODO_STATES[(TODO_STATES.indexOf(heading.state) + 1) % TODO_STATES.length]; + rest = line.replace(/^(\*+)\s+(?:TODO|NEXT|WAIT|DONE)\s+/,"$1 " + nextState + " "); + } else { + rest = line.replace(/^(\*+)\s+/,"$1 TODO "); + } + return {line: rest,state: nextState || "TODO",level: stars.length}; +} + +function dateToTime(value) { + var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || ""), + date; + if(!match) { + return NaN; + } + date = new Date(Number(match[1]),Number(match[2]) - 1,Number(match[3])); + if(date.getFullYear() !== Number(match[1]) || date.getMonth() !== Number(match[2]) - 1 || date.getDate() !== Number(match[3])) { + return NaN; + } + return date.getTime(); +} + +exports.TODO_STATES = TODO_STATES; +exports.TODO_LOOKUP = TODO_LOOKUP; +exports.parseHeading = parseHeading; +exports.parse = parse; +exports.headingAtLine = headingAtLine; +exports.cycleState = cycleState; +exports.dateToTime = dateToTime; + +})(); diff --git a/tiddlers/plugin-org-gtd-parser.tid b/tiddlers/plugin-org-gtd-parser.tid new file mode 100644 index 0000000..4aa4bd4 --- /dev/null +++ b/tiddlers/plugin-org-gtd-parser.tid @@ -0,0 +1,76 @@ +title: $:/plugins/local/org-gtd/parser.js +type: application/javascript +module-type: parser + +/*\ +title: $:/plugins/local/org-gtd/parser.js +type: application/javascript +module-type: parser +\*/ +(function() { +"use strict"; + +var org = require("$:/plugins/local/org-gtd/library.js"); + +function attr(value) { + return {type: "string",value: String(value)}; +} + +function text(value) { + return {type: "text",text: String(value)}; +} + +function element(tag,className,children,attributes) { + var attrs = attributes || {}; + if(className) { + attrs["class"] = attr(className); + } + return { + type: "element", + tag: tag, + attributes: attrs, + children: children || [], + isBlock: tag !== "span" + }; +} + +function headingNode(heading) { + var children = [], + level = Math.min(heading.level,6), + classes = "tc-org-heading tc-org-level-" + heading.level; + if(heading.state) { + classes += " tc-org-heading-" + heading.state.toLowerCase(); + children.push(element("span","tc-org-state tc-org-state-" + heading.state.toLowerCase(),[text(heading.state)])); + children.push(text(" ")); + } + children.push(text(heading.title)); + if(heading.tags.length) { + children.push(text(" ")); + children.push(element("span","tc-org-tags",[text(":" + heading.tags.join(":") + ":")])); + } + return element("h" + level,classes,children,{"data-org-line": attr(heading.sourceLine)}); +} + +function OrgParser(type,source,options) { + var parsed = org.parse(source || ""), + headingsByLine = Object.create(null), + tree = []; + parsed.headings.forEach(function(heading) { + headingsByLine[heading.line] = heading; + }); + parsed.lines.forEach(function(line,index) { + var heading = headingsByLine[index]; + if(heading) { + tree.push(headingNode(heading)); + } else if(/^\s*(?:SCHEDULED|DEADLINE):/.test(line)) { + tree.push(element("div","tc-org-planning",[text(line.trim())])); + } else if(line.trim()) { + tree.push(element("div","tc-org-body-line",[text(line)])); + } + }); + this.tree = options.parseAsInline ? [text(source || "")] : [element("div","tc-org-document",tree)]; +} + +exports["text/x-org"] = OrgParser; + +})(); diff --git a/tiddlers/plugin-org-gtd-sidebar.tid b/tiddlers/plugin-org-gtd-sidebar.tid new file mode 100644 index 0000000..467f946 --- /dev/null +++ b/tiddlers/plugin-org-gtd-sidebar.tid @@ -0,0 +1,13 @@ +caption: GTD +list-after: 🍀 +tags: $:/tags/SideBar +title: $:/plugins/local/org-gtd/sidebar +type: text/vnd.tiddlywiki + +
+<$link to="GTD Agenda">打开 Agenda + +<$list filter="[type[text/x-org]!has[draft.of]sort[title]]" emptyMessage="还没有 Org 文档。"> +
<$link/>
+ +
diff --git a/tiddlers/plugin-org-gtd-styles.tid b/tiddlers/plugin-org-gtd-styles.tid new file mode 100644 index 0000000..345a4ee --- /dev/null +++ b/tiddlers/plugin-org-gtd-styles.tid @@ -0,0 +1,159 @@ +title: $:/plugins/local/org-gtd/styles +tags: $:/tags/Stylesheet + +.tc-org-document { + --org-todo: #c74b50; + --org-next: <>; + --org-wait: #b27b2f; + --org-done: <>; +} + +.tc-org-heading { + margin: 1.15rem 0 0.45rem; + line-height: 1.35; +} + +.tc-org-heading-done { + color: <>; + text-decoration: line-through; +} + +.tc-org-state { + display: inline-block; + padding: 0.1rem 0.4rem; + font-size: 0.72rem; + font-weight: 700; + line-height: 1.25; + letter-spacing: 0.03em; + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.tc-org-state-todo { color: #c74b50; } +.tc-org-state-next { color: <>; } +.tc-org-state-wait { color: #b27b2f; } +.tc-org-state-done { color: <>; } + +.tc-org-tags { + color: <>; + font-size: 0.78em; + font-weight: 500; +} + +.tc-org-planning { + margin: 0.25rem 0 0.25rem 1rem; + color: <>; + font-family: monospace; + font-size: 0.88em; +} + +.tc-org-body-line { + margin: 0.2rem 0; + white-space: pre-wrap; +} + +.cm-org-gtd-fold-marker { + color: <>; + cursor: pointer; + font-weight: 700; +} + +.tc-org-agenda-toolbar { + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; +} + +.tc-org-agenda-new { + padding: 0.45rem 0.8rem; + color: <>; + background: <>; + border-radius: 0.35rem; +} + +.tc-org-agenda-section { + margin: 0 0 1.5rem; + border-top: 2px solid <>; +} + +.tc-org-agenda-overdue { + border-top-color: #c74b50; +} + +.tc-org-agenda-today { + border-top-color: <>; +} + +.tc-org-agenda-section-title { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + padding: 0.6rem 0 0.45rem; + font-size: 1rem; +} + +.tc-org-agenda-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.45rem; + height: 1.45rem; + padding: 0 0.35rem; + color: <>; + background: <>; + border-radius: 999px; + font-size: 0.75rem; +} + +.tc-org-agenda-row { + display: grid; + grid-template-columns: auto minmax(8rem,1fr) auto; + align-items: center; + gap: 0.45rem 0.7rem; + padding: 0.5rem 0; + border-bottom: 1px solid <>; +} + +.tc-org-agenda-task { + min-width: 0; + color: <>; + font-weight: 600; + text-align: left; +} + +.tc-org-agenda-task:hover, +.tc-org-agenda-task:focus { + color: <>; + text-decoration: underline; +} + +.tc-org-agenda-source, +.tc-org-agenda-meta, +.tc-org-agenda-empty { + color: <>; + font-size: 0.82rem; +} + +.tc-org-agenda-meta { + grid-column: 2 / -1; +} + +.tc-org-agenda-empty { + margin: 0; + padding: 0.6rem 0; +} + +@media (max-width: 600px) { + .tc-org-agenda-row { + grid-template-columns: auto minmax(0,1fr); + } + + .tc-org-agenda-source { + grid-column: 2; + } + + .tc-org-agenda-meta { + grid-column: 2; + } +} diff --git a/tiddlers/plugin-task-board-edit-fields.tid b/tiddlers/plugin-task-board-edit-fields.tid deleted file mode 100644 index dd8a351..0000000 --- a/tiddlers/plugin-task-board-edit-fields.tid +++ /dev/null @@ -1,38 +0,0 @@ -title: $:/plugins/local/task-board/edit-fields -tags: $:/tags/EditTemplate -list-before: $:/core/ui/EditTemplate/body -type: text/vnd.tiddlywiki - -<$list filter="[tag[事情]]"> -
- - - - - -
- diff --git a/tiddlers/plugin-task-board-styles.tid b/tiddlers/plugin-task-board-styles.tid deleted file mode 100644 index f3c81b1..0000000 --- a/tiddlers/plugin-task-board-styles.tid +++ /dev/null @@ -1,641 +0,0 @@ -title: $:/plugins/local/task-board/styles -tags: $:/tags/Stylesheet - -\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline - -.tc-task-board { - width: 100%; - --task-p1: #d84b4b; - --task-p2: <>; - --task-p3: #5b9a68; -} - -.tc-task-board-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1.25rem; -} - -.tc-task-board-new { - padding: 0.55rem 1rem; - color: <>; - font-weight: 700; - background: <>; - border-radius: 0.45rem; -} - -.tc-task-board-switcher { - display: grid; - grid-template-columns: repeat(4,auto); - border: 1px solid <>; - border-radius: 999px; - overflow: hidden; -} - -.tc-task-board-view-button { - padding: 0.45rem 0.8rem; - border-right: 1px solid <>; -} - -.tc-task-board-view-button:last-child { - border-right: 0; -} - -.tc-task-board-view-button.tc-selected { - color: <>; - background: <>; -} - -.tc-task-kanban { - display: grid; - grid-template-columns: repeat(4,minmax(15rem,1fr)); - gap: 0.75rem; - padding-bottom: 0.5rem; - overflow-x: auto; - scroll-snap-type: x proximity; -} - -.tc-task-kanban-column { - min-height: 18rem; - padding: 0.7rem; - background: <>; - border: 1px solid <>; - border-radius: 0.65rem; - scroll-snap-align: start; - transition: box-shadow 120ms ease; -} - -.tc-task-kanban-column.tc-drag-over { - box-shadow: inset 0 0 0 3px <>; -} - -.tc-task-kanban-heading { - margin: 0 0 0.7rem; - font-size: 1rem; -} - -.tc-task-kanban-cards { - display: grid; - gap: 0.55rem; -} - -.tc-task-card { - padding: 0.7rem; - background: <>; - border: 1px solid <>; - border-radius: 0.5rem; - box-shadow: 0 1px 2px rgba(0,0,0,0.08); - cursor: grab; -} - -.tc-task-card.tc-dragging { - opacity: 0.45; -} - -.tc-task-card-top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 0.5rem; -} - -.tc-task-card-title, -.tc-task-list-link, -.tc-task-gantt-task-link, -.tc-task-unscheduled-link { - color: <>; - font-weight: 650; - text-align: left; -} - -.tc-task-card-title:hover, -.tc-task-list-link:hover, -.tc-task-gantt-task-link:hover, -.tc-task-unscheduled-link:hover { - text-decoration: underline; -} - -.tc-task-priority { - display: inline-flex; - flex: 0 0 auto; - padding: 0.12rem 0.38rem; - color: #fff; - font-size: 0.72rem; - font-weight: 700; - border-radius: 999px; -} - -.tc-task-priority-p1 {background: var(--task-p1);} -.tc-task-priority-p2 {background: var(--task-p2);} -.tc-task-priority-p3 {background: var(--task-p3);} - -.tc-task-card-dates, -.tc-task-card-tags { - margin-top: 0.45rem; - color: <>; - font-size: 0.78rem; - line-height: 1.45; -} - -.tc-task-issues { - display: block; - margin-top: 0.4rem; - color: #b33a3a; - font-size: 0.75rem; -} - -.tc-task-empty-column, -.tc-task-empty { - padding: 1.25rem; - color: <>; - text-align: center; -} - -.tc-task-list-wrapper { - width: 100%; - overflow-x: auto; -} - -.tc-task-list { - width: 100%; - border-collapse: collapse; -} - -.tc-task-list th, -.tc-task-list td { - padding: 0.65rem; - text-align: left; - vertical-align: top; - border-bottom: 1px solid <>; -} - -.tc-task-list th { - white-space: nowrap; - background: <>; -} - -.tc-task-list-title { - min-width: 12rem; -} - -.tc-task-list-row.tc-task-invalid { - background: rgba(216,75,75,0.08); -} - -.tc-task-calendar-controls { - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - margin-bottom: 0.8rem; -} - -.tc-task-calendar-title { - min-width: 9rem; - margin: 0; - text-align: center; -} - -.tc-task-month-button { - min-height: 2.25rem; - padding: 0.35rem 0.65rem; - border: 1px solid <>; - border-radius: 999px; -} - -.tc-task-calendar-grid { - border: 1px solid <>; - border-radius: 0.5rem; - overflow: hidden; -} - -.tc-task-calendar-header, -.tc-task-calendar-week { - display: grid; - grid-template-columns: repeat(7,minmax(0,1fr)); -} - -.tc-task-calendar-week { - position: relative; - gap: 1px; - background: <>; - border-top: 1px solid <>; -} - -.tc-task-calendar-weekday { - padding: 0.55rem 0.2rem; - font-weight: 700; - text-align: center; - background: <>; -} - -.tc-task-calendar-day { - z-index: 0; - min-width: 0; - min-height: 8rem; - padding: 0.35rem; - background: <>; -} - -.tc-task-calendar-blank { - background: <>; -} - -.tc-task-calendar-day-number { - display: flex; - align-items: center; - justify-content: center; - width: 1.65rem; - height: 1.65rem; - margin-bottom: 0.3rem; - font-weight: 700; - border-radius: 50%; -} - -.tc-task-calendar-today .tc-task-calendar-day-number { - color: <>; - background: <>; -} - -.tc-task-calendar-range, -.tc-task-calendar-point, -.tc-task-calendar-milestone { - z-index: 1; - align-self: center; - display: block; - width: 100%; - min-width: 0; - padding: 0.2rem 0.3rem; - color: #fff; - font-size: 0.72rem; - line-height: 1.2; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.tc-task-calendar-range { - margin-left: -1px; - margin-right: -1px; - background: var(--task-p2); -} - -.tc-task-calendar-range[data-priority="P1"] {background: var(--task-p1);} -.tc-task-calendar-range[data-priority="P3"] {background: var(--task-p3);} -.tc-task-calendar-point[data-priority="P1"] {background: var(--task-p1);} -.tc-task-calendar-point[data-priority="P2"] {background: var(--task-p2);} -.tc-task-calendar-point[data-priority="P3"] {background: var(--task-p3);} -.tc-task-calendar-range.tc-range-start {border-radius: 0.35rem 0 0 0.35rem;} -.tc-task-calendar-range.tc-range-end {border-radius: 0 0.35rem 0.35rem 0;} -.tc-task-calendar-range.tc-range-start.tc-range-end {border-radius: 0.35rem;} - -.tc-task-calendar-point { - border-radius: 0.35rem; -} - -.tc-task-calendar-milestone { - color: #fff; - background: #7656b5; - border-radius: 0.3rem; -} - -.tc-task-unscheduled { - margin-top: 1rem; - padding: 0.8rem; - background: <>; - border-radius: 0.5rem; -} - -.tc-task-unscheduled h3 { - margin: 0 0 0.5rem; - font-size: 1rem; -} - -.tc-task-unscheduled-list { - display: flex; - flex-wrap: wrap; - gap: 0.45rem; -} - -.tc-task-unscheduled-link { - padding: 0.3rem 0.55rem; - background: <>; - border-radius: 0.35rem; -} - -.tc-task-gantt-scroll { - width: 100%; - overflow: auto; - border: 1px solid <>; - border-radius: 0.5rem; -} - -.tc-task-gantt { - width: max-content; - min-width: 100%; - --task-gantt-label-width: 18rem; -} - -.tc-task-gantt-row { - display: grid; - grid-template-columns: var(--task-gantt-label-width) var(--task-gantt-width); - min-height: 3.25rem; - border-bottom: 1px solid <>; -} - -.tc-task-gantt-row:last-child { - border-bottom: 0; -} - -.tc-task-gantt-label { - position: sticky; - left: 0; - z-index: 2; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.45rem; - padding: 0.45rem 0.6rem; - background: <>; - border-right: 1px solid <>; -} - -.tc-task-gantt-header { - position: sticky; - top: 0; - z-index: 4; - min-height: 4.5rem; -} - -.tc-task-gantt-header .tc-task-gantt-label { - z-index: 5; - font-weight: 700; - background: <>; -} - -.tc-task-gantt-timeline { - position: relative; - width: var(--task-gantt-width); - background-image: repeating-linear-gradient(to right,transparent 0,transparent 29px,<> 29px,<> 30px); -} - -.tc-task-gantt-header-timeline { - display: grid; - grid-template-rows: 2rem 2.5rem; - background: <>; -} - -.tc-task-gantt-months { - display: flex; - width: var(--task-gantt-date-width); - height: 2rem; -} - -.tc-task-gantt-month-segment { - position: relative; - flex: 0 0 auto; - height: 2rem; - border-left: 2px solid <>; -} - -.tc-task-gantt-month-label { - position: sticky; - left: calc(var(--task-gantt-label-width) + 0.5rem); - z-index: 1; - display: inline-flex; - align-items: center; - height: 2rem; - padding: 0 0.45rem; - font-size: 0.78rem; - font-weight: 700; - white-space: nowrap; - background: <>; -} - -.tc-task-gantt-days { - display: flex; - width: var(--task-gantt-date-width); - height: 2.5rem; - border-top: 1px solid <>; -} - -.tc-task-gantt-day { - flex: 0 0 auto; - padding-top: 0.65rem; - font-size: 0.72rem; - text-align: center; - white-space: nowrap; - border-right: 1px solid <>; -} - -.tc-task-gantt-bar { - position: absolute; - top: 0.8rem; - height: 1.65rem; - padding: 0; - background: var(--task-p2); - border-radius: 0.35rem; -} - -.tc-task-gantt-bar[data-priority="P1"] {background: var(--task-p1);} -.tc-task-gantt-bar[data-priority="P3"] {background: var(--task-p3);} - -.tc-task-gantt-bar-label { - position: absolute; - top: 0.8rem; - z-index: 2; - display: inline-flex; - align-items: center; - height: 1.65rem; - padding: 0 0.25rem; - color: <>; - font-size: 0.78rem; - font-weight: 650; - white-space: nowrap; -} - -.tc-task-gantt-bar-label:hover, -.tc-task-gantt-bar-label:focus { - text-decoration: underline; -} - -.tc-task-gantt-due, -.tc-task-gantt-end { - position: absolute; - top: 0.95rem; - z-index: 2; - padding: 0; - transform: translateX(-50%); - font-size: 1rem; -} - -.tc-task-gantt-due {color: #b33a3a;} -.tc-task-gantt-end {color: var(--task-p2);} - -.tc-task-gantt-today-line { - position: absolute; - top: 0; - bottom: 0; - z-index: 1; - width: 2px; - background: #b33a3a; - pointer-events: none; -} - -.tc-task-field-panel { - display: grid; - grid-template-columns: repeat(5,minmax(0,1fr)); - gap: 0.6rem; - margin: 0.7rem 0; - padding: 0.75rem; - background: <>; - border: 1px solid <>; - border-radius: 0.5rem; -} - -.tc-task-field { - display: grid; - gap: 0.3rem; -} - -.tc-task-field > span, -.tc-task-view-field span { - color: <>; - font-size: 0.76rem; - font-weight: 650; -} - -.tc-task-field-control { - width: 100%; - min-width: 0; - min-height: 2.25rem; -} - -.tc-task-view-field { - display: grid; - gap: 0.2rem; -} - -@media (max-width: 700px) { - .tc-task-board-toolbar { - display: grid; - gap: 0.65rem; - } - - .tc-task-board-new { - min-height: 2.75rem; - } - - .tc-task-board-switcher { - grid-template-columns: repeat(4,minmax(0,1fr)); - border-radius: 0.5rem; - } - - .tc-task-board-view-button { - min-width: 0; - padding: 0.5rem 0.15rem; - } - - .tc-task-kanban { - grid-template-columns: repeat(4,minmax(82vw,82vw)); - } - - .tc-task-list, - .tc-task-list tbody, - .tc-task-list tr, - .tc-task-list td { - display: block; - } - - .tc-task-list thead { - display: none; - } - - .tc-task-list-row { - margin-bottom: 0.7rem; - padding: 0.65rem; - border: 1px solid <>; - border-radius: 0.5rem; - } - - .tc-task-list td { - display: grid; - grid-template-columns: 4.5rem minmax(0,1fr); - padding: 0.3rem; - border: 0; - } - - .tc-task-list td::before { - content: attr(data-label); - color: <>; - font-size: 0.78rem; - font-weight: 650; - } - - .tc-task-list-title { - min-width: 0; - } - - .tc-task-calendar-controls { - display: grid; - grid-template-columns: repeat(3,minmax(0,1fr)); - } - - .tc-task-calendar-title { - grid-column: 1 / -1; - grid-row: 1; - justify-self: center; - } - - .tc-task-month-button { - grid-row: 2; - min-height: 2.75rem; - padding: 0.3rem 0.15rem; - } - - .tc-task-calendar-weekday { - padding: 0.4rem 0; - font-size: 0.75rem; - } - - .tc-task-calendar-day { - min-height: 6.5rem; - padding: 0.15rem; - } - - .tc-task-calendar-day-number { - width: 1.5rem; - height: 1.5rem; - font-size: 0.78rem; - } - - .tc-task-calendar-range, - .tc-task-calendar-point, - .tc-task-calendar-milestone { - padding: 0.16rem 0.1rem; - font-size: 0.62rem; - text-align: center; - } - - .tc-task-gantt-row { - --task-gantt-label-width: 9rem; - } - - .tc-task-gantt-label { - padding: 0.4rem; - } - - .tc-task-gantt-task-link { - max-width: 6rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .tc-task-field-panel { - grid-template-columns: 1fr; - } -} diff --git a/tiddlers/plugin-task-board-view-fields.tid b/tiddlers/plugin-task-board-view-fields.tid deleted file mode 100644 index 5a2a75a..0000000 --- a/tiddlers/plugin-task-board-view-fields.tid +++ /dev/null @@ -1,14 +0,0 @@ -title: $:/plugins/local/task-board/view-fields -tags: $:/tags/ViewTemplate -list-before: $:/core/ui/ViewTemplate/body -type: text/vnd.tiddlywiki - -<$list filter="[tag[事情]]"> -
-
优先级<$view field="priority"/>
-
状态<$view field="status"/>
-
开始<$view field="start"/>
-
计划结束<$view field="end"/>
-
截止<$view field="due"/>
-
- diff --git a/tiddlers/plugin-task-board-widget.tid b/tiddlers/plugin-task-board-widget.tid deleted file mode 100644 index 397811b..0000000 --- a/tiddlers/plugin-task-board-widget.tid +++ /dev/null @@ -1,729 +0,0 @@ -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; - -})(); diff --git "a/tiddlers/\344\272\213\346\203\205\347\234\213\346\235\277.tid" "b/tiddlers/\344\272\213\346\203\205\347\234\213\346\235\277.tid" deleted file mode 100644 index 51e661d..0000000 --- "a/tiddlers/\344\272\213\346\203\205\347\234\213\346\235\277.tid" +++ /dev/null @@ -1,6 +0,0 @@ -created: 20260726132452393 -modified: 20260726144542413 -title: 事情看板 -type: text/vnd.tiddlywiki - -<$task-board tag="事情"/> -- cgit v1.2.3