Skip to content

Commit

Permalink
Initial implementation of Calendar functionality using FullCalendar v4
Browse files Browse the repository at this point in the history
Need to rebuild on Snowboard / (maybe Vue) with FullCalendar v6.
  • Loading branch information
LukeTowers committed Sep 8, 2023
1 parent 090562a commit 08794be
Show file tree
Hide file tree
Showing 103 changed files with 22,740 additions and 0 deletions.
219 changes: 219 additions & 0 deletions modules/backend/behaviors/CalendarController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php

namespace Backend\Behaviors;

use ApplicationException;
use Backend\Classes\ControllerBehavior;
use Backend\Widgets\Calendar as CalendarWidget;
use Backend\Widgets\Filter as FilterWidget;
use Backend\Widgets\Toolbar as ToolbarWidget;
use Lang;
use stdClass;
use Winter\Storm\Support\Str;
use Winter\Storm\Database\Model;

/**
* Adds features for working with backend records through a Calendar interface.
*
* This behavior is implemented in the controller like so:
*
* public $implement = [
* \Backend\Behaviors\CalendarController::class,
* ];
*
* public $calendarConfig = 'config_calendar.yaml';
*
* The `$calendarConfig` property makes reference to the calendar configuration
* values as either a YAML file, located in the controller view directory,
* or directly as a PHP array.
*
* @package winter\wn-backend-module
* @author Luke Towers
*/
class CalendarController extends ControllerBehavior
{
protected ?ToolbarWidget $toolbarWidget = null;
protected ?FilterWidget $filterWidget = null;
protected ?CalendarWidget $calendarWidget = null;

/**
* The initialized model used by the behavior.
*/
protected Model $model;

/**
* The primary calendar alias to use, default 'calendar'
*/
protected string $primaryDefinition = 'calendar';

/**
* Configuration values that must exist when applying the primary config file.
* - modelClass: Class name for the model
* - searchList: list field definitions for the search widget
*/
protected array $requiredConfig = ['modelClass', 'searchList'];

/**
* Behavior constructor
*/
public function __construct(\Backend\Classes\Controller $controller)
{
parent::__construct($controller);

// Build the configuration
$this->config = $this->makeConfig($controller->calendarConfig, $this->requiredConfig);
$this->config->modelClass = Str::normalizeClassName($this->config->modelClass);
}

/**
* Calendar Controller action
*/
public function calendar(): void
{
$this->controller->pageTitle = $this->controller->pageTitle ? : Lang::get($this->getConfig(
'title',
'luketowers.calendarwidget::lang.behaviors.calendar.title'
));
$this->controller->bodyClass = 'slim-container';
$this->makeCalendar();
}

/**
* Creates the Calendar widget used by this behavior
*/
public function makeCalendar(): CalendarWidget
{
$model = $this->controller->calendarCreateModelObject();

$config = $this->config;
$config->model = $model;
$config->alias = $this->primaryDefinition;

// Initialize the Calendar widget
$widget = $this->makeWidget(CalendarWidget::class, $config);
$widget->model = $model;
$widget->bindToController();
$this->calendarWidget = $widget;

// Initialize the Toolbar & Filter widgets
$this->initToolbar($config, $widget);
$this->initFilter($config, $widget);

return $widget;
}

/**
* Prepare the Toolbar widget if necessary
*/
protected function initToolbar(stdClass $config, CalendarWidget $widget): void
{
if (empty($config->toolbar)) {
return;
}

// Prepare the config and intialize the Toolbar widget
$toolbarConfig = $this->makeConfig($config->toolbar);
$toolbarConfig->alias = $widget->alias . 'Toolbar';
$toolbarWidget = $this->makeWidget(ToolbarWidget::class, $toolbarConfig);
$toolbarWidget->bindToController();
$toolbarWidget->cssClasses[] = 'list-header';

/*
* Link the Search widget to the Calendar widget
*/
if ($searchWidget = $toolbarWidget->getSearchWidget()) {
$searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) {
$widget->setSearchTerm($searchWidget->getActiveTerm());
return $widget->onRefresh();
});

$widget->setSearchOptions([
'mode' => $searchWidget->mode,
'scope' => $searchWidget->scope,
]);

// Find predefined search term
$widget->setSearchTerm($searchWidget->getActiveTerm());
}

$this->toolbarWidget = $toolbarWidget;
}

/**
* Prepare the Filter widget if necessary
*/
protected function initFilter(stdClass $config, CalendarWidget $widget): void
{
if (empty($config->filter)) {
return;
}

$widget->cssClasses[] = 'list-flush';

// Prepare the config and intialize the Toolbar widget
$filterConfig = $this->makeConfig($config->filter);
$filterConfig->alias = $widget->alias . 'Filter';
$filterWidget = $this->makeWidget(FilterWidget::class, $filterConfig);
$filterWidget->bindToController();

/*
* Filter the Calendar when the scopes are changed
*/
$filterWidget->bindEvent('filter.update', function () use ($widget, $filterWidget) {
return $widget->onFilter();
});

// Apply predefined filter values
$widget->addFilter([$filterWidget, 'applyAllScopesToQuery']);
$this->filterWidget = $filterWidget;
$widget->filterWidget = $this->filterWidget;

}

