Doing Google Calendar style dragging-todo-to-agenda in TiddlyWiki

I’ve been using this for a while, but just add the doc for it now.

Read EventCalendarSideBar tiddler on doc site to learn how to build your own draggable todo list.

Switch language of doc site use this button on top right

And open Event Calendar alternative page layout using this button.

Toggle sidebar using top right button on Event Calendar layout, and try drag a todo to the agenda.

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.

2 Likes

This is interesting @linonetwo where you are using the great features of Google Calendar for daily calendar.

  • I would love to take googles features further in a tiddlywiki implementation but I think its too difficult.

Google’s product is prone to shutdown, like rss reader and google+. So it is better to reimplement good things into TW and use it offline.

2 Likes

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

1 Like

Hey @linonetwo, this is incredibly cool, I am definitely going to incorporate this into my workflow :slight_smile:

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

Have replicated on an empty as well.


I have implemented an independent css for this sidebar, because it wasn’t playing nicely with how I’ve got mine configured:

.event-calendar-sidebar {
    width: 25vw !important;
    max-width: 25vw !important;
    min-width: 200px;
    right: 0;
    position: relative;
    display: none;
    height: calc(100vh - 50px); /* Adjust the 50px based on your header height */
    overflow-y: auto; /* Enable vertical scrolling */
    overflow-x: hidden; /* Hide horizontal scrollbar */
}


.tc-sidebar-scrollable:not(.tc-sidebar-hidden) .event-calendar-sidebar {
    display: block;
}


.tc-sidebar-scrollable.tc-sidebar-hidden .event-calendar-sidebar {
    overflow: hidden !important;
}


.event-calendar-sidebar::-webkit-scrollbar {
    width: 6px;
}

.event-calendar-sidebar::-webkit-scrollbar-track {
    background: #f1f1f1;
}

.event-calendar-sidebar::-webkit-scrollbar-thumb {
    background: #888;
    border-radius: 3px;
}

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?

Hi, glad to see someone else is also using it.

It is based on https://fullcalendar.io/ , so it should have supports google calendar connection and recurring event, but I haven’t implement them in tw-calendar, because it involves how to saving data in tiddler fields, there is an issue for it feature request: support for recurence · Issue #70 · tiddly-gittly/tiddlywiki-calendar · GitHub

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.

Currently it only supports

data-event=`{ "duration": "01:00" }` data-tags="xxx"

I use data-tags

, so event tiddler is always tagged with the todo item tiddler. And I use GitHub - tiddly-gittly/inverse-link-and-folder: Add inverse link (bi-directional link) and folder information to the bottom of tiddlers plugin to get relationship in standard layout.

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:

image

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.

1 Like

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 :slight_smile:

$__plugins_linonetwo_tw-calendar_calendar-widget_widget.js.tid (1.3 MB)

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 :wink: Will check in again later.


EDIT: Almost forgot, you’ll need this stylesheet too:

$__plugins_linonetwo_tw-calendar_calendar-widget_widget.css.tid (654 Bytes)

Though this is functional, I have 2 observations about this method

  1. 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
  2. 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.

1 Like

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 :slight_smile: 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

1 Like