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; })();