const obsidian = require('obsidian'); const Plugin = obsidian.Plugin; const ItemView = obsidian.ItemView; const getIcon = obsidian.getIcon; const DateFormat = { months: ["januarie", "februarie", "maart", "april", "mei", "juni", "juli", "augustis", "september", "oktober", "november", "december"], daysLong: ["Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"], week: "y[0-9]{2}w[0-9]{2}", days: ["zo", "ma", "di", "wo", "do", "vrij", "za"], time: "[0-9]{2}:[0-9]{2}[ap]m", date: "" } DateFormat.date = "(?:" + DateFormat.week + ")?(?: ?(?:" + DateFormat.days.join('|') + ") ?)?(?:" + DateFormat.time + ")?"; function parseSingleDate(d, defal) { let year = -1, week = -1, dow = -1, time = moment(defal); d = d.toLowerCase(); // year let res = d.match(/y([0-9]{2})/); if (res !== null) { year = parseInt(res[1]) + 2000; } else { year = defal.isoWeekYear(); } // week res = d.match(/w([0-9]{2})/); if (res !== null) { week = parseInt(res[1]); } else { week = defal.isoWeek(); } // day of week res = d.split(' '); for (const p in res) { let i = DateFormat.days.indexOf(res[p]); if (i != -1){ dow = i; break; } } if (dow == -1) { dow = defal.day() } // time res = d.match(/([0-9]{2}:[0-9]{2}[ap]m)/); if (res !== null) { time = moment(res[1], "hh:mma"); } // combine time.day(dow); time.isoWeek(week); time.isoWeekYear(year); return time; } function stringifySingleDate(date) { let week = date.format("[y]GG[w]WW"); let dow = DateFormat.days[date.day()]; let time = date.format("hh:mma"); return [week, dow, time].join(' '); } function stringifyDate(date) { let res = stringifySingleDate(date[0]); if (date[1].diff(date[0]) > 1000) { res += "-" + stringifySingleDate(date[1]) } return res; } function parseDate(d, defal) { let start, end; let res = d.match(/([^-]+)-([^-]+)/); if (res !== null) { start = parseSingleDate(res[1], defal); end = parseSingleDate(res[2], start); } else { start = parseSingleDate(d, defal); end = start; } return [start, end] } function scanEditor(editor, data) { let lineNum = 0, lineCount = editor.lineCount(); let defal = parseSingleDate("00:00am", moment()); while ((lineNum < lineCount)) { let res, line = editor.getLine(lineNum); res = line.match("weekboek: \"?(" + DateFormat.date + ")\"?"); if (res != null) { defal = parseSingleDate(res[1], defal); } res = [...line.matchAll("\\[(" + DateFormat.date + "(?:-" + DateFormat.date + ")?)\\]")]; if (res.length > 0) { if (res[0][0] == "[-]" && res.length > 1) { res[0] = res[1]; } if (res[0][0] != "[-]") { res = res[0] res[1] = parseDate(res[1], defal); let procesed = processLine(line, line.replace(res[0], ''), res[1], data); data = procesed['data']; editor.setLine(lineNum, procesed['line']); } } lineNum++; } return data; } function scanFile(content, data) { let lines = content.split('\n'); let defal = parseSingleDate("00:00am", moment()); for (let lineNum in lines) { let res = lines[lineNum].match("weekboek: \"?(" + DateFormat.date + ")\"?"); if (res != null) { defal = parseSingleDate(res[1], defal); } res = [...lines[lineNum].matchAll("\\[(" + DateFormat.date + "(?:-" + DateFormat.date + ")?)\\]")]; if (res.length > 0) { if (res[0][0] == "[-]" && res.length > 1) { res[0] = res[1]; } if (res[0][0] != "[-]") { res = res[0] res[1] = parseDate(res[1], defal); let procesed = processLine(lines[lineNum], lines[lineNum].replace(res[0], ''), res[1], data); data = procesed['data']; lines[lineNum] = procesed['line']; } } } return { data: data, content: lines.join('\n') }; } function processLine(line, noDate, date, data) { let res; let item = { "id": null, "date": stringifyDate(date), "title": "Untitled", "group": "default", "location": "", "state": "reminder", "type": "reminder" }; // id res = noDate.match(/<([a-z0-9]+)\/>/); if (res != null) { item["id"] = res[1]; noDate = noDate.replace(res[0], ''); } else { item["id"] = generateId(); line += " <" + item["id"] + "/>"; } // location res = noDate.match(/\{([^\}\{]+)\}/); if (res != null) { item["location"] = res[1]; noDate = noDate.replace(res[0], ''); } // state (checkbox) res = noDate.match(/^[ \t]*- \[(.)\] /); if (res != null) { item['type'] = "task"; item['state'] = res[1]; noDate = noDate.replace('[' + res[1] + '] ', ''); } noDate = noDate.replace(/[ \t][ \t]+/g, ' '); // remve multeple spaces/tabs in a row // title and group res = noDate.match(/^ ?- (.*) - (.*)/); if (res != null) { item["group"] = res[1]; item["title"] = res[2]; } else { // no group look for only title res = noDate.match(/^ ?- (.*)/); if (res != null) { item["title"] = res[1]; } else { item["title"] = noDate; } } // remove leading and ending spaces item["title"] = item['title'].replaceAll(/(^ +)?( +$)?/g, '') item["group"] = item['group'].replaceAll(/(^ +)?( +$)?/g, '') // add/update to database if (data == null) data = {}; data[item["id"]] = item; return { line: line, data: data}; } function generateId() { let id = ''; const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let l = 0; while (l < 10) { id += chars.charAt(Math.floor(Math.random() * chars.length)); l += 1; } return id; } const VIEW_TYPE_CALENDAR = "fr-calendar" class CalendarView extends ItemView { constructor(leaf, plugin) { super(leaf); this.plugin = plugin; this.week = stringifySingleDate(moment()).match(DateFormat.week)[0]; this.zoom = 30/60; // in px/min } getViewType() { return VIEW_TYPE_CALENDAR; } getDisplayText() { return "Calender " + this.week; } async onOpen() { const container = this.containerEl.children[1]; container.empty(); this.eventsEls = {}; // mount view this.mountView = container.createEl('div', { attr: { style: "height:0" } }).createEl("div", { cls: "frcal__mountView" }); // month this.monthEl = container.createEl("div", { cls: "frcal__month" }); let today = this.monthEl.createEl("div", { cls: "frcal__time" }); today.append(getIcon('calendar-days')); today.addEventListener('click', function(e) { this.gotoThisWeek(); }.bind(this)); this.mountFirstEl = this.monthEl.createEl("div", { cls: "frcal__month_first", attr: { style: "flex: 70" } }); this.mountFirstSpanEl = this.mountFirstEl.createEl("span"); this.mountSecondEl = this.monthEl.createEl("div", { cls: "frcal__month_second", attr: { style: "flex: 0" } }); this.mountSecondSpanEl = this.mountSecondEl.createEl("span"); // day header this.headEl = container.createEl("div", { cls: "frcal__days frcal__day_head" }); this.weekNumEl = this.headEl.createEl("div", { cls: "frcal__time" }); this.weekNumEl.addEventListener('click', function(e){ this.openMountView(); }.bind(this)); this.dayHeads = [null, null, null, null, null, null, null]; for (let i = 0; i < 7; i++) { let dayEl = this.headEl.createEl("div", { cls: "frcal__day frcal__day_head_item" }); this.dayHeads[i] = dayEl.createEl("span"); } // all day events this.alldayEl = container.createEl("div", { cls: "frcal__days frcal__day_allday" }); this.alldayEl.createEl("div", { cls: "frcal__time" }); this.allday = [null, null, null, null, null, null, null]; for (let i = 0; i < 7; i++) { this.allday[i] = this.alldayEl.createEl("div", { cls: "frcal__day frcal__day_allday_item" }); } // timed events this.timedEl = container.createEl("div", { cls: "frcal__days frcal__day_timed" }); let timed = this.timedEl.createEl("div", { cls: "frcal__time" }); timed.createEl("div", { cls: "frcal__hour am", text: '12' }); for (let i = 1; i <= 12; i++) { timed.createEl("div", { cls: "frcal__hour " + ((i == 12) ? "pm" : "am"), text: i.toString() }); } for (let i = 1; i <= 11; i++) { timed.createEl("div", { cls: "frcal__hour pm", text: i.toString() }); } this.timed = [null, null, null, null, null, null, null]; for (let i = 0; i < 7; i++) { this.timed[i] = this.timedEl.createEl("div", { cls: "frcal__day frcal__timed_item" }); } this.addEvents(); this.updateWeek(); } openMountView(startMonth = null, endMonth = null) { this.mountView.empty(); if (startMonth == null) { startMonth = moment().subtract(1, "years"); } if (endMonth == null) { endMonth = moment().add(1, "years"); } if (startMonth.day() != 1) { if (startMonth.day() == 0) { startMonth.day(-6); } else { startMonth.day(1); } } if (endMonth.day() != 1) { if (endMonth.day() == 0) { endMonth.day(-6); } else { endMonth.day(1); } } console.log("mount view: start = ", startMonth, " end = ", endMonth); let lastMonth = moment(startMonth).day(4).year() * 12 + moment(startMonth).day(4).month() - 1; let table = this.mountView.createEl('table'); for (let week = moment(startMonth); week < endMonth; week.add(1, "weeks")) { let date = stringifySingleDate(week).match(DateFormat.week); let row = table.createEl("tr", { attr: { 'id': "frcal_mountview_" + date, 'onclick': "app.workspace.getLeavesOfType('" + VIEW_TYPE_CALENDAR + "')[0].view.mountViewClick('" + date + "')" } }); let thursday = moment(week).day(4); if (lastMonth != thursday.year() * 12 + thursday.month()) { lastMonth += 1; let lastDayOfMount = moment(thursday); let span = 0; while (lastDayOfMount.month() == thursday.month()) { span++; lastDayOfMount.add(1, "weeks"); } // let span = moment(week).add(1, "months").date(4).isoWeek() - week.isoWeek(); row.createEl("td", { attr: { rowspan: span }, cls: 'frcal__month_month' }).innerText = (thursday.month() + 1).toString() + " - " + DateFormat.months[thursday.month()]; } row.createEl('td', { cls: 'frcal__weekinmonth' }).innerText = week.isoWeek(); let day=moment(week) for (let i = 7; i > 0; i--) { let classes = 'frcal__dayinmonth'; classes += ((day.month()%2 == 1) ? ' frcal__evenMonth' : ' frcal__oddMonth'); classes += (day.format("y-MM-D") == moment().format("y-MM-D")) ? ' frcal__today' : '' row.createEl('td', { cls: classes }).innerText = day.date().toString(); day.add(1, "day"); } } this.mountView.style.display = "block"; setTimeout(() => { document.getElementById('frcal_mountview_' + stringifySingleDate(moment()).match(DateFormat.week)).scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' }); }, 10); } mountViewClick(week) { this.mountView.style.display = "none"; this.gotoThisWeek(week); } async onClose() { // Nothing to clean up. } addEvents() { this.backButtonEl.ariaDisabled = "false"; this.backButtonEl.addEventListener('click', this.gotoPreviusWeek.bind(this)); this.forwardButtonEl.ariaDisabled = "false"; this.forwardButtonEl.addEventListener('click', this.gotoNextWeek.bind(this)); } gotoPreviusWeek() { let date = parseSingleDate(this.week + ' ma 12:00am', null).subtract(7, 'days'); this.week = stringifySingleDate(date).match(DateFormat.week)[0]; this.updateWeek(); } gotoNextWeek() { let date = parseSingleDate(this.week + ' ma 12:00am', null).add(7, 'days'); this.week = stringifySingleDate(date).match(DateFormat.week)[0]; this.updateWeek(); } gotoThisWeek(week = '') { if (week == '') { this.week = stringifySingleDate(moment()).match(DateFormat.week)[0]; } else if (typeof week == 'string') { // format of this.week is very strikt, this way every format the parser suports will work. this.week = stringifySingleDate(parseSingleDate(week, moment())).match(DateFormat.week)[0]; } else { this.week = stringifySingleDate(week).match(DateFormat.week)[0]; } this.updateWeek(); } updateWeek() { let monthF = parseSingleDate(this.week + " ma 00:00am",).month(); this.mountFirstSpanEl.innerText = (monthF+1).toString() + " - " + DateFormat.months[monthF]; this.weekNumEl.innerHTML = this.week.substring(0, 3) + '
' + this.week.substring(3, 6); let zoDate = parseSingleDate(this.week + " zo 00:00am"); let monthS = zoDate.month(); if (monthF != monthS) { this.mountSecondSpanEl.innerText = (monthS+1).toString() + " - " + DateFormat.months[monthS]; let dow = zoDate.date(); this.mountFirstEl.setAttr("style", "flex: " + ((7-dow)*10).toString()); this.mountSecondEl.setAttr("style", "flex: " + ((dow)*10).toString()) } else { this.mountSecondSpanEl.innerText = ""; this.mountFirstEl.setAttr("style", "flex: 70") this.mountSecondEl.setAttr("style", "flex: 0") } let today = parseSingleDate("12:00pm", moment()); for (let day = 0; day < 7; day++) { let date = parseSingleDate(this.week + " " + DateFormat.days[(day + 1) % 7] + " 12:00pm"); this.dayHeads[day].innerHTML = DateFormat.daysLong[day] + "
" + date.date().toString(); if (date.diff(today, "minutes") == 0) { this.dayHeads[day].style.color = "var(--color-accent)"; } else { this.dayHeads[day].style.color = ""; } } this.updateEvents(); } updateEvents() { // let item = { // "id": null, // "date": stringifyDate(date), // "allDay": false, // "title": "Untitled", // "group": "default", // "location": null, // "state": "reminder" // }; // clear events let today = parseSingleDate("12:00pm", moment()); for (let day = 0; day < 7; day++) { this.allday[day].innerHTML = ""; this.timed[day].innerHTML = ""; // add hour lines let hourlinesEl = this.timed[day].createEl("div", { cls: "frcal__hourlines" }); hourlinesEl.createEl("div", { cls: "frcal__hourline", attr: { style: "margin-top: 0px"} }) for (let hour = 0; hour < 11; hour++) { hourlinesEl.createEl("div", { cls: "frcal__hourline", attr: { style: "margin-top: " + (120*this.zoom-1).toString() + "px"} }) } // add current time line if (parseSingleDate(this.week + " " + DateFormat.days[(day+1) % 7] + " 12:00pm").diff(today, "minutes") == 0) { let time = moment(); time = time.hour()*60 + time.minute(); this.timed[day].createEl("div", { cls: "frcal__hourlines" }) .createEl("div", { cls: "frcal__hourline frcal__now", attr: { style: "top: " + (time*this.zoom-1).toString() + "px"} }) } } let start = parseSingleDate(this.week + " ma 00:00am"); let end = parseSingleDate(this.week + " zo 11:59pm"); for (let event in this.plugin.data) { event = this.plugin.data[event]; let date = parseDate(event['date']) let startInWeek = (date[0] >= start && date[0] <= end); let endInWeek = (date[1] >= start && date[1] <= end); if (startInWeek && endInWeek) { let res = [...event.date.matchAll("(?:" + DateFormat.days.join('|') + ")")]; if ((res.length <= 1) || (res[0][0] == res[1][0])) { this.renderEvent(event); } else { let date_split = event.date.split('-'); let day_start = DateFormat.days.indexOf(res[0][0]), day_end = DateFormat.days.indexOf(res[1][0]); let part = JSON.parse(JSON.stringify(event)); // first day part.date = date_split[0].match(DateFormat.week)[0] + ' ' + DateFormat.days[day_start] + ' ' + date_split[0].match(DateFormat.time)[0]; part.date += '-' + date_split[1].match(DateFormat.week)[0] + ' ' + DateFormat.days[day_start] + ' 23:59pm'; this.renderEvent(part); // middel days for (let day = day_start+1; day < ((day_end == 0) ? 7 : day_end); day++) { part.date = date_split[0].match(DateFormat.week)[0] + ' ' + DateFormat.days[day] + ' 12:00am'; part.date += '-' + date_split[1].match(DateFormat.week)[0] + ' ' + DateFormat.days[day] + ' 23:59pm'; this.renderEvent(part); } // last day part.date = date_split[0].match(DateFormat.week)[0] + ' ' + DateFormat.days[day_end] + ' 12:00am'; part.date += '-' + date_split[1].match(DateFormat.week)[0] + ' ' + DateFormat.days[day_end] + ' ' + date_split[0].match(DateFormat.time)[0]; this.renderEvent(part); } } else if (startInWeek || endInWeek) { console.warn('multi week event not suported', event); console.log(date, start, end); } } } renderEvent(event) { let time = parseDate(event.date); let container = {}, allDay; let day = time[0].day() - 1; day = (day == -1) ? 6 : day; if (event.date.match(DateFormat.time)[0] == '12:00am') { container = this.allday[day]; allDay = true; } else { container = this.timed[day]; allDay = false; } time[0] = time[0].hour()*60 + time[0].minute(); time[1] = time[1].hour()*60 + time[1].minute(); // check for concurrent events let classes = "frcal__event"; let concurrentClash = ""; for (let i in container.children) { let child = container.children[i]; if ((child instanceof HTMLElement) && child.hasClass("frcal__event")) { if ( (time[1] > parseInt(child.dataset.start)) && (time[0] < parseInt(child.dataset.end)) ) { if (child.dataset.concurrentClash != "") { let third = document.getElementById("fr_event_" + child.dataset.concurrentClash); if ( (time[1] > parseInt(third.dataset.start)) && (time[0] < parseInt(third.dataset.end)) ) { console.warn("triple concurrent clashes are not suported with " + event.id + ", " + child.dataset.id + " and " + child.dataset.concurrentClash); } else { classes += (child.hasClass("frcal__event_concurrent2")) ? " frcal__event_concurrent1" : " frcal__event_concurrent2"; } } else { if (time[0] < parseInt(child.dataset.start)) { classes += " frcal__event_concurrent1"; child.addClass("frcal__event_concurrent2"); } else { child.addClass("frcal__event_concurrent1"); classes += " frcal__event_concurrent2"; } child.dataset.concurrentClash = event.id; concurrentClash = child.dataset.id; } } } } let el = container.createEl('div', { cls: classes, attr: { 'id': "fr_event_" + event.id, 'data-id': event.id, 'data-group': event.group, 'data-state': event.state, 'data-type': event.type, 'data-start': time[0].toString(), 'data-end': time[1].toString(), 'data-concurrent-clash': concurrentClash, 'style': ((allDay) ? '' : 'top:' + (time[0]*this.zoom).toString() + 'px') }, }); let eventEl = el.createEl('span', { attr: { 'style': ((allDay) ? '' : 'min-height:' + ((time[1] - time[0]) * this.zoom).toString() + 'px') } }); eventEl.addEventListener('mousedown', (e) => { if (e.button == 2) { this.openMenu(event, {x: e.clientX, y: e.clientY}); } }); eventEl.addEventListener('touchstart', (e) => { if (e.touches.length == 1) { this.touchEvent = { "event": event, "location": {x: e.touches[0].clientX, y: e.touches[0].clientY}, "time": new Date().getTime() }; setTimeout(() => { if (this.touchEvent != null && ((new Date().getTime() - this.touchEvent.time) > 700)) { this.openMenu(this.touchEvent.event, this.touchEvent.location); this.touchEvent = null; } }, 750); } else { this.touchEvent = null; } }); eventEl.addEventListener('touchend', (e) => { this.touchEvent = null; }); eventEl.addEventListener('touchmove', (e) => { this.touchEvent = null; }); switch (event.type) { case 'task': if (event.state == ' ') { eventEl.createEl('input', { cls: 'task-list-item-checkbox', attr: { 'data-task': event.state, 'type': 'checkbox' } }); } else { eventEl.createEl('input', { cls: 'task-list-item-checkbox is-checked', attr: { 'data-task': event.state, 'type': 'checkbox', 'checked': '' } }); } default: eventEl.appendText(event.title); break; } return el; } openMenu(event, location) { let time = [...event.date.matchAll(DateFormat.time)]; if (time.length == 1) { time = time[0]; } else if (time.length == 2) { time = time[0] + "-" + time[1]; } else { time = ""; } let loc = event.location; if (loc != "") { loc = " {" + loc + "}"; } let text = event.group + " " + time + loc; let menu = new obsidian.Menu(); menu.addItem((item) => { item.setIsLabel(true); item.setTitle(text); }); menu.showAtPosition(location); } } class FRCalander extends Plugin { async onload() { this.registerView(VIEW_TYPE_CALENDAR, (leaf) => { return new CalendarView(leaf, this) }); this.addCommand({ id: 'fr-calendar-scan', name: 'scan active file', repeatable: false, callback: async () => { let file = this.app.workspace.getActiveFile(); if (file !== null) { let content = await this.app.vault.read(file); let res = scanFile(content, this.data); this.data = res.data; this.app.vault.modify(file, res.content); } this.saveData(this.data); } }); this.addCommand({ id: 'fr-calendar-scan-all', name: 'scan all files conaining the tag', repeatable: false, callback: async () => { var events = {}; const notes = this.app.vault.getMarkdownFiles(); for (const noteFile of notes) { const fileCachedData = this.app.metadataCache.getFileCache(noteFile) || {}; const tags = obsidian.getAllTags(fileCachedData); if (tags.contains('#fr-calendar')) { let content = await this.app.vault.read(noteFile); let res = scanFile(content, events); events = res.data; this.app.vault.modify(noteFile, res.content); } } this.data = events; this.saveData(this.data); } }); //TODO: scan folder; with DataAdapter // this.addCommand({ // id: 'calnder-save', // name: 'save the calander', // editorCallback: () => { // this.saveData(data); // } // }); // this.addCommand({ // id: 'calnder-load', // name: 'save the calander', // editorCallback: () => { // data = this.loadData(); // } // }); this.addRibbonIcon("calendar", "show fr calender", () => { let leaf = app.workspace.getLeavesOfType(VIEW_TYPE_CALENDAR); if (leaf.length > 0) { leaf = leaf[0]; } else { leaf = app.workspace.getLeavesOfType('empty'); if (leaf.length > 0) { leaf = leaf[0]; } else { leaf = app.workspace.getLeaf('tab'); } } leaf.setViewState({ type: VIEW_TYPE_CALENDAR }); app.workspace.setActiveLeaf(leaf); }); this.data = await this.loadData(); } } module.exports = FRCalander;