From 1888ff59743d83bd7776b0f16ef0bc20d68e3e10 Mon Sep 17 00:00:00 2001 From: shu Date: Sun, 26 Jul 2026 19:54:31 +0800 Subject: Add responsive journal calendar --- tiddlers/journal-calendar-widget.tid | 333 +++++++++++++++++++++++++++++++++++ tiddlers/journal-calendar.tid | 195 ++++++++++++++++++++ 2 files changed, 528 insertions(+) create mode 100644 tiddlers/journal-calendar-widget.tid create mode 100644 tiddlers/journal-calendar.tid diff --git a/tiddlers/journal-calendar-widget.tid b/tiddlers/journal-calendar-widget.tid new file mode 100644 index 0000000..cd54724 --- /dev/null +++ b/tiddlers/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/journal-calendar.tid b/tiddlers/journal-calendar.tid new file mode 100644 index 0000000..3a91f45 --- /dev/null +++ b/tiddlers/journal-calendar.tid @@ -0,0 +1,195 @@ +title: 日历 +type: text/vnd.tiddlywiki + + + +<$journal-calendar tag="日志" view="switchable"/> -- cgit v1.2.3