diff --git a/modules/backend/ServiceProvider.php b/modules/backend/ServiceProvider.php index 8d352d294d..b522862a35 100644 --- a/modules/backend/ServiceProvider.php +++ b/modules/backend/ServiceProvider.php @@ -89,6 +89,7 @@ protected function registerAssetBundles() $combiner->registerBundle('~/modules/backend/assets/js/winter.js'); $combiner->registerBundle('~/modules/backend/widgets/table/assets/js/build.js'); $combiner->registerBundle('~/modules/backend/widgets/mediamanager/assets/js/mediamanager-browser.js'); + $combiner->registerBundle('~/modules/backend/widgets/calendar/assets/less/calendar.less'); $combiner->registerBundle('~/modules/backend/widgets/mediamanager/assets/less/mediamanager.less'); $combiner->registerBundle('~/modules/backend/widgets/reportcontainer/assets/less/reportcontainer.less'); $combiner->registerBundle('~/modules/backend/widgets/table/assets/less/table.less'); diff --git a/modules/backend/behaviors/CalendarController.php b/modules/backend/behaviors/CalendarController.php index 2fe1599d4f..9590c5f28b 100644 --- a/modules/backend/behaviors/CalendarController.php +++ b/modules/backend/behaviors/CalendarController.php @@ -53,6 +53,16 @@ class CalendarController extends ControllerBehavior */ protected array $requiredConfig = ['modelClass', 'searchList']; + /** + * @var array Visible actions in context of the controller + */ + protected $actions = ['calendar']; + + /** + * @var mixed Configuration for this behaviour + */ + public $calendarConfig = 'config_calendar.yaml'; + /** * Behavior constructor */ @@ -60,9 +70,11 @@ 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); + /* + * Build configuration + */ + $config = $controller->calendarConfig ?: $this->calendarConfig; + $this->setConfig($config, $this->requiredConfig); } /** @@ -72,7 +84,7 @@ public function calendar(): void { $this->controller->pageTitle = $this->controller->pageTitle ? : Lang::get($this->getConfig( 'title', - 'luketowers.calendarwidget::lang.behaviors.calendar.title' + 'backend::lang.calendar.title' )); $this->controller->bodyClass = 'slim-container'; $this->makeCalendar(); diff --git a/modules/backend/behaviors/calendarcontroller/docs/example.config_calendar.yaml b/modules/backend/behaviors/calendarcontroller/docs/example.config_calendar.yaml index 934f51d0df..44483d686b 100644 --- a/modules/backend/behaviors/calendarcontroller/docs/example.config_calendar.yaml +++ b/modules/backend/behaviors/calendarcontroller/docs/example.config_calendar.yaml @@ -5,56 +5,49 @@ # Model to use for getting the records to display on the calendar modelClass: Author\Plugin\Models\Event +# Calendar Title +title: 'backend::lang.calendar.title' + # 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 - +recordUrl: author/plugins/events/update/: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 +# Record property used as the title displayed on the calendar recordTitle: name -# The property to use as the start time for the record -recordStart: start_time +# Record property used as the start time +recordStart: start_at + +# Record property used as the end time +recordEnd: end_at -# The property to use as the end time for the record -recordEnd: end_time +# Record property used as all day long event +recordAllDay: all_day -# The property to use as the background color displayed on the record, , '' = the default background color in the calendar.less +# Record property used 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 +# Record property used as the content of the tooltip recordTooltip: [recordTitle] +# Calendar widget theme color for buttons ('' for default, primary or secondary) +calendarTheme: + # Available display modes to be supported in this instance availableDisplayModes: [month, week, day, list] +# Default view for calendar widget (month, week, day or list) +initialView: month + +# First day of week, 0=Sun, 1=Mon ... +firstDay: 0 + # 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 @@ -63,7 +56,31 @@ toolbar: # Search widget configuration search: prompt: backend::lang.list.search_prompt + +# The filter config file for the controller 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 + + +# 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) + +# load one month of records at a time, ensure they stay loaded between month pages diff --git a/modules/backend/widgets/Calendar.php b/modules/backend/widgets/Calendar.php index bc331b9b83..84b0975446 100644 --- a/modules/backend/widgets/Calendar.php +++ b/modules/backend/widgets/Calendar.php @@ -69,6 +69,11 @@ class Calendar extends WidgetBase */ public string $recordEnd = 'end_at'; + /** + * The model property to use as all day long event for the record + */ + public string $recordAllDay = 'all_day'; + /** * The model property to use to show the background color of this record, '' = the default background color in the calendar.less */ @@ -79,13 +84,28 @@ class Calendar extends WidgetBase */ public string|array $recordTooltip = ''; + /** + * Calendar widget theme color for buttons ('' for default, primary or secondary) + */ + public string $calendarTheme = ''; + /** * Display modes to allow ['month', 'week', 'day', 'list'] */ public array $availableDisplayModes = []; /** - * Calendar of CSS classes to apply to the Calendar container element + * Initial calendar view, one of month, week, day, list + */ + public ?string $initialView = 'month'; + + /** + * First day of week, 0=Sun, 1=Mon ... + */ + public ?int $firstDay = 0; + + /** + * Array of CSS classes to apply to the Calendar container element */ public array $cssClasses = []; @@ -93,6 +113,17 @@ class Calendar extends WidgetBase // INTERNAL // + /** + * Available display modes for fullcalendar.js widget + */ + protected array $fullCalendarModes = [ + // 'year' => 'multiMonthYear', + 'month' => 'dayGridMonth', + 'week' => 'timeGridWeek', + 'day' => 'timeGridDay', + 'list' => 'listMonth' + ]; + /** * Collection of functions to apply to each list query. */ @@ -129,6 +160,7 @@ class Calendar extends WidgetBase public function init() { $this->fillFromConfig([ + // 'model', 'columns', 'recordUrl', 'recordOnClick', @@ -136,11 +168,15 @@ public function init() 'recordTitle', 'recordStart', 'recordEnd', + 'recordAllDay', 'recordColor', 'recordTooltip', 'previewMode', 'searchList', + 'calendarTheme', 'availableDisplayModes', + 'initialView', + 'firstDay', ]); // Initialize the search columns @@ -153,7 +189,13 @@ public function init() } $this->searchColumns = $columns; - $this->calendarVisibleColumns = [$this->recordTitle, $this->recordStart, $this->recordEnd]; + $this->calendarVisibleColumns = [ + $this->recordTitle, + $this->recordStart, + $this->recordEnd, + ]; + + // $this->validateModel(); } /** @@ -179,23 +221,11 @@ public function getRecordUrl(Model $record): ?string */ protected function loadAssets() { - $this->addCss(['packages/core/main.min.css', 'packages/list/main.min.css', 'packages/daygrid/main.min.css', 'packages/timegrid/main.min.css'], '4.1.0'); - $this->addCss(['less/calendar.less'], 'Winter.Core'); - - //Tooltip - $this->addJs('packages/vendor/popper.min.js', '4.1.0'); - $this->addJs('packages/vendor/tooltip.min.js', '4.1.0'); - - //Calendar - $this->addJs('packages/core/main.min.js', '4.1.0'); - $this->addJs('packages/list/main.min.js', '4.1.0'); - $this->addJs('packages/daygrid/main.min.js', '4.1.0'); - $this->addJs('packages/timegrid/main.min.js', '4.1.0'); - $this->addJs('packages/interaction/main.min.js', '4.1.0'); - - // @see https://fullcalendar.io/docs/v4/timeZone - $this->addJs('packages/moment-timezone/main.min.js', '4.1.0'); + $this->addJs('vendor/fullcalendar/index.global.min.js', '6.1.15'); + $this->addJs('vendor/fullcalendar/locales-all.global.min.js', '6.1.15'); + // $this->addCss(['less/calendar.less'], 'Winter.Core'); + $this->addCss('css/calendar.css', 'Winter.Core'); $this->addJs('js/calendar.cache.js', 'Winter.Core'); $this->addJs('js/calendar.js', 'Winter.Core'); } @@ -205,33 +235,66 @@ protected function loadAssets() */ public function prepareVars() { + if (!empty($this->calendarTheme)) { + $this->cssClasses[] = $this->calendarTheme; + } + $this->vars['availableDisplayModes'] = $this->getDisplayModes(); + $this->vars['initialView'] = $this->getInitialView(); + $this->vars['firstDay'] = $this->firstDay; $this->vars['cssClasses'] = implode(' ', $this->cssClasses); } + /** + * Validate the supplied form model. + * + * @return mixed + */ + protected function validateModel() + { + if (!$this->model) { + throw new ApplicationException(Lang::get( + 'backend::lang.form.missing_model', + ['class'=>get_class($this->controller)] + )); + } + + $this->data = isset($this->data) + ? (object) $this->data + : $this->model; + + return $this->model; + } + + /** + * Get the fullcalendar.js initial view to be used + */ + protected function getInitialView(): string + { + if (!empty($this->fullCalendarModes[$this->initialView])) { + return $this->fullCalendarModes[$this->initialView]; + } + + return 'dayGridMonth'; + } + /** * Get the fullcalendar.js display modes to be used */ - protected function getDisplayModes(): array + protected function getDisplayModes(): string { // Convert our display modes to FullCalendar display modes if (!is_array($this->availableDisplayModes)) { $this->availableDisplayModes = [$this->availableDisplayModes]; } - $fullCalendarModes = [ - 'month' => 'dayGridMonth', - 'week' => 'timeGridWeek', - 'day' => 'timeGridDay', - 'list' => 'listMonth' - ]; - $selectedModes = []; foreach ($this->availableDisplayModes as $mode) { - if (!empty($fullCalendarModes[$mode])) { - $selectedModes[] = $fullCalendarModes[$mode]; + if (!empty($this->fullCalendarModes[$mode])) { + $selectedModes[] = $this->fullCalendarModes[$mode]; } } + return implode(',', $selectedModes); } @@ -624,7 +687,8 @@ public function prepareQuery($startTime = 0, $endTime = 0) * @param QueryBuilder $query * @return string md5 */ - protected function getCacheKey($query){ + protected function getCacheKey($query) + { $bindings = array_map(function ($binding) { return (string)$binding; }, $query->getBindings()); @@ -673,7 +737,6 @@ public function getRecords($startTime = 0 , $endTime = 0) $records = $event; } - $events = []; $timeZone = new DateTimeZone(Config::get('app.timezone','UTC')); @@ -699,7 +762,7 @@ public function getRecords($startTime = 0 , $endTime = 0) 'title' => $record->{$this->recordTitle}, 'start' => $record->{$this->recordStart}, 'end' => $record->{$this->recordEnd}, - 'allDay' => (bool) $record->allDay, + 'allDay' => (bool) $record->{$this->recordAllDay}, 'color' => empty($this->recordColor) ? '' : $record->{$this->recordColor}, 'tooltip' => $tooltip ], $timeZone); @@ -843,7 +906,7 @@ protected function getMonthStartEndTime() } - /** + /** * Event handler for refreshing the calendar. * The search widget will call onRefresh * @see CalendarController->initToolbar diff --git a/modules/backend/widgets/calendar/assets/css/calendar.css b/modules/backend/widgets/calendar/assets/css/calendar.css new file mode 100644 index 0000000000..4c66b00cbe --- /dev/null +++ b/modules/backend/widgets/calendar/assets/css/calendar.css @@ -0,0 +1,9 @@ +.calendar-container{--fc-event-bg-color:#0594cd;--fc-event-border-color:#035e82;--fc-list-event-hover-bg-color:#d3f2fe;--fc-button-text-color:#fff;--fc-button-bg-color:#656d79;--fc-button-border-color:#656d79;--fc-button-hover-bg-color:#2896b2;--fc-button-hover-border-color:#2896b2;--fc-button-active-bg-color:#2896b2;--fc-button-active-border-color:#2896b2;padding:0 20px 1px;font-size:15px} +.calendar-container.primary{--fc-button-text-color:#fff;--fc-button-bg-color:#2896b2;--fc-button-border-color:#24849d;--fc-button-hover-bg-color:#2896b2;--fc-button-hover-border-color:#2896b2;--fc-button-active-bg-color:#2896b2;--fc-button-active-border-color:#2896b2} +.calendar-container.secondary{--fc-button-text-color:#405261;--fc-button-bg-color:#e1e2e4;--fc-button-border-color:#d4d5d8;--fc-button-hover-bg-color:#e1e2e4;--fc-button-hover-border-color:#e1e2e4;--fc-button-active-bg-color:#e1e2e4;--fc-button-active-border-color:#e1e2e4} +.calendar-container .loading-indicator{background-color:rgba(0,0,0,0.02)} +.calendar-container .calendar-control{margin:0 auto} +.calendar-container .calendar-tooltip{position:absolute;z-index:9999;color:black;width:150px;border-radius:3px;box-shadow:0 0 2px rgba(0,0,0,0.5);text-align:center} +.calendar-container .calendar-tooltip[x-placement^="top"]{margin-bottom:5px} +.calendar-container .calendar-tooltip[x-placement^="top"] .tooltip-arrow{border-width:5px 5px 0 5px;border-left-color:transparent;border-right-color:transparent;border-bottom-color:transparent;bottom:-5px;left:calc(45%);margin-top:0;margin-bottom:0} +.calendar-container .calendar-tooltip .tooltip-arrow{width:0;height:0;border-style:solid;position:absolute;margin:5px;border-color:#34495e} \ No newline at end of file diff --git a/modules/backend/widgets/calendar/assets/css/fullcalendar.css b/modules/backend/widgets/calendar/assets/css/fullcalendar.css deleted file mode 100644 index 936a41f13c..0000000000 --- a/modules/backend/widgets/calendar/assets/css/fullcalendar.css +++ /dev/null @@ -1,1439 +0,0 @@ -/*! - * FullCalendar v4.0.0-alpha.4 - * Docs & License: https://fullcalendar.io/ - * (c) 2018 Adam Shaw - */ -.fc { - direction: ltr; - text-align: left; } - -.fc-rtl { - text-align: right; } - -body .fc { - /* extra precedence to overcome jqui */ - font-size: 1em; } - -/* Colors ---------------------------------------------------------------------------------------------------*/ -.fc-highlight { - /* when user is selecting cells */ - background: #bce8f1; - opacity: .3; } - -.fc-bgevent { - /* default look for background events */ - background: #8fdf82; - opacity: .3; } - -.fc-nonbusiness { - /* default look for non-business-hours areas */ - /* will inherit .fc-bgevent's styles */ - background: #d7d7d7; } - -/* Buttons (styled