saade / filament-fullcalendar

The Most Popular JavaScript Calendar as a Filament Widget
MIT License
291 stars 88 forks source link

Add support for writing custom JS to interact with FullCalendar's render hooks #164

Closed cawecoy closed 8 months ago

cawecoy commented 9 months ago

Possibility to use the eventDidMount Event Render Hook:

return $painel->plugins([
    FilamentFullCalendarPlugin::make()->eventDidMount('calendarEventTooltip')
]);

In the example above we are adding a callback to my calendarEventTooltip Javascript function on the FullCalendar eventDidMount Event Render Hook parameter. The result:

const calendar = new Calendar(this.$el, {
    //...
    eventDidMount: calendarEventTooltip
});

The FullCalendar documentation says:

Customize the rendering of event elements with the following options:

(...)

eventDidMount - called right after the element has been added to the DOM. If the event data changes, this is NOT called again.

FullCalendar even has a demo with sourcecode editable in CodePen on how to use eventDidMount to easily create an event Tooltip.

Similar feature was requested twice on saade/filament-fullcalendar: https://github.com/saade/filament-fullcalendar/issues/162 and https://github.com/saade/filament-fullcalendar/discussions/135.

Currently, one might think we could use the config(array $config) method:

return $painel->plugins([
    FilamentFullCalendarPlugin::make()->config(['eventDidMount' => 'calendarEventTooltip'])
]);

But it has a problem: the callback function is rendered as string (surrounded by quotes).

const calendar = new Calendar(this.$el, {
    //...
    eventDidMount: 'calendarEventTooltip'
});

That generates the following error: Uncaught SyntaxError: Invalid or unexpected token.

In summary, it's currently not possible to use the FullCalendar eventDidMount through this Filament Plugin and this PR will fix that.

Test: implementing the event tooltip on hover

Following the given example exposed before...

return $painel->plugins([
    FilamentFullCalendarPlugin::make()->eventDidMount('calendarEventTooltip')
]);

...additionaly, we just need to add the following Javascript code to our Filament page:

<script>
    function calendarEventTooltip(info){
        info.el.setAttribute("x-tooltip", "tooltip"); // see the tooltip options: https://github.com/ryangjchandler/alpine-tooltip?tab=readme-ov-file#modifiers
        info.el.setAttribute("x-data", "{ tooltip: '"+info.event.title+"' }"); // we can use info.event.description or anything else sent on the Calendar Widget's fetchEvents method
    }
</script>

And we are done (this example uses the Alpine Tooltip package that comes already installed with Filament by default).

image

cawecoy commented 8 months ago

Hi @saade! Is there any chance of this PR being accepted? Thx!

saade commented 8 months ago

I'm thinking on another aproach, what about adding a method eventDidMount on the calendar class, and use like this:

public function eventDidMount(info) {
    return <<<JS
        info.el.setAttribute("x-tooltip", "tooltip");
        info.el.setAttribute("x-data", "{ tooltip: '"+info.event.title+"' }");
    JS;
}

thoughts?

cawecoy commented 8 months ago

@saade it looks like a great approach. But how will the user customize it? What is the "calendar class" exactly?

saade commented 8 months ago

@saade it looks like a great approach. But how will the user customize it? What is the "calendar class" exactly?

the default definition for the function will be:

public function eventDidMount(info) {
    //
}

then the user can override this method in their own widget class that extends the FullCalendarWidget

public function eventDidMount(info) {
    // this is the custom js wrote by the user while overriding the default method
    return <<<JS
        info.el.setAttribute("x-tooltip", "tooltip");
        info.el.setAttribute("x-data", "{ tooltip: '"+info.event.title+"' }");
    JS;
}
cawecoy commented 8 months ago

@saade ok. Another questions:

  1. What is the "calendar class" exactly?
  2. Is your your eventDidMount method a PHP or JavaScript method? I guess it's JavaScript and you used public by mistake?
cawecoy commented 8 months ago

Oh no, it's PHP because of the <<<JS. But you missed $ in the parameter: public function eventDidMount($info)

How would we make $info accessible via JavaScript info.event.title?

saade commented 8 months ago

@saade ok. Another questions:

  1. What is the "calendar class" exactly?
  2. Is your your eventDidMount method a PHP or JavaScript method? I guess it's JavaScript and you used public by mistake?

1.

<?php

