From 338cc101f127e4841689c1f81227ee033e57ad28 Mon Sep 17 00:00:00 2001 From: shu Date: Sun, 26 Jul 2026 22:42:45 +0800 Subject: Add task board and organize local plugins --- tiddlers/copy-codeblock.tid | 72 -- tiddlers/current-tiddler-toc-widget.tid | 284 -------- tiddlers/files-list-api.tid | 132 ---- tiddlers/highlight-caddyfile.tid | 65 -- tiddlers/highlight-json5.tid | 59 -- tiddlers/highlight-tiddlywiki.tid | 191 ------ tiddlers/journal-calendar-widget.tid | 333 ---------- tiddlers/plugin-copy-codeblock-widget.tid | 72 ++ tiddlers/plugin-current-tiddler-toc-sidebar.tid | 69 ++ tiddlers/plugin-current-tiddler-toc-widget.tid | 284 ++++++++ tiddlers/plugin-files-api-route.tid | 132 ++++ tiddlers/plugin-highlight-caddyfile.tid | 65 ++ tiddlers/plugin-highlight-json5.tid | 59 ++ tiddlers/plugin-highlight-tiddlywiki.tid | 191 ++++++ tiddlers/plugin-journal-calendar-widget.tid | 333 ++++++++++ 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 +++++++++++++++++++++ tiddlers/sidebar-current-tiddler-toc.tid | 69 -- ...72\213\346\203\205\347\234\213\346\235\277.tid" | 6 + 21 files changed, 2633 insertions(+), 1205 deletions(-) delete mode 100644 tiddlers/copy-codeblock.tid delete mode 100644 tiddlers/current-tiddler-toc-widget.tid delete mode 100644 tiddlers/files-list-api.tid delete mode 100644 tiddlers/highlight-caddyfile.tid delete mode 100644 tiddlers/highlight-json5.tid delete mode 100644 tiddlers/highlight-tiddlywiki.tid delete mode 100644 tiddlers/journal-calendar-widget.tid create mode 100644 tiddlers/plugin-copy-codeblock-widget.tid create mode 100644 tiddlers/plugin-current-tiddler-toc-sidebar.tid create mode 100644 tiddlers/plugin-current-tiddler-toc-widget.tid create mode 100644 tiddlers/plugin-files-api-route.tid create mode 100644 tiddlers/plugin-highlight-caddyfile.tid create mode 100644 tiddlers/plugin-highlight-json5.tid create mode 100644 tiddlers/plugin-highlight-tiddlywiki.tid create mode 100644 tiddlers/plugin-journal-calendar-widget.tid create mode 100644 tiddlers/plugin-task-board-edit-fields.tid create mode 100644 tiddlers/plugin-task-board-styles.tid create mode 100644 tiddlers/plugin-task-board-view-fields.tid create mode 100644 tiddlers/plugin-task-board-widget.tid delete mode 100644 tiddlers/sidebar-current-tiddler-toc.tid create mode 100644 "tiddlers/\344\272\213\346\203\205\347\234\213\346\235\277.tid" (limited to 'tiddlers') diff --git a/tiddlers/copy-codeblock.tid b/tiddlers/copy-codeblock.tid deleted file mode 100644 index 2251c62..0000000 --- a/tiddlers/copy-codeblock.tid +++ /dev/null @@ -1,72 +0,0 @@ -title: $:/plugins/local/copy-codeblock/copy-codeblock.js -type: application/javascript -module-type: widget - -/*\ -title: $:/plugins/local/copy-codeblock/copy-codeblock.js -type: application/javascript -module-type: widget - -Adds a copy button to code blocks. -\*/ -(function() { -"use strict"; - -var CodeBlockWidget = require("$:/core/modules/widgets/codeblock.js").codeblock; - -var CopyCodeBlockWidget = function(parseTreeNode,options) { - this.initialise(parseTreeNode,options); -}; - -CopyCodeBlockWidget.prototype = new CodeBlockWidget(); -CopyCodeBlockWidget.prototype.constructor = CopyCodeBlockWidget; - -CopyCodeBlockWidget.prototype.postRender = function() { - var self = this, - preNode = this.domNodes[0], - previousNode, - button; - - // Preserve syntax highlighting supplied by the highlight plugin. - if(CodeBlockWidget.prototype.postRender) { - CodeBlockWidget.prototype.postRender.call(this); - } - - if(!$tw.browser || !preNode || preNode.isTiddlyWikiFakeDom) { - return; - } - - // The system code view already renders its own copy button. - previousNode = preNode.previousElementSibling; - if(previousNode && previousNode.querySelector && previousNode.querySelector(".tc-image-copy-clipboard")) { - return; - } - - preNode.style.position = "relative"; - preNode.style.paddingRight = "5em"; - - button = this.document.createElement("button"); - button.setAttribute("type","button"); - button.setAttribute("aria-label","Copy code"); - button.setAttribute("title","Copy code"); - button.className = "tc-btn-invisible tc-copy-codeblock-button"; - button.textContent = "Copy"; - button.style.position = "absolute"; - button.style.top = "0.5em"; - button.style.right = "0.5em"; - button.style.zIndex = "1"; - button.style.padding = "0.2em 0.45em"; - button.style.cursor = "pointer"; - - button.addEventListener("click",function(event) { - event.preventDefault(); - event.stopPropagation(); - $tw.utils.copyToClipboard(self.getAttribute("code","")); - }); - - preNode.appendChild(button); -}; - -exports.codeblock = CopyCodeBlockWidget; - -})(); diff --git a/tiddlers/current-tiddler-toc-widget.tid b/tiddlers/current-tiddler-toc-widget.tid deleted file mode 100644 index a68e449..0000000 --- a/tiddlers/current-tiddler-toc-widget.tid +++ /dev/null @@ -1,284 +0,0 @@ -title: $:/plugins/local/current-tiddler-toc/widget.js -type: application/javascript -module-type: widget - -/*\ -title: $:/plugins/local/current-tiddler-toc/widget.js -type: application/javascript -module-type: widget - -Displays a table of contents for the current story tiddler. -\*/ -(function() { -"use strict"; - -var Widget = require("$:/core/modules/widgets/widget.js").widget; - -var CurrentTiddlerTocWidget = function(parseTreeNode,options) { - this.initialise(parseTreeNode,options); -}; - -CurrentTiddlerTocWidget.prototype = new Widget(); -CurrentTiddlerTocWidget.prototype.constructor = CurrentTiddlerTocWidget; - -CurrentTiddlerTocWidget.prototype.render = function(parent,nextSibling) { - this.parentDomNode = parent; - this.computeAttributes(); - this.execute(); - - var container = this.document.createElement("div"); - container.className = "tc-current-tiddler-toc"; - - parent.insertBefore(container,nextSibling); - this.domNodes.push(container); - this.containerNode = container; - - if($tw.browser) { - var self = this, - windowObject = this.document.defaultView || window; - - this.windowObject = windowObject; - this.resizeHandler = function() { - self.updateMaxHeight(); - }; - windowObject.addEventListener("resize",this.resizeHandler); - if(windowObject.visualViewport) { - windowObject.visualViewport.addEventListener("resize",this.resizeHandler); - } - } - - this.scheduleBuild(); -}; - -CurrentTiddlerTocWidget.prototype.execute = function() { -}; - -CurrentTiddlerTocWidget.prototype.getStoryTitles = function() { - return this.wiki.getTiddlerList("$:/StoryList") || []; -}; - -CurrentTiddlerTocWidget.prototype.findTiddlerFrame = function(title) { - var frames = this.document.querySelectorAll(".tc-tiddler-frame[data-tiddler-title]"), - index; - - for(index = 0; index < frames.length; index++) { - var frame = frames[index], - frameTitle = frame.getAttribute("data-tiddler-title"), - frameTiddler; - - if(frameTitle === title) { - return frame; - } - - frameTiddler = this.wiki.getTiddler(frameTitle); - - if(frameTiddler && (frameTiddler.fields["draft.of"] === title || frameTiddler.fields["draft.title"] === title)) { - return frame; - } - } - - return null; -}; - -CurrentTiddlerTocWidget.prototype.clearContainer = function() { - while(this.containerNode && this.containerNode.firstChild) { - this.containerNode.removeChild(this.containerNode.firstChild); - } -}; - -CurrentTiddlerTocWidget.prototype.showMessage = function(message) { - var messageNode = this.document.createElement("div"); - - messageNode.className = "tc-current-tiddler-toc-empty"; - messageNode.textContent = message; - this.containerNode.appendChild(messageNode); -}; - -CurrentTiddlerTocWidget.prototype.updateMaxHeight = function() { - if(!this.containerNode || !this.containerNode.parentNode || !this.windowObject) { - return; - } - - var viewport = this.windowObject.visualViewport, - viewportHeight = viewport ? viewport.height : this.windowObject.innerHeight, - top = this.containerNode.getBoundingClientRect().top, - availableHeight = Math.max(160,Math.floor(viewportHeight - top - 16)); - - this.containerNode.style.maxHeight = availableHeight + "px"; -}; - -CurrentTiddlerTocWidget.prototype.getHeadings = function(frame) { - var body = frame.querySelector(".tc-tiddler-body"); - - if(!body) { - body = frame.querySelector(".tc-tiddler-preview-preview"); - } - - if(!body) { - return []; - } - - var headingNodes = body.querySelectorAll("h1, h2, h3, h4, h5, h6"), - headings = [], - index; - - for(index = 0; index < headingNodes.length; index++) { - var heading = headingNodes[index]; - - // Ignore headings that belong to nested, independent tiddler frames. - if(heading.closest && heading.closest(".tc-tiddler-frame") !== frame) { - continue; - } - - headings.push({ - node: heading, - level: parseInt(heading.tagName.substring(1),10), - text: heading.textContent.trim() || "未命名标题" - }); - } - - return headings; -}; - -CurrentTiddlerTocWidget.prototype.addTiddlerGroup = function(title,frame) { - var self = this, - groupNode = this.document.createElement("section"), - titleButton = this.document.createElement("button"); - - groupNode.className = "tc-current-tiddler-toc-group"; - titleButton.setAttribute("type","button"); - titleButton.className = "tc-btn-invisible tc-current-tiddler-toc-title"; - titleButton.textContent = title; - titleButton.title = "滚动到条目:" + title; - titleButton.addEventListener("click",function(event) { - event.preventDefault(); - frame.scrollIntoView({ - behavior: "smooth", - block: "start" - }); - }); - groupNode.appendChild(titleButton); - - var headings = this.getHeadings(frame); - - if(!headings.length) { - var emptyNode = this.document.createElement("div"); - emptyNode.className = "tc-current-tiddler-toc-group-empty"; - emptyNode.textContent = "没有标题"; - groupNode.appendChild(emptyNode); - this.containerNode.appendChild(groupNode); - return; - } - - var minimumLevel = headings.reduce(function(result,heading) { - return Math.min(result,heading.level); - },6), - listNode = this.document.createElement("ol"); - - listNode.className = "tc-current-tiddler-toc-list"; - groupNode.appendChild(listNode); - - headings.forEach(function(heading) { - var itemNode = self.document.createElement("li"), - buttonNode = self.document.createElement("button"); - - itemNode.className = "tc-current-tiddler-toc-item"; - itemNode.style.paddingLeft = ((heading.level - minimumLevel) * 1.1) + "em"; - - buttonNode.setAttribute("type","button"); - buttonNode.className = "tc-btn-invisible tc-current-tiddler-toc-link"; - buttonNode.textContent = heading.text; - buttonNode.title = heading.text; - buttonNode.addEventListener("click",function(event) { - event.preventDefault(); - heading.node.scrollIntoView({ - behavior: "smooth", - block: "start" - }); - }); - - itemNode.appendChild(buttonNode); - listNode.appendChild(itemNode); - }); - - this.containerNode.appendChild(groupNode); -}; - -CurrentTiddlerTocWidget.prototype.build = function() { - if(!this.containerNode || !this.containerNode.parentNode) { - return; - } - - this.clearContainer(); - - var titles = this.getStoryTitles(), - index, - visibleCount = 0; - - if(!titles.length) { - this.showMessage("当前没有打开的条目"); - this.updateMaxHeight(); - return; - } - - for(index = 0; index < titles.length; index++) { - var title = titles[index], - frame = this.findTiddlerFrame(title); - - if(frame) { - this.addTiddlerGroup(title,frame); - visibleCount += 1; - } - } - - if(!visibleCount) { - this.showMessage("打开的条目尚未显示"); - } - - this.updateMaxHeight(); -}; - -CurrentTiddlerTocWidget.prototype.scheduleBuild = function() { - var self = this; - - if(!$tw.browser) { - return; - } - - if(this.buildTimer) { - clearTimeout(this.buildTimer); - } - - // Wait until the story river has completed the current DOM update. - this.buildTimer = setTimeout(function() { - self.buildTimer = null; - self.build(); - },0); -}; - -CurrentTiddlerTocWidget.prototype.refresh = function() { - this.computeAttributes(); - this.scheduleBuild(); - return false; -}; - -CurrentTiddlerTocWidget.prototype.removeChildDomNodes = function() { - if(this.buildTimer) { - clearTimeout(this.buildTimer); - this.buildTimer = null; - } - if(this.windowObject && this.resizeHandler) { - this.windowObject.removeEventListener("resize",this.resizeHandler); - if(this.windowObject.visualViewport) { - this.windowObject.visualViewport.removeEventListener("resize",this.resizeHandler); - } - this.resizeHandler = null; - this.windowObject = null; - } - - Widget.prototype.removeChildDomNodes.call(this); -}; - -exports["current-tiddler-toc"] = CurrentTiddlerTocWidget; - -})(); diff --git a/tiddlers/files-list-api.tid b/tiddlers/files-list-api.tid deleted file mode 100644 index 063fddb..0000000 --- a/tiddlers/files-list-api.tid +++ /dev/null @@ -1,132 +0,0 @@ -title: $:/plugins/local/files-api/get-files.js -type: application/javascript -module-type: route - -/*\ -title: $:/plugins/local/files-api/get-files.js -type: application/javascript -module-type: route - -GET /api/files -\*/ -"use strict"; - -var fs = require("fs"), - path = require("path"); - -exports.methods = ["GET"]; -exports.path = /^\/api\/files\/?$/; - -exports.info = { - priority: 100 -}; - -function encodeFilePath(filename) { - return filename - .split(path.sep) - .map(encodeURIComponent) - .join("/"); -} - -async function scanDirectory(rootDirectory,directory,prefix) { - var entries, - files = []; - - try { - entries = await fs.promises.readdir(directory,{ - withFileTypes: true - }); - } catch(error) { - if(error.code === "ENOENT") { - return []; - } - throw error; - } - - for(var index = 0; index < entries.length; index++) { - var entry = entries[index]; - - // 不公开隐藏文件。 - if(entry.name.charAt(0) === ".") { - continue; - } - - var absolutePath = path.join(directory,entry.name); - - // 忽略符号链接,避免扫描到 files/ 目录之外。 - if(entry.isDirectory()) { - var childFiles = await scanDirectory( - rootDirectory, - absolutePath, - path.join(prefix,entry.name) - ); - files.push.apply(files,childFiles); - continue; - } - - if(!entry.isFile()) { - continue; - } - - var relativePath = path.join(prefix,entry.name), - extension = path.extname(entry.name).toLowerCase(), - extensionInfo = $tw.config.fileExtensionInfo[extension], - stats; - - try { - stats = await fs.promises.stat(absolutePath); - } catch(error) { - // 文件可能在扫描过程中被删除。 - continue; - } - - files.push({ - name: entry.name, - path: relativePath.split(path.sep).join("/"), - url: (prefix === "" ? "" : "") + - encodeFilePath(relativePath), - type: extensionInfo ? - extensionInfo.type : - "application/octet-stream", - size: stats.size, - modified: stats.mtime.toISOString() - }); - } - - return files; -} - -exports.handler = function(request,response,state) { - var rootDirectory = path.resolve(state.boot.wikiPath,"files"), - pathPrefix = state.pathPrefix || ""; - - scanDirectory(rootDirectory,rootDirectory,"") - .then(function(files) { - files.sort(function(left,right) { - return left.path.localeCompare(right.path); - }); - - files.forEach(function(file) { - file.url = pathPrefix + "/files/" + file.url; - }); - - state.sendResponse( - 200, - {"Content-Type": "application/json; charset=utf-8"}, - JSON.stringify(files), - "utf8" - ); - }) - .catch(function(error) { - console.error("Unable to list files directory:",error); - - state.sendResponse( - 500, - {"Content-Type": "application/json; charset=utf-8"}, - JSON.stringify({ - error: "Unable to list files directory" - }), - "utf8" - ); - }); -}; \ No newline at end of file diff --git a/tiddlers/highlight-caddyfile.tid b/tiddlers/highlight-caddyfile.tid deleted file mode 100644 index d36ea30..0000000 --- a/tiddlers/highlight-caddyfile.tid +++ /dev/null @@ -1,65 +0,0 @@ -tiddlers/highlight-caddyfile.tid -title: $:/plugins/local/highlight-languages/caddyfile.js -type: application/javascript -module-type: highlight - -/*\ -title: $:/plugins/local/highlight-languages/caddyfile.js -type: application/javascript -module-type: highlight - -Caddyfile language definition for highlight.js. -\*/ -"use strict"; - -hljs.registerLanguage("caddyfile",function(hljs) { - return { - name: "Caddyfile", - aliases: ["caddy"], - case_insensitive: true, - contains: [ - { - className: "comment", - begin: /#/, - end: /$/ - }, - { - className: "section", - begin: /^\s*(?=[^#\n{]*(?:[.:,]|\blocalhost\b))(?:https?:\/\/)?[^\s#{}]+(?:\s*,\s*[^\s#{}]+)*(?=\s*\{?\s*$)/, - relevance: 10 - }, - { - className: "keyword", - begin: /^\s*[A-Za-z_][A-Za-z0-9_-]*(?=\s|$)/, - relevance: 1 - }, - { - className: "meta", - match: /@[A-Za-z0-9_-]+/ - }, - { - className: "variable", - match: /\{\$?[A-Za-z_][A-Za-z0-9_.\-[\]*]*\}/ - }, - { - className: "title", - match: /\([A-Za-z_][A-Za-z0-9_-]*\)/ - }, - hljs.QUOTE_STRING_MODE, - hljs.APOS_STRING_MODE, - { - className: "literal", - match: /\b(?:true|false|on|off)\b/ - }, - { - className: "number", - match: /\b\d+(?:\.\d+)?(?:ns|us|ms|s|m|h|d|kb|mb|gb|tb)?\b/i, - relevance: 0 - }, - { - className: "punctuation", - match: /[{}]/ - } - ] - }; -}); \ No newline at end of file diff --git a/tiddlers/highlight-json5.tid b/tiddlers/highlight-json5.tid deleted file mode 100644 index 099df98..0000000 --- a/tiddlers/highlight-json5.tid +++ /dev/null @@ -1,59 +0,0 @@ -title: $:/plugins/local/highlight-languages/json5.js -type: application/javascript -module-type: highlight - -/*\ -title: $:/plugins/local/highlight-languages/json5.js -type: application/javascript -module-type: highlight - -JSON5 language definition for highlight.js. -\*/ -"use strict"; - -hljs.registerLanguage("json5",function(hljs) { - return { - name: "JSON5", - aliases: ["json5"], - keywords: { - literal: "true false null" - }, - contains: [ - hljs.C_LINE_COMMENT_MODE, - hljs.C_BLOCK_COMMENT_MODE, - { - className: "attr", - match: /[A-Za-z_$][0-9A-Za-z_$]*(?=\s*:)/ - }, - { - className: "attr", - match: /"(?:\\.|[^"\\\r\n])*"(?=\s*:)/ - }, - { - className: "attr", - match: /'(?:\\.|[^'\\\r\n])*'(?=\s*:)/ - }, - hljs.QUOTE_STRING_MODE, - hljs.APOS_STRING_MODE, - { - className: "number", - variants: [ - { - begin: /[+-]?(?:Infinity|NaN)\b/ - }, - { - begin: /[+-]?0[xX][0-9a-fA-F]+\b/ - }, - { - begin: /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/ - } - ], - relevance: 0 - }, - { - className: "punctuation", - match: /[{}\[\],:]/ - } - ] - }; -}); \ No newline at end of file diff --git a/tiddlers/highlight-tiddlywiki.tid b/tiddlers/highlight-tiddlywiki.tid deleted file mode 100644 index 1d3d53a..0000000 --- a/tiddlers/highlight-tiddlywiki.tid +++ /dev/null @@ -1,191 +0,0 @@ -title: $:/plugins/local/highlight-languages/tiddlywiki.js -type: application/javascript -module-type: highlight - -/*\ -title: $:/plugins/local/highlight-languages/tiddlywiki.js -type: application/javascript -module-type: highlight - -TiddlyWiki TID and WikiText language definition for highlight.js. -\*/ -"use strict"; - -hljs.registerLanguage("tiddlywiki",function(hljs) { - var systemTiddler = { - className: "symbol", - match: /\$:\/[^\s\][\]<>{}"']+/ - }, - variable = { - className: "variable", - variants: [ - { - begin: /<>/ - }, - { - match: /<[^<>\s]+>/ - } - ] - }, - transclusion = { - className: "template-variable", - variants: [ - { - begin: /\{\{\{/, - end: /\}\}\}/ - }, - { - begin: /\{\{/, - end: /\}\}/ - } - ], - contains: [ - systemTiddler, - variable - ] - }, - link = { - className: "link", - variants: [ - { - begin: /\[\[/, - end: /\]\]/ - }, - { - begin: /\[(?:img|ext)\[/, - end: /\]\]/ - } - ], - contains: [ - systemTiddler - ] - }, - filter = { - className: "selector", - begin: /\[(?=[^\]\n]*[A-Za-z][A-Za-z0-9_-]*\[)/, - end: /\]/, - contains: [ - { - className: "keyword", - match: /[A-Za-z][A-Za-z0-9_.-]*(?=\[)/ - }, - { - className: "string", - begin: /\[/, - end: /\]/, - contains: [ - systemTiddler, - variable - ] - }, - variable, - systemTiddler - ] - }; - - return { - name: "TiddlyWiki", - aliases: [ - "tid", - "wikitext" - ], - contains: [ - { - className: "comment", - begin: // - }, - { - className: "comment", - begin: /\/%/, - end: /%\// - }, - { - className: "meta", - begin: /^\\(?:define|end|function|import|parameters|procedure|rules|whitespace|widget)\b/, - end: /$/, - contains: [ - variable, - systemTiddler, - hljs.QUOTE_STRING_MODE, - hljs.APOS_STRING_MODE - ] - }, - { - className: "attr", - match: /^[A-Za-z0-9_.-]+(?=\s*:)/ - }, - { - className: "section", - begin: /^!{1,6}\s+/, - end: /$/ - }, - { - className: "bullet", - match: /^[*#;:]+\s+/ - }, - { - className: "quote", - begin: /^>{1,3}\s+/, - end: /$/ - }, - { - className: "tag", - begin: /<\/?\$?[A-Za-z][A-Za-z0-9_.:-]*/, - end: /\/?>/, - contains: [ - { - className: "attr", - match: /[A-Za-z_$][A-Za-z0-9_.:-]*(?=\s*=)/ - }, - hljs.QUOTE_STRING_MODE, - hljs.APOS_STRING_MODE, - transclusion, - variable, - filter, - systemTiddler - ] - }, - { - className: "template-tag", - begin: /<>/, - contains: [ - hljs.QUOTE_STRING_MODE, - hljs.APOS_STRING_MODE, - systemTiddler - ] - }, - transclusion, - link, - filter, - systemTiddler, - { - className: "code", - begin: /`+/, - end: /`+/ - }, - { - className: "strong", - begin: /''/, - end: /''/ - }, - { - className: "emphasis", - begin: /\/\//, - end: /\/\// - }, - { - className: "emphasis", - begin: /__/, - end: /__/ - }, - { - className: "deletion", - begin: /~~/, - end: /~~/ - } - ] - }; -}); \ No newline at end of file diff --git a/tiddlers/journal-calendar-widget.tid b/tiddlers/journal-calendar-widget.tid deleted file mode 100644 index cd54724..0000000 --- a/tiddlers/journal-calendar-widget.tid +++ /dev/null @@ -1,333 +0,0 @@ -title: $:/plugins/local/journal-calendar/widget.js -type: application/javascript -module-type: widget - -/*\ -title: $:/plugins/local/journal-calendar/widget.js -type: application/javascript -module-type: widget - -Renders journal tiddlers as monthly calendars. -\*/ -(function() { -"use strict"; - -var Widget = require("$:/core/modules/widgets/widget.js").widget, - VIEW_STATE_TITLE = "$:/state/journal-calendar/view", - MONTH_STATE_TITLE = "$:/state/journal-calendar/month"; - -var JournalCalendarWidget = function(parseTreeNode,options) { - this.initialise(parseTreeNode,options); -}; - -JournalCalendarWidget.prototype = new Widget(); -JournalCalendarWidget.prototype.constructor = JournalCalendarWidget; - -function pad(value) { - return value < 10 ? "0" + value : "" + value; -} - -function dateKey(year,month,day) { - return year + "-" + pad(month) + "-" + pad(day); -} - -function monthKey(year,month) { - return year + "-" + pad(month); -} - -function isValidMonth(value) { - var match = /^(\d{4})-(\d{2})$/.exec(value || ""); - return !!match && Number(match[2]) >= 1 && Number(match[2]) <= 12; -} - -function parseMonth(value) { - var parts = value.split("-"); - return { - year: Number(parts[0]), - month: Number(parts[1]) - }; -} - -JournalCalendarWidget.prototype.render = function(parent,nextSibling) { - this.parentDomNode = parent; - this.computeAttributes(); - this.execute(); - - var container = this.document.createElement("div"); - container.className = "tc-journal-calendar"; - parent.insertBefore(container,nextSibling); - this.domNodes.push(container); - this.containerNode = container; - - this.renderCalendar(); -}; - -JournalCalendarWidget.prototype.execute = function() { - this.tag = this.getAttribute("tag","日志"); - this.view = this.getAttribute("view","switchable"); - this.month = this.getAttribute("month",""); -}; - -JournalCalendarWidget.prototype.getJournalDate = function(tiddler) { - var titleMatch = /^(\d{4})年(\d{2})月(\d{2})日$/.exec(tiddler.fields.title), - date; - - if(titleMatch) { - date = new Date(Number(titleMatch[1]),Number(titleMatch[2]) - 1,Number(titleMatch[3])); - if(date.getFullYear() === Number(titleMatch[1]) && date.getMonth() === Number(titleMatch[2]) - 1 && date.getDate() === Number(titleMatch[3])) { - return date; - } - } - return null; -}; - -JournalCalendarWidget.prototype.getJournalData = function() { - var titles = this.wiki.getTiddlersWithTag(this.tag), - byDate = Object.create(null), - months = Object.create(null), - self = this; - - titles.forEach(function(title) { - var tiddler = self.wiki.getTiddler(title); - if(!tiddler || tiddler.fields["draft.of"]) { - return; - } - - var date = self.getJournalDate(tiddler); - if(!date) { - return; - } - - var day = dateKey(date.getFullYear(),date.getMonth() + 1,date.getDate()), - month = day.substring(0,7); - byDate[day] = { - title: title, - tags: (tiddler.fields.tags || []).filter(function(tag) { - return tag !== self.tag; - }) - }; - months[month] = true; - }); - - return { - byDate: byDate, - months: Object.keys(months).sort().reverse() - }; -}; - -JournalCalendarWidget.prototype.getCurrentMonth = function() { - if(isValidMonth(this.month)) { - return this.month; - } - - var stateMonth = this.wiki.getTiddlerText(MONTH_STATE_TITLE,""); - if(isValidMonth(stateMonth)) { - return stateMonth; - } - - var now = new Date(); - return monthKey(now.getFullYear(),now.getMonth() + 1); -}; - -JournalCalendarWidget.prototype.getCurrentView = function() { - if(this.view === "month" || this.view === "all") { - return this.view; - } - return this.wiki.getTiddlerText(VIEW_STATE_TITLE,"month") === "all" ? "all" : "month"; -}; - -JournalCalendarWidget.prototype.setState = function(title,value) { - this.wiki.addTiddler({ - title: title, - text: value - }); -}; - -JournalCalendarWidget.prototype.createButton = function(label,className,handler) { - var button = this.document.createElement("button"); - button.setAttribute("type","button"); - button.className = className; - button.textContent = label; - button.addEventListener("click",handler); - return button; -}; - -JournalCalendarWidget.prototype.renderViewSwitcher = function(currentView) { - if(this.view !== "switchable") { - return; - } - - var self = this, - controls = this.document.createElement("div"); - controls.className = "tc-journal-calendar-view-switcher"; - - controls.appendChild(this.createButton("单月视图","tc-btn-invisible tc-journal-calendar-switch" + (currentView === "month" ? " tc-selected" : ""),function() { - self.setState(VIEW_STATE_TITLE,"month"); - })); - controls.appendChild(this.createButton("全部月份","tc-btn-invisible tc-journal-calendar-switch" + (currentView === "all" ? " tc-selected" : ""),function() { - self.setState(VIEW_STATE_TITLE,"all"); - })); - this.containerNode.appendChild(controls); -}; - -JournalCalendarWidget.prototype.renderMonthControls = function(selectedMonth) { - if(isValidMonth(this.month)) { - return; - } - - var self = this, - selected = parseMonth(selectedMonth), - controls = this.document.createElement("div"); - controls.className = "tc-journal-calendar-month-controls"; - - function setOffset(offset) { - var date = new Date(selected.year,selected.month - 1 + offset,1); - self.setState(MONTH_STATE_TITLE,monthKey(date.getFullYear(),date.getMonth() + 1)); - } - - controls.appendChild(this.createButton("上个月","tc-btn-invisible tc-journal-calendar-nav",function() { - setOffset(-1); - })); - controls.appendChild(this.createButton("本月","tc-btn-invisible tc-journal-calendar-nav",function() { - var now = new Date(); - self.setState(MONTH_STATE_TITLE,monthKey(now.getFullYear(),now.getMonth() + 1)); - })); - controls.appendChild(this.createButton("下个月","tc-btn-invisible tc-journal-calendar-nav",function() { - setOffset(1); - })); - this.containerNode.appendChild(controls); -}; - -JournalCalendarWidget.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.metaKey || event.ctrlKey || event.button === 1 - }); -}; - -JournalCalendarWidget.prototype.renderMonth = function(value,byDate) { - var self = this, - selected = parseMonth(value), - section = this.document.createElement("section"), - heading = this.document.createElement("h2"), - viewport = this.document.createElement("div"), - grid = this.document.createElement("div"), - weekdays = ["一","二","三","四","五","六","日"], - firstWeekday = (new Date(selected.year,selected.month - 1,1).getDay() + 6) % 7, - daysInMonth = new Date(selected.year,selected.month,0).getDate(), - today = new Date(), - index; - - section.className = "tc-journal-calendar-month"; - heading.className = "tc-journal-calendar-month-title"; - heading.textContent = selected.year + "年" + selected.month + "月"; - section.appendChild(heading); - viewport.className = "tc-journal-calendar-viewport"; - grid.className = "tc-journal-calendar-grid"; - - weekdays.forEach(function(weekday) { - var weekdayNode = self.document.createElement("div"); - weekdayNode.className = "tc-journal-calendar-weekday"; - weekdayNode.textContent = weekday; - grid.appendChild(weekdayNode); - }); - - for(index = 0; index < firstWeekday; index++) { - var blank = this.document.createElement("div"); - blank.className = "tc-journal-calendar-day tc-journal-calendar-day-empty"; - grid.appendChild(blank); - } - - for(var day = 1; day <= daysInMonth; day++) { - var key = dateKey(selected.year,selected.month,day), - journal = byDate[key], - dayNode = this.document.createElement("div"), - numberNode; - - dayNode.className = "tc-journal-calendar-day" + (journal ? " tc-journal-calendar-day-has-journal" : ""); - if(selected.year === today.getFullYear() && selected.month === today.getMonth() + 1 && day === today.getDate()) { - dayNode.className += " tc-journal-calendar-day-today"; - } - - if(journal) { - numberNode = this.createButton(day,"tc-btn-invisible tc-journal-calendar-day-number tc-journal-calendar-day-link",function(title) { - return function(event) { - self.navigateTo(title,event.currentTarget,event); - }; - }(journal.title)); - numberNode.title = "打开日志:" + journal.title; - numberNode.setAttribute("aria-label","打开日志:" + journal.title); - } else { - numberNode = this.document.createElement("div"); - numberNode.className = "tc-journal-calendar-day-number"; - numberNode.textContent = day; - } - dayNode.appendChild(numberNode); - - if(journal) { - journal.tags.forEach(function(tag) { - var tagButton = self.createButton(tag,"tc-btn-invisible tc-tag-label tc-journal-calendar-tag",function(event) { - self.navigateTo(tag,event.currentTarget,event); - }); - tagButton.title = "打开标签:" + tag; - tagButton.setAttribute("aria-label","打开标签:" + tag); - dayNode.appendChild(tagButton); - }); - } - grid.appendChild(dayNode); - } - - viewport.appendChild(grid); - section.appendChild(viewport); - this.containerNode.appendChild(section); -}; - -JournalCalendarWidget.prototype.renderCalendar = function() { - var data = this.getJournalData(), - currentView = this.getCurrentView(); - - this.renderViewSwitcher(currentView); - - if(currentView === "all") { - if(!data.months.length) { - var empty = this.document.createElement("div"); - empty.className = "tc-journal-calendar-empty"; - empty.textContent = "目前没有日志。"; - this.containerNode.appendChild(empty); - return; - } - data.months.forEach(function(month) { - this.renderMonth(month,data.byDate); - },this); - return; - } - - var selectedMonth = this.getCurrentMonth(); - this.renderMonthControls(selectedMonth); - this.renderMonth(selectedMonth,data.byDate); -}; - -JournalCalendarWidget.prototype.refresh = function(changedTiddlers) { - var changedAttributes = this.computeAttributes(); - if(Object.keys(changedAttributes).length || Object.keys(changedTiddlers).length) { - this.refreshSelf(); - return true; - } - return false; -}; - -exports["journal-calendar"] = JournalCalendarWidget; - -})(); diff --git a/tiddlers/plugin-copy-codeblock-widget.tid b/tiddlers/plugin-copy-codeblock-widget.tid new file mode 100644 index 0000000..2251c62 --- /dev/null +++ b/tiddlers/plugin-copy-codeblock-widget.tid @@ -0,0 +1,72 @@ +title: $:/plugins/local/copy-codeblock/copy-codeblock.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/copy-codeblock/copy-codeblock.js +type: application/javascript +module-type: widget + +Adds a copy button to code blocks. +\*/ +(function() { +"use strict"; + +var CodeBlockWidget = require("$:/core/modules/widgets/codeblock.js").codeblock; + +var CopyCodeBlockWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +CopyCodeBlockWidget.prototype = new CodeBlockWidget(); +CopyCodeBlockWidget.prototype.constructor = CopyCodeBlockWidget; + +CopyCodeBlockWidget.prototype.postRender = function() { + var self = this, + preNode = this.domNodes[0], + previousNode, + button; + + // Preserve syntax highlighting supplied by the highlight plugin. + if(CodeBlockWidget.prototype.postRender) { + CodeBlockWidget.prototype.postRender.call(this); + } + + if(!$tw.browser || !preNode || preNode.isTiddlyWikiFakeDom) { + return; + } + + // The system code view already renders its own copy button. + previousNode = preNode.previousElementSibling; + if(previousNode && previousNode.querySelector && previousNode.querySelector(".tc-image-copy-clipboard")) { + return; + } + + preNode.style.position = "relative"; + preNode.style.paddingRight = "5em"; + + button = this.document.createElement("button"); + button.setAttribute("type","button"); + button.setAttribute("aria-label","Copy code"); + button.setAttribute("title","Copy code"); + button.className = "tc-btn-invisible tc-copy-codeblock-button"; + button.textContent = "Copy"; + button.style.position = "absolute"; + button.style.top = "0.5em"; + button.style.right = "0.5em"; + button.style.zIndex = "1"; + button.style.padding = "0.2em 0.45em"; + button.style.cursor = "pointer"; + + button.addEventListener("click",function(event) { + event.preventDefault(); + event.stopPropagation(); + $tw.utils.copyToClipboard(self.getAttribute("code","")); + }); + + preNode.appendChild(button); +}; + +exports.codeblock = CopyCodeBlockWidget; + +})(); diff --git a/tiddlers/plugin-current-tiddler-toc-sidebar.tid b/tiddlers/plugin-current-tiddler-toc-sidebar.tid new file mode 100644 index 0000000..9d73d6d --- /dev/null +++ b/tiddlers/plugin-current-tiddler-toc-sidebar.tid @@ -0,0 +1,69 @@ +title: $:/plugins/local/current-tiddler-toc/sidebar +type: text/vnd.tiddlywiki +tags: $:/tags/SideBar +caption: 目录 +list-after: $:/core/ui/SideBar/More + + + +<$current-tiddler-toc/> diff --git a/tiddlers/plugin-current-tiddler-toc-widget.tid b/tiddlers/plugin-current-tiddler-toc-widget.tid new file mode 100644 index 0000000..a68e449 --- /dev/null +++ b/tiddlers/plugin-current-tiddler-toc-widget.tid @@ -0,0 +1,284 @@ +title: $:/plugins/local/current-tiddler-toc/widget.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/current-tiddler-toc/widget.js +type: application/javascript +module-type: widget + +Displays a table of contents for the current story tiddler. +\*/ +(function() { +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var CurrentTiddlerTocWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +CurrentTiddlerTocWidget.prototype = new Widget(); +CurrentTiddlerTocWidget.prototype.constructor = CurrentTiddlerTocWidget; + +CurrentTiddlerTocWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + var container = this.document.createElement("div"); + container.className = "tc-current-tiddler-toc"; + + parent.insertBefore(container,nextSibling); + this.domNodes.push(container); + this.containerNode = container; + + if($tw.browser) { + var self = this, + windowObject = this.document.defaultView || window; + + this.windowObject = windowObject; + this.resizeHandler = function() { + self.updateMaxHeight(); + }; + windowObject.addEventListener("resize",this.resizeHandler); + if(windowObject.visualViewport) { + windowObject.visualViewport.addEventListener("resize",this.resizeHandler); + } + } + + this.scheduleBuild(); +}; + +CurrentTiddlerTocWidget.prototype.execute = function() { +}; + +CurrentTiddlerTocWidget.prototype.getStoryTitles = function() { + return this.wiki.getTiddlerList("$:/StoryList") || []; +}; + +CurrentTiddlerTocWidget.prototype.findTiddlerFrame = function(title) { + var frames = this.document.querySelectorAll(".tc-tiddler-frame[data-tiddler-title]"), + index; + + for(index = 0; index < frames.length; index++) { + var frame = frames[index], + frameTitle = frame.getAttribute("data-tiddler-title"), + frameTiddler; + + if(frameTitle === title) { + return frame; + } + + frameTiddler = this.wiki.getTiddler(frameTitle); + + if(frameTiddler && (frameTiddler.fields["draft.of"] === title || frameTiddler.fields["draft.title"] === title)) { + return frame; + } + } + + return null; +}; + +CurrentTiddlerTocWidget.prototype.clearContainer = function() { + while(this.containerNode && this.containerNode.firstChild) { + this.containerNode.removeChild(this.containerNode.firstChild); + } +}; + +CurrentTiddlerTocWidget.prototype.showMessage = function(message) { + var messageNode = this.document.createElement("div"); + + messageNode.className = "tc-current-tiddler-toc-empty"; + messageNode.textContent = message; + this.containerNode.appendChild(messageNode); +}; + +CurrentTiddlerTocWidget.prototype.updateMaxHeight = function() { + if(!this.containerNode || !this.containerNode.parentNode || !this.windowObject) { + return; + } + + var viewport = this.windowObject.visualViewport, + viewportHeight = viewport ? viewport.height : this.windowObject.innerHeight, + top = this.containerNode.getBoundingClientRect().top, + availableHeight = Math.max(160,Math.floor(viewportHeight - top - 16)); + + this.containerNode.style.maxHeight = availableHeight + "px"; +}; + +CurrentTiddlerTocWidget.prototype.getHeadings = function(frame) { + var body = frame.querySelector(".tc-tiddler-body"); + + if(!body) { + body = frame.querySelector(".tc-tiddler-preview-preview"); + } + + if(!body) { + return []; + } + + var headingNodes = body.querySelectorAll("h1, h2, h3, h4, h5, h6"), + headings = [], + index; + + for(index = 0; index < headingNodes.length; index++) { + var heading = headingNodes[index]; + + // Ignore headings that belong to nested, independent tiddler frames. + if(heading.closest && heading.closest(".tc-tiddler-frame") !== frame) { + continue; + } + + headings.push({ + node: heading, + level: parseInt(heading.tagName.substring(1),10), + text: heading.textContent.trim() || "未命名标题" + }); + } + + return headings; +}; + +CurrentTiddlerTocWidget.prototype.addTiddlerGroup = function(title,frame) { + var self = this, + groupNode = this.document.createElement("section"), + titleButton = this.document.createElement("button"); + + groupNode.className = "tc-current-tiddler-toc-group"; + titleButton.setAttribute("type","button"); + titleButton.className = "tc-btn-invisible tc-current-tiddler-toc-title"; + titleButton.textContent = title; + titleButton.title = "滚动到条目:" + title; + titleButton.addEventListener("click",function(event) { + event.preventDefault(); + frame.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + }); + groupNode.appendChild(titleButton); + + var headings = this.getHeadings(frame); + + if(!headings.length) { + var emptyNode = this.document.createElement("div"); + emptyNode.className = "tc-current-tiddler-toc-group-empty"; + emptyNode.textContent = "没有标题"; + groupNode.appendChild(emptyNode); + this.containerNode.appendChild(groupNode); + return; + } + + var minimumLevel = headings.reduce(function(result,heading) { + return Math.min(result,heading.level); + },6), + listNode = this.document.createElement("ol"); + + listNode.className = "tc-current-tiddler-toc-list"; + groupNode.appendChild(listNode); + + headings.forEach(function(heading) { + var itemNode = self.document.createElement("li"), + buttonNode = self.document.createElement("button"); + + itemNode.className = "tc-current-tiddler-toc-item"; + itemNode.style.paddingLeft = ((heading.level - minimumLevel) * 1.1) + "em"; + + buttonNode.setAttribute("type","button"); + buttonNode.className = "tc-btn-invisible tc-current-tiddler-toc-link"; + buttonNode.textContent = heading.text; + buttonNode.title = heading.text; + buttonNode.addEventListener("click",function(event) { + event.preventDefault(); + heading.node.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + }); + + itemNode.appendChild(buttonNode); + listNode.appendChild(itemNode); + }); + + this.containerNode.appendChild(groupNode); +}; + +CurrentTiddlerTocWidget.prototype.build = function() { + if(!this.containerNode || !this.containerNode.parentNode) { + return; + } + + this.clearContainer(); + + var titles = this.getStoryTitles(), + index, + visibleCount = 0; + + if(!titles.length) { + this.showMessage("当前没有打开的条目"); + this.updateMaxHeight(); + return; + } + + for(index = 0; index < titles.length; index++) { + var title = titles[index], + frame = this.findTiddlerFrame(title); + + if(frame) { + this.addTiddlerGroup(title,frame); + visibleCount += 1; + } + } + + if(!visibleCount) { + this.showMessage("打开的条目尚未显示"); + } + + this.updateMaxHeight(); +}; + +CurrentTiddlerTocWidget.prototype.scheduleBuild = function() { + var self = this; + + if(!$tw.browser) { + return; + } + + if(this.buildTimer) { + clearTimeout(this.buildTimer); + } + + // Wait until the story river has completed the current DOM update. + this.buildTimer = setTimeout(function() { + self.buildTimer = null; + self.build(); + },0); +}; + +CurrentTiddlerTocWidget.prototype.refresh = function() { + this.computeAttributes(); + this.scheduleBuild(); + return false; +}; + +CurrentTiddlerTocWidget.prototype.removeChildDomNodes = function() { + if(this.buildTimer) { + clearTimeout(this.buildTimer); + this.buildTimer = null; + } + if(this.windowObject && this.resizeHandler) { + this.windowObject.removeEventListener("resize",this.resizeHandler); + if(this.windowObject.visualViewport) { + this.windowObject.visualViewport.removeEventListener("resize",this.resizeHandler); + } + this.resizeHandler = null; + this.windowObject = null; + } + + Widget.prototype.removeChildDomNodes.call(this); +}; + +exports["current-tiddler-toc"] = CurrentTiddlerTocWidget; + +})(); diff --git a/tiddlers/plugin-files-api-route.tid b/tiddlers/plugin-files-api-route.tid new file mode 100644 index 0000000..6236926 --- /dev/null +++ b/tiddlers/plugin-files-api-route.tid @@ -0,0 +1,132 @@ +title: $:/plugins/local/files-api/get-files.js +type: application/javascript +module-type: route + +/*\ +title: $:/plugins/local/files-api/get-files.js +type: application/javascript +module-type: route + +GET /api/files +\*/ +"use strict"; + +var fs = require("fs"), + path = require("path"); + +exports.methods = ["GET"]; +exports.path = /^\/api\/files\/?$/; + +exports.info = { + priority: 100 +}; + +function encodeFilePath(filename) { + return filename + .split(path.sep) + .map(encodeURIComponent) + .join("/"); +} + +async function scanDirectory(rootDirectory,directory,prefix) { + var entries, + files = []; + + try { + entries = await fs.promises.readdir(directory,{ + withFileTypes: true + }); + } catch(error) { + if(error.code === "ENOENT") { + return []; + } + throw error; + } + + for(var index = 0; index < entries.length; index++) { + var entry = entries[index]; + + // 不公开隐藏文件。 + if(entry.name.charAt(0) === ".") { + continue; + } + + var absolutePath = path.join(directory,entry.name); + + // 忽略符号链接,避免扫描到 files/ 目录之外。 + if(entry.isDirectory()) { + var childFiles = await scanDirectory( + rootDirectory, + absolutePath, + path.join(prefix,entry.name) + ); + files.push.apply(files,childFiles); + continue; + } + + if(!entry.isFile()) { + continue; + } + + var relativePath = path.join(prefix,entry.name), + extension = path.extname(entry.name).toLowerCase(), + extensionInfo = $tw.config.fileExtensionInfo[extension], + stats; + + try { + stats = await fs.promises.stat(absolutePath); + } catch(error) { + // 文件可能在扫描过程中被删除。 + continue; + } + + files.push({ + name: entry.name, + path: relativePath.split(path.sep).join("/"), + url: (prefix === "" ? "" : "") + + encodeFilePath(relativePath), + type: extensionInfo ? + extensionInfo.type : + "application/octet-stream", + size: stats.size, + modified: stats.mtime.toISOString() + }); + } + + return files; +} + +exports.handler = function(request,response,state) { + var rootDirectory = path.resolve(state.boot.wikiPath,"files"), + pathPrefix = state.pathPrefix || ""; + + scanDirectory(rootDirectory,rootDirectory,"") + .then(function(files) { + files.sort(function(left,right) { + return left.path.localeCompare(right.path); + }); + + files.forEach(function(file) { + file.url = pathPrefix + "/files/" + file.url; + }); + + state.sendResponse( + 200, + {"Content-Type": "application/json; charset=utf-8"}, + JSON.stringify(files), + "utf8" + ); + }) + .catch(function(error) { + console.error("Unable to list files directory:",error); + + state.sendResponse( + 500, + {"Content-Type": "application/json; charset=utf-8"}, + JSON.stringify({ + error: "Unable to list files directory" + }), + "utf8" + ); + }); +}; diff --git a/tiddlers/plugin-highlight-caddyfile.tid b/tiddlers/plugin-highlight-caddyfile.tid new file mode 100644 index 0000000..8427c3f --- /dev/null +++ b/tiddlers/plugin-highlight-caddyfile.tid @@ -0,0 +1,65 @@ +tiddlers/highlight-caddyfile.tid +title: $:/plugins/local/highlight-languages/caddyfile.js +type: application/javascript +module-type: highlight + +/*\ +title: $:/plugins/local/highlight-languages/caddyfile.js +type: application/javascript +module-type: highlight + +Caddyfile language definition for highlight.js. +\*/ +"use strict"; + +hljs.registerLanguage("caddyfile",function(hljs) { + return { + name: "Caddyfile", + aliases: ["caddy"], + case_insensitive: true, + contains: [ + { + className: "comment", + begin: /#/, + end: /$/ + }, + { + className: "section", + begin: /^\s*(?=[^#\n{]*(?:[.:,]|\blocalhost\b))(?:https?:\/\/)?[^\s#{}]+(?:\s*,\s*[^\s#{}]+)*(?=\s*\{?\s*$)/, + relevance: 10 + }, + { + className: "keyword", + begin: /^\s*[A-Za-z_][A-Za-z0-9_-]*(?=\s|$)/, + relevance: 1 + }, + { + className: "meta", + match: /@[A-Za-z0-9_-]+/ + }, + { + className: "variable", + match: /\{\$?[A-Za-z_][A-Za-z0-9_.\-[\]*]*\}/ + }, + { + className: "title", + match: /\([A-Za-z_][A-Za-z0-9_-]*\)/ + }, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + { + className: "literal", + match: /\b(?:true|false|on|off)\b/ + }, + { + className: "number", + match: /\b\d+(?:\.\d+)?(?:ns|us|ms|s|m|h|d|kb|mb|gb|tb)?\b/i, + relevance: 0 + }, + { + className: "punctuation", + match: /[{}]/ + } + ] + }; +}); diff --git a/tiddlers/plugin-highlight-json5.tid b/tiddlers/plugin-highlight-json5.tid new file mode 100644 index 0000000..fae9bd4 --- /dev/null +++ b/tiddlers/plugin-highlight-json5.tid @@ -0,0 +1,59 @@ +title: $:/plugins/local/highlight-languages/json5.js +type: application/javascript +module-type: highlight + +/*\ +title: $:/plugins/local/highlight-languages/json5.js +type: application/javascript +module-type: highlight + +JSON5 language definition for highlight.js. +\*/ +"use strict"; + +hljs.registerLanguage("json5",function(hljs) { + return { + name: "JSON5", + aliases: ["json5"], + keywords: { + literal: "true false null" + }, + contains: [ + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE, + { + className: "attr", + match: /[A-Za-z_$][0-9A-Za-z_$]*(?=\s*:)/ + }, + { + className: "attr", + match: /"(?:\\.|[^"\\\r\n])*"(?=\s*:)/ + }, + { + className: "attr", + match: /'(?:\\.|[^'\\\r\n])*'(?=\s*:)/ + }, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + { + className: "number", + variants: [ + { + begin: /[+-]?(?:Infinity|NaN)\b/ + }, + { + begin: /[+-]?0[xX][0-9a-fA-F]+\b/ + }, + { + begin: /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/ + } + ], + relevance: 0 + }, + { + className: "punctuation", + match: /[{}\[\],:]/ + } + ] + }; +}); diff --git a/tiddlers/plugin-highlight-tiddlywiki.tid b/tiddlers/plugin-highlight-tiddlywiki.tid new file mode 100644 index 0000000..dd1a891 --- /dev/null +++ b/tiddlers/plugin-highlight-tiddlywiki.tid @@ -0,0 +1,191 @@ +title: $:/plugins/local/highlight-languages/tiddlywiki.js +type: application/javascript +module-type: highlight + +/*\ +title: $:/plugins/local/highlight-languages/tiddlywiki.js +type: application/javascript +module-type: highlight + +TiddlyWiki TID and WikiText language definition for highlight.js. +\*/ +"use strict"; + +hljs.registerLanguage("tiddlywiki",function(hljs) { + var systemTiddler = { + className: "symbol", + match: /\$:\/[^\s\][\]<>{}"']+/ + }, + variable = { + className: "variable", + variants: [ + { + begin: /<>/ + }, + { + match: /<[^<>\s]+>/ + } + ] + }, + transclusion = { + className: "template-variable", + variants: [ + { + begin: /\{\{\{/, + end: /\}\}\}/ + }, + { + begin: /\{\{/, + end: /\}\}/ + } + ], + contains: [ + systemTiddler, + variable + ] + }, + link = { + className: "link", + variants: [ + { + begin: /\[\[/, + end: /\]\]/ + }, + { + begin: /\[(?:img|ext)\[/, + end: /\]\]/ + } + ], + contains: [ + systemTiddler + ] + }, + filter = { + className: "selector", + begin: /\[(?=[^\]\n]*[A-Za-z][A-Za-z0-9_-]*\[)/, + end: /\]/, + contains: [ + { + className: "keyword", + match: /[A-Za-z][A-Za-z0-9_.-]*(?=\[)/ + }, + { + className: "string", + begin: /\[/, + end: /\]/, + contains: [ + systemTiddler, + variable + ] + }, + variable, + systemTiddler + ] + }; + + return { + name: "TiddlyWiki", + aliases: [ + "tid", + "wikitext" + ], + contains: [ + { + className: "comment", + begin: // + }, + { + className: "comment", + begin: /\/%/, + end: /%\// + }, + { + className: "meta", + begin: /^\\(?:define|end|function|import|parameters|procedure|rules|whitespace|widget)\b/, + end: /$/, + contains: [ + variable, + systemTiddler, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE + ] + }, + { + className: "attr", + match: /^[A-Za-z0-9_.-]+(?=\s*:)/ + }, + { + className: "section", + begin: /^!{1,6}\s+/, + end: /$/ + }, + { + className: "bullet", + match: /^[*#;:]+\s+/ + }, + { + className: "quote", + begin: /^>{1,3}\s+/, + end: /$/ + }, + { + className: "tag", + begin: /<\/?\$?[A-Za-z][A-Za-z0-9_.:-]*/, + end: /\/?>/, + contains: [ + { + className: "attr", + match: /[A-Za-z_$][A-Za-z0-9_.:-]*(?=\s*=)/ + }, + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + transclusion, + variable, + filter, + systemTiddler + ] + }, + { + className: "template-tag", + begin: /<>/, + contains: [ + hljs.QUOTE_STRING_MODE, + hljs.APOS_STRING_MODE, + systemTiddler + ] + }, + transclusion, + link, + filter, + systemTiddler, + { + className: "code", + begin: /`+/, + end: /`+/ + }, + { + className: "strong", + begin: /''/, + end: /''/ + }, + { + className: "emphasis", + begin: /\/\//, + end: /\/\// + }, + { + className: "emphasis", + begin: /__/, + end: /__/ + }, + { + className: "deletion", + begin: /~~/, + end: /~~/ + } + ] + }; +}); diff --git a/tiddlers/plugin-journal-calendar-widget.tid b/tiddlers/plugin-journal-calendar-widget.tid new file mode 100644 index 0000000..cd54724 --- /dev/null +++ b/tiddlers/plugin-journal-calendar-widget.tid @@ -0,0 +1,333 @@ +title: $:/plugins/local/journal-calendar/widget.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/journal-calendar/widget.js +type: application/javascript +module-type: widget + +Renders journal tiddlers as monthly calendars. +\*/ +(function() { +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget, + VIEW_STATE_TITLE = "$:/state/journal-calendar/view", + MONTH_STATE_TITLE = "$:/state/journal-calendar/month"; + +var JournalCalendarWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +JournalCalendarWidget.prototype = new Widget(); +JournalCalendarWidget.prototype.constructor = JournalCalendarWidget; + +function pad(value) { + return value < 10 ? "0" + value : "" + value; +} + +function dateKey(year,month,day) { + return year + "-" + pad(month) + "-" + pad(day); +} + +function monthKey(year,month) { + return year + "-" + pad(month); +} + +function isValidMonth(value) { + var match = /^(\d{4})-(\d{2})$/.exec(value || ""); + return !!match && Number(match[2]) >= 1 && Number(match[2]) <= 12; +} + +function parseMonth(value) { + var parts = value.split("-"); + return { + year: Number(parts[0]), + month: Number(parts[1]) + }; +} + +JournalCalendarWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + var container = this.document.createElement("div"); + container.className = "tc-journal-calendar"; + parent.insertBefore(container,nextSibling); + this.domNodes.push(container); + this.containerNode = container; + + this.renderCalendar(); +}; + +JournalCalendarWidget.prototype.execute = function() { + this.tag = this.getAttribute("tag","日志"); + this.view = this.getAttribute("view","switchable"); + this.month = this.getAttribute("month",""); +}; + +JournalCalendarWidget.prototype.getJournalDate = function(tiddler) { + var titleMatch = /^(\d{4})年(\d{2})月(\d{2})日$/.exec(tiddler.fields.title), + date; + + if(titleMatch) { + date = new Date(Number(titleMatch[1]),Number(titleMatch[2]) - 1,Number(titleMatch[3])); + if(date.getFullYear() === Number(titleMatch[1]) && date.getMonth() === Number(titleMatch[2]) - 1 && date.getDate() === Number(titleMatch[3])) { + return date; + } + } + return null; +}; + +JournalCalendarWidget.prototype.getJournalData = function() { + var titles = this.wiki.getTiddlersWithTag(this.tag), + byDate = Object.create(null), + months = Object.create(null), + self = this; + + titles.forEach(function(title) { + var tiddler = self.wiki.getTiddler(title); + if(!tiddler || tiddler.fields["draft.of"]) { + return; + } + + var date = self.getJournalDate(tiddler); + if(!date) { + return; + } + + var day = dateKey(date.getFullYear(),date.getMonth() + 1,date.getDate()), + month = day.substring(0,7); + byDate[day] = { + title: title, + tags: (tiddler.fields.tags || []).filter(function(tag) { + return tag !== self.tag; + }) + }; + months[month] = true; + }); + + return { + byDate: byDate, + months: Object.keys(months).sort().reverse() + }; +}; + +JournalCalendarWidget.prototype.getCurrentMonth = function() { + if(isValidMonth(this.month)) { + return this.month; + } + + var stateMonth = this.wiki.getTiddlerText(MONTH_STATE_TITLE,""); + if(isValidMonth(stateMonth)) { + return stateMonth; + } + + var now = new Date(); + return monthKey(now.getFullYear(),now.getMonth() + 1); +}; + +JournalCalendarWidget.prototype.getCurrentView = function() { + if(this.view === "month" || this.view === "all") { + return this.view; + } + return this.wiki.getTiddlerText(VIEW_STATE_TITLE,"month") === "all" ? "all" : "month"; +}; + +JournalCalendarWidget.prototype.setState = function(title,value) { + this.wiki.addTiddler({ + title: title, + text: value + }); +}; + +JournalCalendarWidget.prototype.createButton = function(label,className,handler) { + var button = this.document.createElement("button"); + button.setAttribute("type","button"); + button.className = className; + button.textContent = label; + button.addEventListener("click",handler); + return button; +}; + +JournalCalendarWidget.prototype.renderViewSwitcher = function(currentView) { + if(this.view !== "switchable") { + return; + } + + var self = this, + controls = this.document.createElement("div"); + controls.className = "tc-journal-calendar-view-switcher"; + + controls.appendChild(this.createButton("单月视图","tc-btn-invisible tc-journal-calendar-switch" + (currentView === "month" ? " tc-selected" : ""),function() { + self.setState(VIEW_STATE_TITLE,"month"); + })); + controls.appendChild(this.createButton("全部月份","tc-btn-invisible tc-journal-calendar-switch" + (currentView === "all" ? " tc-selected" : ""),function() { + self.setState(VIEW_STATE_TITLE,"all"); + })); + this.containerNode.appendChild(controls); +}; + +JournalCalendarWidget.prototype.renderMonthControls = function(selectedMonth) { + if(isValidMonth(this.month)) { + return; + } + + var self = this, + selected = parseMonth(selectedMonth), + controls = this.document.createElement("div"); + controls.className = "tc-journal-calendar-month-controls"; + + function setOffset(offset) { + var date = new Date(selected.year,selected.month - 1 + offset,1); + self.setState(MONTH_STATE_TITLE,monthKey(date.getFullYear(),date.getMonth() + 1)); + } + + controls.appendChild(this.createButton("上个月","tc-btn-invisible tc-journal-calendar-nav",function() { + setOffset(-1); + })); + controls.appendChild(this.createButton("本月","tc-btn-invisible tc-journal-calendar-nav",function() { + var now = new Date(); + self.setState(MONTH_STATE_TITLE,monthKey(now.getFullYear(),now.getMonth() + 1)); + })); + controls.appendChild(this.createButton("下个月","tc-btn-invisible tc-journal-calendar-nav",function() { + setOffset(1); + })); + this.containerNode.appendChild(controls); +}; + +JournalCalendarWidget.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.metaKey || event.ctrlKey || event.button === 1 + }); +}; + +JournalCalendarWidget.prototype.renderMonth = function(value,byDate) { + var self = this, + selected = parseMonth(value), + section = this.document.createElement("section"), + heading = this.document.createElement("h2"), + viewport = this.document.createElement("div"), + grid = this.document.createElement("div"), + weekdays = ["一","二","三","四","五","六","日"], + firstWeekday = (new Date(selected.year,selected.month - 1,1).getDay() + 6) % 7, + daysInMonth = new Date(selected.year,selected.month,0).getDate(), + today = new Date(), + index; + + section.className = "tc-journal-calendar-month"; + heading.className = "tc-journal-calendar-month-title"; + heading.textContent = selected.year + "年" + selected.month + "月"; + section.appendChild(heading); + viewport.className = "tc-journal-calendar-viewport"; + grid.className = "tc-journal-calendar-grid"; + + weekdays.forEach(function(weekday) { + var weekdayNode = self.document.createElement("div"); + weekdayNode.className = "tc-journal-calendar-weekday"; + weekdayNode.textContent = weekday; + grid.appendChild(weekdayNode); + }); + + for(index = 0; index < firstWeekday; index++) { + var blank = this.document.createElement("div"); + blank.className = "tc-journal-calendar-day tc-journal-calendar-day-empty"; + grid.appendChild(blank); + } + + for(var day = 1; day <= daysInMonth; day++) { + var key = dateKey(selected.year,selected.month,day), + journal = byDate[key], + dayNode = this.document.createElement("div"), + numberNode; + + dayNode.className = "tc-journal-calendar-day" + (journal ? " tc-journal-calendar-day-has-journal" : ""); + if(selected.year === today.getFullYear() && selected.month === today.getMonth() + 1 && day === today.getDate()) { + dayNode.className += " tc-journal-calendar-day-today"; + } + + if(journal) { + numberNode = this.createButton(day,"tc-btn-invisible tc-journal-calendar-day-number tc-journal-calendar-day-link",function(title) { + return function(event) { + self.navigateTo(title,event.currentTarget,event); + }; + }(journal.title)); + numberNode.title = "打开日志:" + journal.title; + numberNode.setAttribute("aria-label","打开日志:" + journal.title); + } else { + numberNode = this.document.createElement("div"); + numberNode.className = "tc-journal-calendar-day-number"; + numberNode.textContent = day; + } + dayNode.appendChild(numberNode); + + if(journal) { + journal.tags.forEach(function(tag) { + var tagButton = self.createButton(tag,"tc-btn-invisible tc-tag-label tc-journal-calendar-tag",function(event) { + self.navigateTo(tag,event.currentTarget,event); + }); + tagButton.title = "打开标签:" + tag; + tagButton.setAttribute("aria-label","打开标签:" + tag); + dayNode.appendChild(tagButton); + }); + } + grid.appendChild(dayNode); + } + + viewport.appendChild(grid); + section.appendChild(viewport); + this.containerNode.appendChild(section); +}; + +JournalCalendarWidget.prototype.renderCalendar = function() { + var data = this.getJournalData(), + currentView = this.getCurrentView(); + + this.renderViewSwitcher(currentView); + + if(currentView === "all") { + if(!data.months.length) { + var empty = this.document.createElement("div"); + empty.className = "tc-journal-calendar-empty"; + empty.textContent = "目前没有日志。"; + this.containerNode.appendChild(empty); + return; + } + data.months.forEach(function(month) { + this.renderMonth(month,data.byDate); + },this); + return; + } + + var selectedMonth = this.getCurrentMonth(); + this.renderMonthControls(selectedMonth); + this.renderMonth(selectedMonth,data.byDate); +}; + +JournalCalendarWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(Object.keys(changedAttributes).length || Object.keys(changedTiddlers).length) { + this.refreshSelf(); + return true; + } + return false; +}; + +exports["journal-calendar"] = JournalCalendarWidget; + +})(); diff --git a/tiddlers/plugin-task-board-edit-fields.tid b/tiddlers/plugin-task-board-edit-fields.tid new file mode 100644 index 0000000..dd8a351 --- /dev/null +++ b/tiddlers/plugin-task-board-edit-fields.tid @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..f3c81b1 --- /dev/null +++ b/tiddlers/plugin-task-board-styles.tid @@ -0,0 +1,641 @@ +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 new file mode 100644 index 0000000..5a2a75a --- /dev/null +++ b/tiddlers/plugin-task-board-view-fields.tid @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..397811b --- /dev/null +++ b/tiddlers/plugin-task-board-widget.tid @@ -0,0 +1,729 @@ +title: $:/plugins/local/task-board/widget.js +type: application/javascript +module-type: widget + +/*\ +title: $:/plugins/local/task-board/widget.js +type: application/javascript +module-type: widget + +Task board with kanban, list, calendar, and Gantt views. +\*/ +(function() { +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget, + VIEW_STATE_TITLE = "$:/state/task-board/view", + MONTH_STATE_TITLE = "$:/state/task-board/month", + DAY_MS = 24 * 60 * 60 * 1000, + DAY_WIDTH = 30, + STATUSES = ["待处理","进行中","等待","完成"], + PRIORITIES = ["P1","P2","P3"], + VIEW_LABELS = { + board: "看板", + list: "列表", + calendar: "日历", + gantt: "甘特图" + }; + +var TaskBoardWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +TaskBoardWidget.prototype = new Widget(); +TaskBoardWidget.prototype.constructor = TaskBoardWidget; + +function pad(value) { + return value < 10 ? "0" + value : "" + value; +} + +function todayKey() { + var now = new Date(); + return now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate()); +} + +function monthKey(date) { + return date.getUTCFullYear() + "-" + pad(date.getUTCMonth() + 1); +} + +function parseDate(value) { + var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || ""), + date; + if(!match) { + return null; + } + date = new Date(0); + date.setUTCHours(0,0,0,0); + date.setUTCFullYear(Number(match[1]),Number(match[2]) - 1,Number(match[3])); + if(date.getUTCFullYear() !== Number(match[1]) || date.getUTCMonth() !== Number(match[2]) - 1 || date.getUTCDate() !== Number(match[3])) { + return null; + } + return date; +} + +function dateKey(date) { + return date.getUTCFullYear() + "-" + pad(date.getUTCMonth() + 1) + "-" + pad(date.getUTCDate()); +} + +function dayNumber(date) { + return Math.floor(date.getTime() / DAY_MS); +} + +function addDays(date,amount) { + return new Date(date.getTime() + amount * DAY_MS); +} + +function isValidMonth(value) { + var match = /^(\d{4})-(\d{2})$/.exec(value || ""); + return !!match && Number(match[2]) >= 1 && Number(match[2]) <= 12; +} + +function priorityRank(priority) { + var rank = PRIORITIES.indexOf(priority); + return rank === -1 ? PRIORITIES.length : rank; +} + +function compareTasks(left,right) { + var priorityDifference = priorityRank(left.effectivePriority) - priorityRank(right.effectivePriority); + if(priorityDifference) { + return priorityDifference; + } + var leftDue = left.dueDate ? dayNumber(left.dueDate) : Infinity, + rightDue = right.dueDate ? dayNumber(right.dueDate) : Infinity; + if(leftDue !== rightDue) { + return leftDue - rightDue; + } + return left.title.localeCompare(right.title); +} + +TaskBoardWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + this.containerNode = this.document.createElement("div"); + this.containerNode.className = "tc-task-board"; + parent.insertBefore(this.containerNode,nextSibling); + this.domNodes.push(this.containerNode); + this.renderBoard(); +}; + +TaskBoardWidget.prototype.execute = function() { + this.tag = this.getAttribute("tag","事情"); +}; + +TaskBoardWidget.prototype.createElement = function(tagName,className,text) { + var node = this.document.createElement(tagName); + if(className) { + node.className = className; + } + if(text !== undefined && text !== null) { + node.textContent = text; + } + return node; +}; + +TaskBoardWidget.prototype.createButton = function(label,className,handler) { + var button = this.createElement("button",className,label); + button.setAttribute("type","button"); + button.addEventListener("click",handler); + return button; +}; + +TaskBoardWidget.prototype.setState = function(title,value) { + this.wiki.addTiddler({title: title,text: value}); +}; + +TaskBoardWidget.prototype.getView = function() { + var view = this.wiki.getTiddlerText(VIEW_STATE_TITLE,"board"); + return VIEW_LABELS[view] ? view : "board"; +}; + +TaskBoardWidget.prototype.getMonth = function() { + var value = this.wiki.getTiddlerText(MONTH_STATE_TITLE,""); + if(isValidMonth(value)) { + return value; + } + var today = parseDate(todayKey()); + return monthKey(today); +}; + +TaskBoardWidget.prototype.getTasks = function() { + var self = this, + tasks = []; + + this.wiki.getTiddlersWithTag(this.tag).forEach(function(title) { + var tiddler = self.wiki.getTiddler(title); + if(!tiddler || tiddler.fields["draft.of"]) { + return; + } + var fields = tiddler.fields, + start = fields.start || "", + end = fields.end || "", + due = fields.due || "", + startDate = parseDate(start), + endDate = parseDate(end), + dueDate = parseDate(due), + issues = [], + rangeError, + dateError, + calendarDates = []; + + if(start && !startDate) { + issues.push("开始日期无效"); + } + if(end && !endDate) { + issues.push("结束日期无效"); + } + if(due && !dueDate) { + issues.push("截止日期无效"); + } + if(startDate && endDate && endDate < startDate) { + issues.push("结束日期早于开始日期"); + } + dateError = !!((start && !startDate) || (end && !endDate) || (due && !dueDate)); + rangeError = !!((start && !startDate) || (end && !endDate) || (startDate && endDate && endDate < startDate)); + [ + {type: "start",label: "开始",date: startDate}, + {type: "end",label: "结束",date: endDate}, + {type: "due",label: "截止",date: dueDate} + ].forEach(function(item) { + if(item.date) { + calendarDates.push(item); + } + }); + calendarDates.sort(function(left,right) { + return dayNumber(left.date) - dayNumber(right.date); + }); + if(STATUSES.indexOf(fields.status) === -1) { + issues.push("状态无效"); + } + if(PRIORITIES.indexOf(fields.priority) === -1) { + issues.push("优先级无效"); + } + + tasks.push({ + title: title, + status: fields.status || "", + effectiveStatus: STATUSES.indexOf(fields.status) === -1 ? STATUSES[0] : fields.status, + priority: fields.priority || "", + effectivePriority: PRIORITIES.indexOf(fields.priority) === -1 ? "P2" : fields.priority, + start: start, + end: end, + due: due, + startDate: startDate, + endDate: endDate, + dueDate: dueDate, + rangeValid: !!startDate && !rangeError, + rangeError: rangeError, + dateError: dateError, + calendarDates: calendarDates, + tags: (fields.tags || []).filter(function(tag) {return tag !== self.tag;}), + issues: issues + }); + }); + + return tasks.sort(compareTasks); +}; + +TaskBoardWidget.prototype.navigateTo = function(title,node,event) { + var bounds = node.getBoundingClientRect(); + this.dispatchEvent({ + type: "tm-navigate", + navigateTo: title, + navigateFromTitle: this.getVariable("storyTiddler"), + navigateFromNode: this, + navigateFromClientRect: { + top: bounds.top, + left: bounds.left, + width: bounds.width, + right: bounds.right, + bottom: bounds.bottom, + height: bounds.height + }, + navigateSuppressNavigation: event && (event.metaKey || event.ctrlKey || event.button === 1) + }); +}; + +TaskBoardWidget.prototype.createTaskLink = function(task,className,label) { + var self = this, + button = this.createButton(label !== undefined ? label : task.title,"tc-btn-invisible " + className,function(event) { + self.navigateTo(task.title,event.currentTarget,event); + }); + button.title = "打开事情:" + task.title; + button.setAttribute("aria-label","打开事情:" + task.title); + return button; +}; + +TaskBoardWidget.prototype.renderBoard = function() { + var tasks = this.getTasks(), + view = this.getView(); + this.renderToolbar(view); + if(view === "list") { + this.renderList(tasks); + } else if(view === "calendar") { + this.renderCalendar(tasks); + } else if(view === "gantt") { + this.renderGantt(tasks); + } else { + this.renderKanban(tasks); + } +}; + +TaskBoardWidget.prototype.renderToolbar = function(currentView) { + var self = this, + toolbar = this.createElement("div","tc-task-board-toolbar"), + switcher = this.createElement("div","tc-task-board-switcher"); + + toolbar.appendChild(this.createButton("+ 新建事情","tc-btn-invisible tc-task-board-new",function() { + self.dispatchEvent({ + type: "tm-new-tiddler", + paramObject: { + tags: $tw.utils.stringifyList([self.tag]), + priority: "P2", + status: "待处理", + start: todayKey() + }, + navigateFromTitle: self.getVariable("storyTiddler") + }); + })); + + Object.keys(VIEW_LABELS).forEach(function(view) { + var button = self.createButton(VIEW_LABELS[view],"tc-btn-invisible tc-task-board-view-button" + (view === currentView ? " tc-selected" : ""),function() { + self.setState(VIEW_STATE_TITLE,view); + }); + button.setAttribute("aria-pressed",view === currentView ? "true" : "false"); + switcher.appendChild(button); + }); + toolbar.appendChild(switcher); + this.containerNode.appendChild(toolbar); +}; + +TaskBoardWidget.prototype.renderPriority = function(task) { + var badge = this.createElement("span","tc-task-priority tc-task-priority-" + task.effectivePriority.toLowerCase(),task.priority || "未设置"); + badge.title = "优先级"; + return badge; +}; + +TaskBoardWidget.prototype.renderIssues = function(task,parent) { + if(task.issues.length) { + var issue = this.createElement("span","tc-task-issues","⚠ " + task.issues.join(";")); + issue.title = task.issues.join(";"); + parent.appendChild(issue); + } +}; + +TaskBoardWidget.prototype.renderTaskDates = function(task,parent) { + var values = []; + if(task.start) {values.push("开始 " + task.start);} + if(task.end) {values.push("结束 " + task.end);} + if(task.due) {values.push("截止 " + task.due);} + parent.appendChild(this.createElement("div","tc-task-card-dates",values.length ? values.join(" · ") : "未排期")); +}; + +TaskBoardWidget.prototype.updateTaskStatus = function(title,status) { + var tiddler = this.wiki.getTiddler(title); + if(tiddler && !tiddler.fields["draft.of"] && (tiddler.fields.tags || []).indexOf(this.tag) !== -1 && STATUSES.indexOf(status) !== -1) { + this.wiki.addTiddler(new $tw.Tiddler(tiddler,{status: status},this.wiki.getModificationFields())); + } +}; + +TaskBoardWidget.prototype.renderKanban = function(tasks) { + var self = this, + board = this.createElement("div","tc-task-kanban"); + + STATUSES.forEach(function(status) { + var column = self.createElement("section","tc-task-kanban-column tc-task-status-" + STATUSES.indexOf(status)), + columnTasks = tasks.filter(function(task) {return task.effectiveStatus === status;}), + heading = self.createElement("h2","tc-task-kanban-heading",status + " · " + columnTasks.length), + cards = self.createElement("div","tc-task-kanban-cards"); + + column.setAttribute("data-task-status",status); + column.addEventListener("dragover",function(event) { + event.preventDefault(); + column.classList.add("tc-drag-over"); + }); + column.addEventListener("dragleave",function() { + column.classList.remove("tc-drag-over"); + }); + column.addEventListener("drop",function(event) { + event.preventDefault(); + column.classList.remove("tc-drag-over"); + var title = self.draggedTaskTitle; + if(title) { + self.updateTaskStatus(title,status); + } + self.draggedTaskTitle = null; + }); + + column.appendChild(heading); + columnTasks.forEach(function(task) { + var card = self.createElement("article","tc-task-card"); + card.setAttribute("draggable","true"); + card.setAttribute("data-task-title",task.title); + card.addEventListener("dragstart",function(event) { + self.draggedTaskTitle = task.title; + if(event.dataTransfer) { + event.dataTransfer.setData("application/x-tiddlywiki-task",task.title); + event.dataTransfer.effectAllowed = "move"; + } + card.classList.add("tc-dragging"); + }); + card.addEventListener("dragend",function() { + self.draggedTaskTitle = null; + card.classList.remove("tc-dragging"); + }); + var cardTop = self.createElement("div","tc-task-card-top"); + cardTop.appendChild(self.createTaskLink(task,"tc-task-card-title")); + cardTop.appendChild(self.renderPriority(task)); + card.appendChild(cardTop); + self.renderTaskDates(task,card); + if(task.tags.length) { + card.appendChild(self.createElement("div","tc-task-card-tags",task.tags.join(" · "))); + } + self.renderIssues(task,card); + cards.appendChild(card); + }); + if(!columnTasks.length) { + cards.appendChild(self.createElement("div","tc-task-empty-column","暂无事情")); + } + column.appendChild(cards); + board.appendChild(column); + }); + this.containerNode.appendChild(board); +}; + +TaskBoardWidget.prototype.renderList = function(tasks) { + var self = this, + wrapper = this.createElement("div","tc-task-list-wrapper"), + table = this.createElement("table","tc-task-list"), + thead = this.createElement("thead"), + headerRow = this.createElement("tr"), + tbody = this.createElement("tbody"); + + ["事情","优先级","状态","开始","结束","截止"].forEach(function(label) { + headerRow.appendChild(self.createElement("th","",label)); + }); + thead.appendChild(headerRow); + table.appendChild(thead); + + tasks.forEach(function(task) { + var row = self.createElement("tr","tc-task-list-row" + (task.issues.length ? " tc-task-invalid" : "")), + titleCell = self.createElement("td","tc-task-list-title"); + titleCell.setAttribute("data-label","事情"); + titleCell.appendChild(self.createTaskLink(task,"tc-task-list-link")); + self.renderIssues(task,titleCell); + row.appendChild(titleCell); + [ + ["优先级",task.priority || "-"], + ["状态",task.status || "-"], + ["开始",task.start || "-"], + ["结束",task.end || "-"], + ["截止",task.due || "-"] + ].forEach(function(item) { + var cell = self.createElement("td","",item[1]); + cell.setAttribute("data-label",item[0]); + row.appendChild(cell); + }); + tbody.appendChild(row); + }); + + if(!tasks.length) { + var emptyRow = this.createElement("tr"), + emptyCell = this.createElement("td","tc-task-empty","还没有事情"); + emptyCell.setAttribute("colspan","6"); + emptyRow.appendChild(emptyCell); + tbody.appendChild(emptyRow); + } + + table.appendChild(tbody); + wrapper.appendChild(table); + this.containerNode.appendChild(wrapper); +}; + +TaskBoardWidget.prototype.renderMonthControls = function(value) { + var self = this, + controls = this.createElement("div","tc-task-calendar-controls"), + parts = value.split("-"), + year = Number(parts[0]), + month = Number(parts[1]); + + function move(offset) { + self.setState(MONTH_STATE_TITLE,monthKey(new Date(Date.UTC(year,month - 1 + offset,1)))); + } + + controls.appendChild(this.createButton("上个月","tc-btn-invisible tc-task-month-button",function() {move(-1);})); + controls.appendChild(this.createElement("h2","tc-task-calendar-title",year + "年" + month + "月")); + controls.appendChild(this.createButton("本月","tc-btn-invisible tc-task-month-button",function() { + self.setState(MONTH_STATE_TITLE,monthKey(parseDate(todayKey()))); + })); + controls.appendChild(this.createButton("下个月","tc-btn-invisible tc-task-month-button",function() {move(1);})); + this.containerNode.appendChild(controls); +}; + +TaskBoardWidget.prototype.renderCalendar = function(tasks) { + var self = this, + value = this.getMonth(), + parts = value.split("-"), + year = Number(parts[0]), + month = Number(parts[1]), + firstDate = new Date(Date.UTC(year,month - 1,1)), + firstWeekday = (firstDate.getUTCDay() + 6) % 7, + daysInMonth = new Date(Date.UTC(year,month,0)).getUTCDate(), + calendar = this.createElement("div","tc-task-calendar-grid"), + header = this.createElement("div","tc-task-calendar-header"), + weeks = this.createElement("div","tc-task-calendar-weeks"), + weekdays = ["一","二","三","四","五","六","日"], + unscheduled = tasks.filter(function(task) { + return !task.calendarDates.length || task.dateError || task.rangeError; + }), + today = todayKey(), + monthStart = firstDate, + monthEnd = new Date(Date.UTC(year,month - 1,daysInMonth)), + calendarStart = addDays(firstDate,-firstWeekday), + weekCount = Math.ceil((firstWeekday + daysInMonth) / 7); + + this.renderMonthControls(value); + calendar.setAttribute("role","grid"); + header.setAttribute("role","row"); + weeks.setAttribute("role","rowgroup"); + weekdays.forEach(function(weekday) { + var heading = self.createElement("div","tc-task-calendar-weekday",weekday); + heading.setAttribute("role","columnheader"); + header.appendChild(heading); + }); + calendar.appendChild(header); + + for(var weekIndex = 0; weekIndex < weekCount; weekIndex++) { + var weekStart = addDays(calendarStart,weekIndex * 7), + weekEnd = addDays(weekStart,6), + week = this.createElement("div","tc-task-calendar-week"), + items = [], + lanes = []; + week.setAttribute("role","row"); + + tasks.forEach(function(task) { + if(task.calendarDates.length) { + var firstCalendarDate = task.calendarDates[0].date, + lastCalendarDate = task.calendarDates[task.calendarDates.length - 1].date, + segmentStart = new Date(Math.max(firstCalendarDate.getTime(),monthStart.getTime(),weekStart.getTime())), + segmentEnd = new Date(Math.min(lastCalendarDate.getTime(),monthEnd.getTime(),weekEnd.getTime())); + if(segmentStart <= segmentEnd) { + items.push({ + task: task, + type: task.calendarDates.length === 1 ? "point" : "range", + start: dayNumber(segmentStart) - dayNumber(weekStart) + 1, + end: dayNumber(segmentEnd) - dayNumber(weekStart) + 1, + starts: dateKey(segmentStart) === dateKey(firstCalendarDate), + ends: dateKey(segmentEnd) === dateKey(lastCalendarDate), + dateLabel: task.calendarDates[0].label + }); + } + if(task.calendarDates.length === 3) { + var middle = task.calendarDates[1]; + if(middle.date >= monthStart && middle.date <= monthEnd && middle.date >= weekStart && middle.date <= weekEnd) { + var middleColumn = dayNumber(middle.date) - dayNumber(weekStart) + 1; + items.push({task: task,type: "milestone",start: middleColumn,end: middleColumn,dateLabel: middle.label}); + } + } + } + }); + + items.forEach(function(item) { + var lane = 0; + while(lanes[lane] && lanes[lane].some(function(occupied) { + return item.start <= occupied.end && item.end >= occupied.start; + })) { + lane++; + } + lanes[lane] = lanes[lane] || []; + lanes[lane].push(item); + item.lane = lane; + }); + + week.style.gridTemplateRows = "minmax(2.25rem,1fr) repeat(" + lanes.length + ",1.5rem)"; + for(var column = 0; column < 7; column++) { + var cellDate = addDays(weekStart,column), + inMonth = cellDate >= monthStart && cellDate <= monthEnd, + key = dateKey(cellDate), + cell = self.createElement("div","tc-task-calendar-day" + (!inMonth ? " tc-task-calendar-blank" : "") + (key === today ? " tc-task-calendar-today" : "")); + cell.style.gridColumn = (column + 1) + ""; + cell.style.gridRow = "1 / span " + (lanes.length + 1); + cell.setAttribute("role","gridcell"); + cell.setAttribute("aria-label",key); + if(inMonth) { + cell.appendChild(self.createElement("div","tc-task-calendar-day-number",cellDate.getUTCDate())); + } + week.appendChild(cell); + } + + items.forEach(function(item) { + var className = item.type === "range" ? "tc-task-calendar-range" + (item.starts ? " tc-range-start" : "") + (item.ends ? " tc-range-end" : "") : item.type === "point" ? "tc-task-calendar-point" : "tc-task-calendar-milestone", + label = item.type === "point" || item.type === "milestone" ? item.dateLabel + " · " + item.task.title : item.task.title, + entry = self.createTaskLink(item.task,className,label); + entry.style.gridColumn = item.start + " / " + (item.end + 1); + entry.style.gridRow = (item.lane + 2) + ""; + entry.setAttribute("data-priority",item.task.effectivePriority); + week.appendChild(entry); + }); + weeks.appendChild(week); + } + + calendar.appendChild(weeks); + this.containerNode.appendChild(calendar); + this.renderUnscheduled(unscheduled,"未排期或日期异常"); +}; + +TaskBoardWidget.prototype.renderUnscheduled = function(tasks,label) { + var self = this; + if(!tasks.length) { + return; + } + var section = this.createElement("section","tc-task-unscheduled"), + list = this.createElement("div","tc-task-unscheduled-list"); + section.appendChild(this.createElement("h3","",label + " · " + tasks.length)); + tasks.forEach(function(task) { + list.appendChild(self.createTaskLink(task,"tc-task-unscheduled-link")); + }); + section.appendChild(list); + this.containerNode.appendChild(section); +}; + +TaskBoardWidget.prototype.renderGantt = function(tasks) { + var self = this, + datedTasks = tasks.filter(function(task) {return task.rangeValid || (!task.start && task.endDate) || task.dueDate;}), + unscheduled = tasks.filter(function(task) {return task.rangeError || (!task.rangeValid && !(!task.start && task.endDate) && !task.dueDate);}), + dates = []; + + datedTasks.forEach(function(task) { + var renderedDates = []; + if(task.rangeValid) { + renderedDates.push(task.startDate,task.endDate || task.startDate); + } else if(!task.start && task.endDate) { + renderedDates.push(task.endDate); + } + if(task.dueDate) { + renderedDates.push(task.dueDate); + } + renderedDates.forEach(function(date) { + if(date) {dates.push(dayNumber(date));} + }); + }); + if(!dates.length) { + this.containerNode.appendChild(this.createElement("div","tc-task-empty","没有可绘制的日期")); + this.renderUnscheduled(unscheduled,"未排期事情"); + return; + } + + var minimum = Math.min.apply(Math,dates) - 2, + maximum = Math.max.apply(Math,dates) + 2, + span = maximum - minimum + 1, + step = Math.max(1,Math.ceil(span / 366)), + units = Math.ceil(span / step), + labelReserve = Math.max.apply(Math,datedTasks.map(function(task) { + return task.title.length * 14 + 32; + }).concat([180])), + startDate = new Date(minimum * DAY_MS), + wrapper = this.createElement("div","tc-task-gantt-scroll"), + gantt = this.createElement("div","tc-task-gantt"), + header = this.createElement("div","tc-task-gantt-row tc-task-gantt-header"), + headerLabel = this.createElement("div","tc-task-gantt-label","事情"), + headerTimeline = this.createElement("div","tc-task-gantt-timeline tc-task-gantt-header-timeline"), + monthTrack = this.createElement("div","tc-task-gantt-months"), + dayTrack = this.createElement("div","tc-task-gantt-days"), + monthGroups = [], + today = parseDate(todayKey()), + todayOffset = (dayNumber(today) - minimum) / step; + + gantt.style.setProperty("--task-gantt-width",units * DAY_WIDTH + labelReserve + "px"); + gantt.style.setProperty("--task-gantt-date-width",units * DAY_WIDTH + "px"); + header.appendChild(headerLabel); + for(var index = 0; index < units; index++) { + var date = addDays(startDate,index * step), + key = monthKey(date), + group = monthGroups[monthGroups.length - 1], + day = this.createElement("div","tc-task-gantt-day",pad(date.getUTCDate())); + if(!group || group.key !== key) { + group = {key: key,date: date,count: 0}; + monthGroups.push(group); + } + group.count++; + day.style.width = DAY_WIDTH + "px"; + day.setAttribute("title",dateKey(date) + (step > 1 ? " 起 " + step + " 天" : "")); + day.setAttribute("aria-label",dateKey(date) + (step > 1 ? " 起 " + step + " 天" : "")); + dayTrack.appendChild(day); + } + monthGroups.forEach(function(group) { + var segment = self.createElement("div","tc-task-gantt-month-segment"), + label = self.createElement("span","tc-task-gantt-month-label",group.date.getUTCFullYear() + "年" + (group.date.getUTCMonth() + 1) + "月"); + segment.style.width = group.count * DAY_WIDTH + "px"; + segment.setAttribute("title",label.textContent); + segment.appendChild(label); + monthTrack.appendChild(segment); + }); + headerTimeline.appendChild(monthTrack); + headerTimeline.appendChild(dayTrack); + header.appendChild(headerTimeline); + gantt.appendChild(header); + + datedTasks.forEach(function(task) { + var row = self.createElement("div","tc-task-gantt-row"), + label = self.createElement("div","tc-task-gantt-label"), + timeline = self.createElement("div","tc-task-gantt-timeline"), + barStart = task.startDate, + barEnd = task.rangeValid ? (task.endDate || task.startDate) : null; + label.appendChild(self.createTaskLink(task,"tc-task-gantt-task-link")); + label.appendChild(self.renderPriority(task)); + self.renderIssues(task,label); + + if(barStart && barEnd) { + var left = (dayNumber(barStart) - minimum) / step * DAY_WIDTH, + width = (dayNumber(barEnd) - dayNumber(barStart) + 1) / step * DAY_WIDTH, + barWidth = Math.max(width,DAY_WIDTH), + bar = self.createTaskLink(task,"tc-task-gantt-bar",""), + barLabel = self.createTaskLink(task,"tc-task-gantt-bar-label"); + bar.style.left = left + "px"; + bar.style.width = barWidth + "px"; + bar.setAttribute("data-priority",task.effectivePriority); + barLabel.style.left = (left + barWidth + 8) + "px"; + timeline.appendChild(bar); + timeline.appendChild(barLabel); + } else if(!task.start && task.endDate) { + var endMarker = self.createTaskLink(task,"tc-task-gantt-end","◆"); + endMarker.style.left = ((dayNumber(task.endDate) - minimum) / step * DAY_WIDTH + DAY_WIDTH / 2) + "px"; + endMarker.title = "计划结束:" + task.end; + timeline.appendChild(endMarker); + } + if(task.dueDate) { + var dueMarker = self.createTaskLink(task,"tc-task-gantt-due","◆"); + dueMarker.style.left = ((dayNumber(task.dueDate) - minimum) / step * DAY_WIDTH + DAY_WIDTH / 2) + "px"; + dueMarker.title = "截止:" + task.due; + timeline.appendChild(dueMarker); + } + if(todayOffset >= 0 && todayOffset < units) { + var todayLine = self.createElement("span","tc-task-gantt-today-line"); + todayLine.style.left = (todayOffset * DAY_WIDTH + DAY_WIDTH / 2) + "px"; + timeline.appendChild(todayLine); + } + row.appendChild(label); + row.appendChild(timeline); + gantt.appendChild(row); + }); + + wrapper.appendChild(gantt); + this.containerNode.appendChild(wrapper); + this.renderUnscheduled(unscheduled,"未排期事情"); +}; + +TaskBoardWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(Object.keys(changedAttributes).length || Object.keys(changedTiddlers).length) { + this.refreshSelf(); + return true; + } + return false; +}; + +exports["task-board"] = TaskBoardWidget; + +})(); diff --git a/tiddlers/sidebar-current-tiddler-toc.tid b/tiddlers/sidebar-current-tiddler-toc.tid deleted file mode 100644 index 9d73d6d..0000000 --- a/tiddlers/sidebar-current-tiddler-toc.tid +++ /dev/null @@ -1,69 +0,0 @@ -title: $:/plugins/local/current-tiddler-toc/sidebar -type: text/vnd.tiddlywiki -tags: $:/tags/SideBar -caption: 目录 -list-after: $:/core/ui/SideBar/More - - - -<$current-tiddler-toc/> 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" new file mode 100644 index 0000000..4f4567f --- /dev/null +++ "b/tiddlers/\344\272\213\346\203\205\347\234\213\346\235\277.tid" @@ -0,0 +1,6 @@ +created: 20260726132452393 +modified: 20260726132452393 +title: 事情看板 +type: text/vnd.tiddlywiki + +<$task-board tag="事情"/> -- cgit v1.2.3