/**
* Creates a new instance of a calendar model. This logic can be changed by overriding it in the controller.
*/
public function calendarCreateModelObject(): Model
{
$class = $this->config->modelClass;
return new $class;
}

/**
* Render the calendar widget
*
* @throws ApplicationException if the calendar widget has not been initialized
*/
public function calendarRender($options = []): string
{
if (empty($this->calendarWidget)) {
throw new ApplicationException(Lang::get('backend::lang.calendar.behavior_not_ready'));
}

if (!empty($options['readOnly']) || !empty($options['disabled'])){
$this->calendarWidget->previewMode = true;
}

if (isset($options['preview'])) {
$this->calendarWidget->previewMode = $options['preview'];
}

return $this->calendarMakePartial('container', [
'toolbar' => $this->toolbarWidget,
'filter' => $this->filterWidget,
'calendar' => $this->calendarWidget,
]);
}

/**
* Render the requested partial, providing opportunity for the controller to take over
*/
public function calendarMakePartial(string $partial, array $params = []): string
{
$contents = $this->controller->makePartial('calendar_' . $partial, $params, false);
if (!$contents) {
$contents = $this->makePartial($partial, $params);
}
return $contents;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# ===================================
# Calendar Behavior Config
# ===================================

# Model to use for getting the records to display on the calendar
modelClass: Author\Plugin\Models\Event

# Search columns
# Used for configuration of additional columns to search by
searchList: $/author/plugin/models/event/columns.yaml

# Record URL
recordUrl: author/plugins/events/update/:event_id


# Record on click
# @see custom.calendar.js sample
# data is a plain object with the following properties:
# startDate: is a JS Date Object
# endDate: is a JS Date Object, may be null
# event: A standard JavaScript object that FullCalendar uses to store information about a calendar event, including id, title, start, end
# eventEl: The HTML element for this event
# recordOnClick: $.wn.eventController.onEventClick(:data, :startDate, :endDate, :event, :eventEl)

# Triggered when the user clicks on a date or a time
# data is a plain object with the following properties
# date: is the a JS Date Object for the clicked day/time.
# dateStr: An ISO8601 string representation of the date
# allDay: true or false
# dayEl: An HTML element that represents the whole-day that was clicked on.
# event: The native JavaScript event with low-level information such as click coordinates.
# view: The current view @see https://fullcalendar.io/docs/v4/view-object
onClickDate: $.wn.availabilitySlotController.onClickDate(:data, :date, :dateStr, :allDay, :dayEl, :event, :view)

# The property to use as the title displayed on the calendar
recordTitle: name

# The property to use as the start time for the record
recordStart: start_time

# The property to use as the end time for the record
recordEnd: end_time

# The property to use as the background color displayed on the record, , '' = the default background color in the calendar.less
recordColor: event_color

# The property to use as the content of the tooltip for the record
recordTooltip: [recordTitle]

# Available display modes to be supported in this instance
availableDisplayModes: [month, week, day, list]

# Flag for whether calendar is read only or editable
previewMode: true

# load one month of records at a time, ensure they stay loaded between month pages

# Toolbar widget configuration
toolbar:
# Partial for toolbar buttons
buttons: calendar_toolbar

# Search widget configuration
search:
prompt: backend::lang.list.search_prompt
filter: calendar_filter.yaml

# when filter gets applied, clear the client's cache of events, essentially start them over
# if they had just loaded this page / month with the current filters applied
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Sample for config_calendar.yaml -> recordOnClick
+ function ($) {
"use strict";

var EventController = function () {

this.onEventClick = function (data, startDate, endDate, event, eventEl) {
alert('eventID = '+ event.id);
}

}
$.wn.eventController = new EventController;

}(window.jQuery);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php if ($toolbar) : ?>
<?= $toolbar->render() ?>
<?php endif ?>

<?php if ($filter) : ?>
<?= $filter->render() ?>
<?php endif ?>

<?= $calendar->render() ?>
4 changes: 4 additions & 0 deletions modules/backend/lang/en/lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@
'trashed_hint_title' => 'This account has been deleted',
'trashed_hint_desc' => 'This account has been deleted and will be unable to be signed in under. To restore it, click the restore user icon in the bottom right',
],
'calendar' => [
'title' => 'Calendar',
'behavior_not_ready' => 'Calendar behavior has not been initialized, check that you have called makeCalendar() in your controller.',
],
'list' => [
'default_title' => 'List',
'search_prompt' => 'Search...',
Expand Down
Loading

0 comments on commit 08794be

Please sign in to comment.