aboutsummaryrefslogtreecommitdiffstats
path: root/tiddlers/plugin-journal-calendar-widget.tid
diff options
context:
space:
mode:
Diffstat (limited to 'tiddlers/plugin-journal-calendar-widget.tid')
-rw-r--r--tiddlers/plugin-journal-calendar-widget.tid333
1 files changed, 333 insertions, 0 deletions
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;
+
+})();