Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of Calendar functionality using FullCalendar v4 #970

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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