aboutsummaryrefslogtreecommitdiffstats
path: root/tiddlers
diff options
context:
space:
mode:
Diffstat (limited to 'tiddlers')
-rw-r--r--tiddlers/journal-calendar-widget.tid333
-rw-r--r--tiddlers/journal-calendar.tid195
2 files changed, 528 insertions, 0 deletions
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
+
+<style>
+.tc-journal-calendar {
+ width: 100%;
+}
+
+.tc-journal-calendar-view-switcher,
+.tc-journal-calendar-month-controls {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.tc-journal-calendar-switch,
+.tc-journal-calendar-nav {
+ padding: 0.35rem 0.8rem;
+ border: 1px solid <<colour tiddler-border>>;
+ border-radius: 999px;
+}
+
+.tc-journal-calendar-switch.tc-selected {
+ color: <<colour background>>;
+ background: <<colour primary>>;
+}
+
+.tc-journal-calendar-month {
+ margin-bottom: 2rem;
+}
+
+.tc-journal-calendar-month-title {
+ margin: 0 0 0.75rem;
+ text-align: center;
+}
+
+.tc-journal-calendar-viewport {
+ width: 100%;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+}
+
+.tc-journal-calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7,minmax(5.5rem,1fr));
+ min-width: 38.5rem;
+ gap: 1px;
+ background: <<colour tiddler-border>>;
+ border: 1px solid <<colour tiddler-border>>;
+ border-radius: 0.5rem;
+ overflow: hidden;
+}
+
+.tc-journal-calendar-weekday {
+ padding: 0.55rem 0.25rem;
+ font-weight: 600;
+ text-align: center;
+ background: <<colour sidebar-tab-background>>;
+}
+
+.tc-journal-calendar-day {
+ min-height: 6rem;
+ padding: 0.45rem;
+ background: <<colour tiddler-background>>;
+}
+
+.tc-journal-calendar-day-empty {
+ background: <<colour sidebar-tab-background>>;
+}
+
+.tc-journal-calendar-day-has-journal {
+ background: <<colour tiddler-background>>;
+}
+
+.tc-journal-calendar-day-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 1.7rem;
+ height: 1.7rem;
+ color: <<colour background>>;
+ background: <<colour primary>>;
+ border-radius: 50%;
+}
+
+.tc-journal-calendar-day-link:hover,
+.tc-journal-calendar-day-link:focus {
+ text-decoration: underline;
+}
+
+.tc-journal-calendar-day-today .tc-journal-calendar-day-number {
+ box-shadow: 0 0 0 2px <<colour primary>>;
+ border-radius: 50%;
+}
+
+.tc-journal-calendar-day-number {
+ margin-bottom: 0.35rem;
+ font-weight: 600;
+}
+
+.tc-journal-calendar-tag {
+ display: block;
+ max-width: 100%;
+ width: 100%;
+ margin-top: 0.25rem;
+ padding: 0.25rem 0.35rem;
+ text-align: left;
+ line-height: 1.25;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ border-radius: 0.3rem;
+ background: <<colour sidebar-tab-background>>;
+}
+
+.tc-journal-calendar-tag:hover,
+.tc-journal-calendar-tag:focus {
+ text-decoration: underline;
+}
+
+.tc-journal-calendar-empty {
+ padding: 2rem;
+ color: <<colour muted-foreground>>;
+ text-align: center;
+}
+
+@media (max-width: 600px) {
+ .tc-journal-calendar-view-switcher {
+ display: grid;
+ grid-template-columns: repeat(2,minmax(0,1fr));
+ gap: 0.35rem;
+ }
+
+ .tc-journal-calendar-month-controls {
+ display: grid;
+ grid-template-columns: repeat(3,minmax(0,1fr));
+ gap: 0.35rem;
+ }
+
+ .tc-journal-calendar-switch,
+ .tc-journal-calendar-nav {
+ min-height: 2.75rem;
+ padding: 0.35rem 0.25rem;
+ }
+
+ .tc-journal-calendar-month {
+ margin-bottom: 1.25rem;
+ }
+
+ .tc-journal-calendar-month-title {
+ font-size: 1.25rem;
+ }
+
+ .tc-journal-calendar-viewport {
+ overflow-x: visible;
+ }
+
+ .tc-journal-calendar-grid {
+ grid-template-columns: repeat(7,minmax(0,1fr));
+ min-width: 0;
+ }
+
+ .tc-journal-calendar-weekday {
+ padding: 0.45rem 0;
+ font-size: 0.8rem;
+ }
+
+ .tc-journal-calendar-day {
+ min-width: 0;
+ min-height: 4.5rem;
+ padding: 0.2rem;
+ }
+
+ .tc-journal-calendar-day-number {
+ margin-bottom: 0.2rem;
+ font-size: 0.85rem;
+ }
+
+ .tc-journal-calendar-day-link {
+ min-width: 1.75rem;
+ height: 1.75rem;
+ }
+
+ .tc-journal-calendar-tag {
+ margin-top: 0.15rem;
+ padding: 0.15rem 0.2rem;
+ font-size: 0.7rem;
+ text-align: center;
+ }
+}
+</style>
+
+<$journal-calendar tag="日志" view="switchable"/>