aboutsummaryrefslogtreecommitdiffstats
path: root/tiddlers
diff options
context:
space:
mode:
authorshu <nvcsadjkojf@outlook.com>2026-07-28 19:16:16 +0800
committershu <nvcsadjkojf@outlook.com>2026-07-28 19:16:16 +0800
commit8efba22ed34b6eeb28b5f410177e5a1545b5d798 (patch)
treeb4e751ec9a0940659bc50651974e96c57f6a66d8 /tiddlers
parent1e2031e8850fa91ed9e3b5cd11f7a5b01bbd999e (diff)
用 Org GTD 工作流替换事情看板
注册 text/x-org 内容类型和 CodeMirror 编辑器,支持 TODO 状态流转、层级调整、子树移动与折叠。 新增 Org 文档解析、Agenda 日期分组、源任务定位、侧栏入口和响应式样式,并补充中文使用说明。 快捷键改为仅在 Org 编辑器中按配置加载,默认不绑定;无效 JSON 或键名只禁用自定义键位,不影响编辑器启动。 移除旧事情看板的字段模板、组件、样式和默认入口,统一使用 Org GTD 工作流。
Diffstat (limited to 'tiddlers')
-rw-r--r--tiddlers/$__DefaultTiddlers.tid3
-rw-r--r--tiddlers/GTD Agenda.tid5
-rw-r--r--tiddlers/Org GTD 使用说明.tid46
-rw-r--r--tiddlers/plugin-org-gtd-agenda-widget.tid229
-rw-r--r--tiddlers/plugin-org-gtd-codemirror.tid341
-rw-r--r--tiddlers/plugin-org-gtd-content-type.tid12
-rw-r--r--tiddlers/plugin-org-gtd-editor-mapping.tid2
-rw-r--r--tiddlers/plugin-org-gtd-library.tid141
-rw-r--r--tiddlers/plugin-org-gtd-parser.tid76
-rw-r--r--tiddlers/plugin-org-gtd-sidebar.tid13
-rw-r--r--tiddlers/plugin-org-gtd-styles.tid159
-rw-r--r--tiddlers/plugin-task-board-edit-fields.tid38
-rw-r--r--tiddlers/plugin-task-board-styles.tid641
-rw-r--r--tiddlers/plugin-task-board-view-fields.tid14
-rw-r--r--tiddlers/plugin-task-board-widget.tid729
-rw-r--r--tiddlers/事情看板.tid6
16 files changed, 1025 insertions, 1430 deletions
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 使用说明.tid b/tiddlers/Org GTD 使用说明.tid
new file mode 100644
index 0000000..2a670ee
--- /dev/null
+++ b/tiddlers/Org GTD 使用说明.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
+
+<div class="tc-sidebar-tab-open">
+<$link to="GTD Agenda">打开 Agenda</$link>
+
+<$list filter="[type[text/x-org]!has[draft.of]sort[title]]" emptyMessage="还没有 Org 文档。">
+<div class="tc-sidebar-tab-open-item"><$link/></div>
+</$list>
+</div>
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: <<colour primary>>;
+ --org-wait: #b27b2f;
+ --org-done: <<colour muted-foreground>>;
+}
+
+.tc-org-heading {
+ margin: 1.15rem 0 0.45rem;
+ line-height: 1.35;
+}
+
+.tc-org-heading-done {
+ color: <<colour muted-foreground>>;
+ 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: <<colour primary>>; }
+.tc-org-state-wait { color: #b27b2f; }
+.tc-org-state-done { color: <<colour muted-foreground>>; }
+
+.tc-org-tags {
+ color: <<colour muted-foreground>>;
+ font-size: 0.78em;
+ font-weight: 500;
+}
+
+.tc-org-planning {
+ margin: 0.25rem 0 0.25rem 1rem;
+ color: <<colour muted-foreground>>;
+ 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: <<colour primary>>;
+ 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: <<colour background>>;
+ background: <<colour primary>>;
+ border-radius: 0.35rem;
+}
+
+.tc-org-agenda-section {
+ margin: 0 0 1.5rem;
+ border-top: 2px solid <<colour tiddler-border>>;
+}
+
+.tc-org-agenda-overdue {
+ border-top-color: #c74b50;
+}
+
+.tc-org-agenda-today {
+ border-top-color: <<colour primary>>;
+}
+
+.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: <<colour muted-foreground>>;
+ background: <<colour sidebar-tab-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 <<colour tiddler-border>>;
+}
+
+.tc-org-agenda-task {
+ min-width: 0;
+ color: <<colour foreground>>;
+ font-weight: 600;
+ text-align: left;
+}
+
+.tc-org-agenda-task:hover,
+.tc-org-agenda-task:focus {
+ color: <<colour primary>>;
+ text-decoration: underline;
+}
+
+.tc-org-agenda-source,
+.tc-org-agenda-meta,
+.tc-org-agenda-empty {
+ color: <<colour muted-foreground>>;
+ 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="[<currentTiddler>tag[事情]]">
-<div class="tc-task-field-panel tc-task-edit-fields">
- <label class="tc-task-field">
- <span>优先级</span>
- <$select tiddler=<<currentTiddler>> field="priority" class="tc-task-field-control" aria-label="优先级">
- <option value="P1">P1 · 高</option>
- <option value="P2">P2 · 中</option>
- <option value="P3">P3 · 低</option>
- </$select>
- </label>
- <label class="tc-task-field">
- <span>状态</span>
- <$select tiddler=<<currentTiddler>> field="status" class="tc-task-field-control" aria-label="状态">
- <option value="待处理">待处理</option>
- <option value="进行中">进行中</option>
- <option value="等待">等待</option>
- <option value="完成">完成</option>
- </$select>
- </label>
- <label class="tc-task-field">
- <span>开始日期</span>
- <$edit-text tiddler=<<currentTiddler>> field="start" tag="input" type="date" class="tc-edit-texteditor tc-task-field-control" aria-label="开始日期"/>
- </label>
- <label class="tc-task-field">
- <span>计划结束</span>
- <$edit-text tiddler=<<currentTiddler>> field="end" tag="input" type="date" class="tc-edit-texteditor tc-task-field-control" aria-label="计划结束日期"/>
- </label>
- <label class="tc-task-field">
- <span>截止日期</span>
- <$edit-text tiddler=<<currentTiddler>> field="due" tag="input" type="date" class="tc-edit-texteditor tc-task-field-control" aria-label="截止日期"/>
- </label>
-</div>
-</$list>
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: <<colour primary>>;
- --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: <<colour background>>;
- font-weight: 700;
- background: <<colour primary>>;
- border-radius: 0.45rem;
-}
-
-.tc-task-board-switcher {
- display: grid;
- grid-template-columns: repeat(4,auto);
- border: 1px solid <<colour table-border>>;
- border-radius: 999px;
- overflow: hidden;
-}
-
-.tc-task-board-view-button {
- padding: 0.45rem 0.8rem;
- border-right: 1px solid <<colour table-border>>;
-}
-
-.tc-task-board-view-button:last-child {
- border-right: 0;
-}
-
-.tc-task-board-view-button.tc-selected {
- color: <<colour background>>;
- background: <<colour primary>>;
-}
-
-.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: <<colour sidebar-tab-background>>;
- border: 1px solid <<colour table-border>>;
- 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 <<colour primary>>;
-}
-
-.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: <<colour tiddler-background>>;
- border: 1px solid <<colour table-border>>;
- 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: <<colour tiddler-link-foreground>>;
- 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: <<colour muted-foreground>>;
- 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: <<colour muted-foreground>>;
- 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 <<colour table-border>>;
-}
-
-.tc-task-list th {
- white-space: nowrap;
- background: <<colour table-header-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 <<colour table-border>>;
- border-radius: 999px;
-}
-
-.tc-task-calendar-grid {
- border: 1px solid <<colour table-border>>;
- 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: <<colour table-border>>;
- border-top: 1px solid <<colour table-border>>;
-}
-
-.tc-task-calendar-weekday {
- padding: 0.55rem 0.2rem;
- font-weight: 700;
- text-align: center;
- background: <<colour table-header-background>>;
-}
-
-.tc-task-calendar-day {
- z-index: 0;
- min-width: 0;
- min-height: 8rem;
- padding: 0.35rem;
- background: <<colour tiddler-background>>;
-}
-
-.tc-task-calendar-blank {
- background: <<colour sidebar-tab-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: <<colour background>>;
- background: <<colour primary>>;
-}
-
-.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: <<colour sidebar-tab-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: <<colour tiddler-background>>;
- border-radius: 0.35rem;
-}
-
-.tc-task-gantt-scroll {
- width: 100%;
- overflow: auto;
- border: 1px solid <<colour table-border>>;
- 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 <<colour table-border>>;
-}
-
-.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: <<colour tiddler-background>>;
- border-right: 1px solid <<colour table-border>>;
-}
-
-.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: <<colour table-header-background>>;
-}
-
-.tc-task-gantt-timeline {
- position: relative;
- width: var(--task-gantt-width);
- background-image: repeating-linear-gradient(to right,transparent 0,transparent 29px,<<colour table-border>> 29px,<<colour table-border>> 30px);
-}
-
-.tc-task-gantt-header-timeline {
- display: grid;
- grid-template-rows: 2rem 2.5rem;
- background: <<colour table-header-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 <<colour primary>>;
-}
-
-.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: <<colour table-header-background>>;
-}
-
-.tc-task-gantt-days {
- display: flex;
- width: var(--task-gantt-date-width);
- height: 2.5rem;
- border-top: 1px solid <<colour table-border>>;
-}
-
-.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 <<colour table-border>>;
-}
-
-.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: <<colour tiddler-link-foreground>>;
- 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: <<colour sidebar-tab-background>>;
- border: 1px solid <<colour table-border>>;
- border-radius: 0.5rem;
-}
-
-.tc-task-field {
- display: grid;
- gap: 0.3rem;
-}
-
-.tc-task-field > span,
-.tc-task-view-field span {
- color: <<colour muted-foreground>>;
- 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 <<colour table-border>>;
- 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: <<colour muted-foreground>>;
- 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="[<currentTiddler>tag[事情]]">
-<div class="tc-task-field-panel tc-task-view-fields">
- <div class="tc-task-view-field"><span>优先级</span><strong><$view field="priority"/></strong></div>
- <div class="tc-task-view-field"><span>状态</span><strong><$view field="status"/></strong></div>
- <div class="tc-task-view-field"><span>开始</span><strong><$view field="start"/></strong></div>
- <div class="tc-task-view-field"><span>计划结束</span><strong><$view field="end"/></strong></div>
- <div class="tc-task-view-field"><span>截止</span><strong><$view field="due"/></strong></div>
-</div>
-</$list>
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/事情看板.tid b/tiddlers/事情看板.tid
deleted file mode 100644
index 51e661d..0000000
--- a/tiddlers/事情看板.tid
+++ /dev/null
@@ -1,6 +0,0 @@
-created: 20260726132452393
-modified: 20260726144542413
-title: 事情看板
-type: text/vnd.tiddlywiki
-
-<$task-board tag="事情"/>