This is how I use Google Calendar to arrange my Todos when I was a student. And now this experience is fully open-sourced and hackable on TiddlyWiki platfrom.
Always a pleasure seeing your work, @linonetwo, I look forward to playing around with this – I’d like to ultimately have most events on my calendar backed up in my tiddlywiki and vice-versa
Hey @linonetwo, this is incredibly cool, I am definitely going to incorporate this into my workflow
A small pile of questions and notes:
Is there any native way to push calendar events to one’s google calendar? I already have a system working for doing it with my current setup, but if you’ve already got something native to this plugin that works well I’ll switch to yours.
Also, have you implemented any sort of recurring event feature? I have toyed around with that in the past, but never have gotten very far – would love to see your approach.
I notice that if I open the sidebar and then close (in the calendar view), there remains a gap on the calendar
The resulting sidebar scrolls independently of the calendar, in case you have long todo lists.
What is the relationship between the dragged item and the calendar supposed to be? I am finding that when I make a draggable list of items I can move them into the calendar, but the resulting calendar blocks don’t reflect anything about the tiddler the dragged item links to – am I doing something wrong?
I have already import all things from google calendar, and I have TidGi mobile to use it everywhere, so I don’t need it now, so never consider connect to it. And my life is very casual, no much recurring event, so I never spend time on this feature.
Don’t know why it didn’t trigger refresh, I had look into it but didn’t find a fix. Now quit the layout and reenter will restore it.
I have another week day based todo system, so each day’s todo list won’t be long. I will look at it when I encounter the bug.
I’ve had just a few minutes over coffee to scan the code and toy with some modifications – so far, I’ve found that modifying the eventReceive method seems to be the key to actually “dragging the tiddler into the calendar”
eventReceive(e) {
const tiddlerTitle = e.draggedEl.dataset.title;
if (tiddlerTitle) {
// Get UTC time values
const startUTC = e.event.start.getTime();
const endUTC = e.event.end.getTime();
// Format dates directly from UTC timestamps
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.getUTCFullYear() +
String(date.getUTCMonth() + 1).padStart(2, '0') +
String(date.getUTCDate()).padStart(2, '0') +
String(date.getUTCHours()).padStart(2, '0') +
String(date.getUTCMinutes()).padStart(2, '0') +
String(date.getUTCSeconds()).padStart(2, '0') +
'000';
};
// Update the tiddler
$tw.wiki.setText(tiddlerTitle, "calendarEntry", null, "yes");
$tw.wiki.setText(tiddlerTitle, "startDate", null, formatDate(startUTC));
$tw.wiki.setText(tiddlerTitle, "endDate", null, formatDate(endUTC));
// Try to remove the event from calendar
if (e.event && typeof e.event.remove === 'function') {
e.event.remove();
}
}
}
and then I set <$draggable data-title={{!!title}} data-event=`{ "duration": "00:30" }`>
Because of the particular way I have my todos set up (They are unique timestamp titles and I use streams to generate them) I’ve further modified my implementation to accept data-caption={{!!text}} which generates a nice little calendar block for me:
eventReceive(e) {
const tiddlerTitle = e.draggedEl.dataset.title;
const caption = e.draggedEl.dataset.caption;
if (tiddlerTitle) {
// Get UTC time values
const startUTC = e.event.start.getTime();
const endUTC = e.event.end.getTime();
// Format dates directly from UTC timestamps
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.getUTCFullYear() +
String(date.getUTCMonth() + 1).padStart(2, '0') +
String(date.getUTCDate()).padStart(2, '0') +
String(date.getUTCHours()).padStart(2, '0') +
String(date.getUTCMinutes()).padStart(2, '0') +
String(date.getUTCSeconds()).padStart(2, '0') +
'000';
};
// Update the tiddler's fields
$tw.wiki.setText(tiddlerTitle, "calendarEntry", null, "yes");
$tw.wiki.setText(tiddlerTitle, "startDate", null, formatDate(startUTC));
$tw.wiki.setText(tiddlerTitle, "endDate", null, formatDate(endUTC));
$tw.wiki.setText(tiddlerTitle, "caption", null, caption);
// Remove the event that was automatically created
if (e.event && typeof e.event.remove === 'function') {
e.event.remove();
}
}
}
Will just need to modify the “delete” button to remove the calendar field rather than removing the event altogether – any idea where to start?
Do you mean that you have imported them manually, or you have a method for importing G-Calendar events into your wiki? I would be interested in any method for doing that, I set it aside as a possibility last time I thought to look into it.
I share some calendar events with others, so I will need to continue to use Google Calendar, but being able to see my google calendar events reflected in tiddlywiki without needing to manually move the info would be very nice.
Of course, it is all in e.draggedEl.dataset, if you want to add more field, you can modify this part and send a PR. It is pretty easy with Github Copilot, but the key part is test it in the wiki.
I’d suggest using tag field, because TW will index it.
You can add custom button using $:/tags/ViewTemplate, and only show it via condition <%if [field:calendarEntry[yes]] %>
I think there is a ical import plugin in CPL, you can search for it, but it is an one-way import, can’t sync back.
Toying around with it a bit, have not tested thoroughly enough to say whether there will be wider consequence, but I got the calendar to rerender when the sidebar is closed by adjusting these methods:
render(e, M) {
this.parentDomNode = e;
this.computeAttributes();
this.execute();
// Create elements if they don't exist
if (!this.#containerElement || !this.#mountElement) {
this.#containerElement = document.createElement("div");
this.#mountElement = document.createElement("div");
this.#mountElement.classList.add("tiddlywiki-calendar-widget-container");
this.#containerElement.append(this.#mountElement);
// Handle width/height attributes
const [width, height] = [this.getAttribute("width"), this.getAttribute("height")];
if (width) this.#containerElement.style.width = width;
if (height) {
this.#containerElement.style.height = height;
this.#mountElement.style.minHeight = height;
}
// Setup resize observer instead of connection observer
this.resizeObserver = new ResizeObserver(() => {
if (this.#calendar) {
this.#calendar.updateSize();
}
});
this.resizeObserver.observe(this.#mountElement);
}
if (!this.#calendar) { this.#calendar = initCalendar(this.#mountElement, this.getContext()); requestAnimationFrame(() => {
this.#calendar?.render();
});
} else {
this.#calendar.render();
}
// Append to DOM
this.domNodes.push(this.#containerElement);
e.appendChild(this.#containerElement);
}
and
refresh(b) {
let e = !1; // changed flag
let t = !1; // draft flag
const z = this.getContext();
// Check for draft changes
const hasDraftChanges = Object.keys(b).some(e => e.startsWith(draftTiddlerTitle));
if (hasDraftChanges) {
t = !0;
}
// Check for relevant tiddler changes
const hasTiddlerChanges = Object.keys(b).some(e => {
if (e.startsWith("$:/state/")) return false;
const endDateField = z.endDateFields?.[0] ?? "endDate";
return b[e].modified ? changedTiddlerInViewRange(e, this.#calendar, endDateField) : b[e].deleted;
});
if (hasTiddlerChanges) {
if (t) {
this.#triggerRefetch();
} else {
this.refreshTiddlerEventCalendar();
}
e = !0;
}
// Check for plugin settings changes
if (Object.keys(b).some(e => e.startsWith("$:/plugins/linonetwo/tw-calendar/settings"))) {
this.refreshTiddlerEventCalendar(!0);
e = !0;
}
// Handle sidebar state changes
if (Object.keys(b).some(e => e.startsWith("$:/state/event-calendar-sidebar"))) {
// Add a small delay to ensure DOM has settled
requestAnimationFrame(() => {
this.#calendar?.updateSize();
this.#triggerRefetch();
});
}
// Handle search mode changes
if (getIsSearchMode() && (
b["$:/temp/volatile/linonetwo/tw-calendar/tiddlywiki-ui/PageLayout/EventsCalendarSearchLayout/keywords"]?.modified ||
b["$:/state/linonetwo/tw-calendar/tiddlywiki-ui/PageLayout/EventsCalendarSearchLayout/pagination"]?.modified
)) {
this.refreshTiddlerEventCalendar();
e = !0;
}
this.refreshChildren(b);
return e;
}
Since that was relatively straightforward, I’ve been toying around with a resize function for the sidebar, which I think works pretty well currently – check it out
Just need to trigger the calendar contents to refresh upon resize, but I’ve run out of my morning coffee and it’s time to work out Will check in again later.
EDIT: Almost forgot, you’ll need this stylesheet too:
Though this is functional, I have 2 observations about this method
The correct styling of the calendar layout sidebar goes out the window if the standard layout sidebar is closed (though I have mind render as closed by default and it seems to work fine, leading me to believe it’s the collapsing itself which is triggering the problem), and
The actual tiddlers being transcluded in the sidebar are not expanding to fill the expanded space, which would be the ideal. Currently do not know where to even look for that, but will do some investigating (I imagine it’s a simple CSS problem) if you don’t point me in the right direction first, @linonetwo
EDIT EDIT: Fixed re-render mentioned above and updated the file
Hi @well-noted thanks for the improvment, but I’m not used to sending diff through message or email (I’v heard Linus liks doing so?) To save time finding diff and make it easier to test, hope you can fork and send PR on Github instead, thanks. And that will list you as contributor of the project, that is how Github as a MMORPG is played.
I appreciate your encouragement, @linonetwo, I’m fairly new to participating in github and have been putting off learning forking/PR – But I could use an MMO I’ll look into it and use that method in the future.
I did solve the calendar refresh upon resize issue, as well as identified some leftover code that was creating a redundant resize handle – so I went ahead and updated the above file