namespace App\Filament\Widgets;

use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget;

class CalendarWidget extends FullCalendarWidget
{
    // ....

    public function eventDidMount(info) {
        return <<<JS
            info.el.setAttribute("x-tooltip", "tooltip");
            info.el.setAttribute("x-data", "{ tooltip: '"+info.event.title+"' }");
        JS;
    }

   // ....
}
  1. eventDidMount is a method inside the above class. In the js side you just:
eventDidMount: function (info) {
   this.$wire.eventDidMount(info);
} 
cawecoy commented 8 months ago

@saade ok. Another questions:

  1. What is the "calendar class" exactly?
  2. Is your your eventDidMount method a PHP or JavaScript method? I guess it's JavaScript and you used public by mistake?
<?php

namespace App\Filament\Widgets;

use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget;

class CalendarWidget extends FullCalendarWidget
{
    // ....

    public function eventDidMount(info) {
        return <<<JS
            info.el.setAttribute("x-tooltip", "tooltip");
            info.el.setAttribute("x-data", "{ tooltip: '"+info.event.title+"' }");
        JS;
    }

   // ....
}
  1. eventDidMount is a method inside the above class. In the js side you just:
eventDidMount: function (info) {
   this.$wire.eventDidMount(info);
} 

I've tried that, but it's not working somehow... the events are not displayed. No errors on the browser console log. Any ideas?

saade commented 8 months ago

oh shoot, yes i see why, its something along those lines, but i've expressed myself wrong while writing the snippet. Give me a couple of minutes and i i'll try to implement it

cawecoy commented 8 months ago

Hi @saade! Do you have any update?

cawecoy commented 8 months ago

@saade I finally got to make it work through the approach you suggested:

public function eventDidMount() {
    return <<<JS
        function (info){
            info.el.setAttribute("x-tooltip", "tooltip");
            info.el.setAttribute("x-data", "{ tooltip: '"+info.event.title+"' }");
        }
    JS;
}

How about that?

KhairulAzmi21 commented 8 months ago

I need this PR :)

KhairulAzmi21 commented 8 months ago

@cawecoy . do you have any idea how to styling the tooltip ?

cawecoy commented 8 months ago

I need this PR :)

@KhairulAzmi21 me too. Sometimes the full event content does not fit in the small event container on the calendar - it often ends up partially hidden. So the Tooltip would provide a great UX here by allowing the user to easily see the full event content just by hovering the event.

I am waiting for the maintainer @saade to review my PR (:

@cawecoy . do you have any idea how to styling the tooltip ?

I style it throught this CSS code:

.tippy-box[data-theme~=light]{
    background-color: rgb(var(--gray-700)) !important;
    color: rgb(255 255 255);
}
.tippy-box[data-theme~=light][data-placement^=top]>.tippy-arrow:before{
    border-top-color: rgb(var(--gray-700)) !important;
}
.tippy-box[data-theme~=light][data-placement^=right]>.tippy-arrow:before{
    border-right-color: rgb(var(--gray-700)) !important;
}
.tippy-box[data-theme~=light][data-placement^=bottom]>.tippy-arrow:before{
    border-bottom-color: rgb(var(--gray-700)) !important;
}
.tippy-box[data-theme~=light][data-placement^=left]>.tippy-arrow:before{
    border-left-color: rgb(var(--gray-700)) !important;
}

FilamentPHP uses Tippy.js. I've found that CSS on its official website: https://atomiks.github.io/tippyjs/v6/themes/

saade commented 8 months ago

@cawecoy could you test to see if its still working as desired?

saade commented 8 months ago

@cawecoy? :)

cawecoy commented 8 months ago

@cawecoy could you test to see if its still working as desired?

@saade sure! I'm testing it now (:

cawecoy commented 8 months ago

@saade I just tried all the Event Render Hooks (except eventWillUnmount), they are working as expected.

eventWillUnmount is called right before the event will be removed from the DOM. Any idea on how to remove an event via fullcalendar in order to test it?

saade commented 8 months ago

@cawecoy i think paginating through the months of the calendar will fire it. Since eventDidMount is fired under the same condition

cawecoy commented 8 months ago

@cawecoy i think paginating through the months of the calendar will fire it. Since eventDidMount is fired under the same condition

Great idea! It works!

saade commented 8 months ago

Cool, thank you for your contribution!