You are here

FullCalendar.php in FullCalendar 8.5

File

src/Plugin/views/style/FullCalendar.php
View source
<?php

namespace Drupal\fullcalendar\Plugin\views\style;

use DateTime;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\fullcalendar\Plugin\fullcalendar\type\OptionsFormHelperTrait;
use Drupal\fullcalendar\Plugin\FullcalendarBase;
use Drupal\fullcalendar\Plugin\FullcalendarPluginCollection;
use Drupal\views\Plugin\views\style\StylePluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * @ViewsStyle(
 *   id = "fullcalendar",
 *   title = @Translation("FullCalendar"),
 *   help = @Translation("Displays items on a calendar."),
 *   theme = "views_view--fullcalendar",
 *   display_types = {"normal"}
 * )
 */
class FullCalendar extends StylePluginBase {
  use OptionsFormHelperTrait;

  /**
   * {@inheritdoc}
   */
  protected $usesFields = TRUE;

  /**
   * {@inheritdoc}
   */
  protected $usesGrouping = FALSE;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Entity Field Manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $fieldManager;

  /**
   * Stores the FullCalendar plugins used by this style plugin.
   *
   * @var \Drupal\fullcalendar\Plugin\FullcalendarPluginCollection
   */
  protected $pluginBag;

  /**
   * @var \Drupal\Core\Datetime\DateFormatter $dateFormatter
   *   The date formatter service.
   */
  protected $dateFormatter;

  /**
   * The messenger.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * {@inheritdoc}
   */
  public function evenEmpty() {
    return TRUE;
  }

  /**
   * Get all available FullCalendar plugins.
   *
   * @return \Drupal\fullcalendar\Plugin\FullcalendarPluginCollection|\Drupal\fullcalendar\Plugin\FullcalendarInterface[]
   */
  public function getPlugins() {
    return $this->pluginBag;
  }

  /**
   * Constructs a new Fullcalendar object.
   *
   * @param array $configuration
   * @param string $plugin_id
   * @param mixed $plugin_definition
   * @param \Drupal\Component\Plugin\PluginManagerInterface $fullcalendar_manager
   *   FullCalendar Manager.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
   *   Entity Field Manager.
   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
   *   The date formatter service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, PluginManagerInterface $fullcalendar_manager, ModuleHandlerInterface $module_handler, $field_manager, DateFormatter $date_formatter, MessengerInterface $messenger) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->pluginBag = new FullcalendarPluginCollection($fullcalendar_manager, $this);
    $this->moduleHandler = $module_handler;
    $this->fieldManager = $field_manager;
    $this->dateFormatter = $date_formatter;
    $this->messenger = $messenger;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($configuration, $plugin_id, $plugin_definition, $container
      ->get('plugin.manager.fullcalendar'), $container
      ->get('module_handler'), $container
      ->get('entity_field.manager'), $container
      ->get('date.formatter'), $container
      ->get('messenger'));
  }

  /**
   * {@inheritdoc}
   */
  protected function defineOptions() {
    $options = parent::defineOptions();

    /* @var \Drupal\fullcalendar\Plugin\fullcalendar\type\FullCalendar $plugin */
    foreach ($this
      ->getPlugins() as $plugin) {
      if ($plugin instanceof FullcalendarBase) {
        $options += $plugin
          ->defineOptions();
      }
    }
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);

    /* @var \Drupal\fullcalendar\Plugin\fullcalendar\type\FullCalendar $plugin */
    foreach ($this
      ->getPlugins() as $plugin) {
      if ($plugin instanceof FullcalendarBase) {
        $plugin
          ->buildOptionsForm($form, $form_state);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function validateOptionsForm(&$form, FormStateInterface $form_state) {
    parent::validateOptionsForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitOptionsForm(&$form, FormStateInterface $form_state) {
    parent::submitOptionsForm($form, $form_state);

    /* @var \Drupal\fullcalendar\Plugin\fullcalendar\type\FullCalendar $plugin */
    foreach ($this
      ->getPlugins() as $plugin) {
      if ($plugin instanceof FullcalendarBase) {
        $plugin
          ->submitOptionsForm($form, $form_state);
      }
    }
  }

  /**
   * Extracts date fields from the view.
   */
  public function parseFields() {
    $this->view
      ->initHandlers();
    $labels = $this->displayHandler
      ->getFieldLabels();
    $date_fields = [];

    /** @var \Drupal\views\Plugin\views\field\EntityField $field */
    foreach ($this->view->field as $id => $field) {
      if (fullcalendar_field_is_date($field)) {
        $date_fields[$id] = $labels[$id];
      }
    }
    return $date_fields;
  }

  /**
   * {@inheritdoc}
   */
  public function validate() {
    $settings = $this
      ->prepareSettings();
    if ($this->displayHandler->display['display_plugin'] != 'default' && !$this
      ->parseFields() && empty($settings['google']['googleCalendarApiKey'])) {
      $this->messenger
        ->deleteAll();
      $this->messenger
        ->addWarning($this
        ->t('Display "@display" requires at least one date field unless you are displaying data from a Google Calendar.', [
        '@display' => $this->displayHandler->display['display_title'],
      ]), 'error');
    }
    return parent::validate();
  }

  /**
   * {@inheritdoc}
   * @throws \Exception
   */
  public function render() {
    return [
      '#theme' => $this
        ->themeFunctions(),
      '#view' => $this->view,
      '#options' => $this->options,
      '#attached' => $this
        ->prepareAttached(),
    ];
  }

  /**
   * Load libraries.
   *
   * @throws \Exception
   */
  protected function prepareAttached() {

    /* @var \Drupal\fullcalendar\Plugin\fullcalendar\type\FullCalendar $plugin */
    $attached['library'][] = 'fullcalendar/drupal.fullcalendar';
    $settings = $this
      ->prepareSettings();

    // Only attach assets for enabled FC plugins.
    $fcPlugins = !empty($settings['options']['plugins']) ? $settings['options']['plugins'] : [
      'dayGrid',
    ];
    foreach ($fcPlugins as $fcPlugin) {
      $attached['library'][] = 'fullcalendar/fullcalendar.' . strtolower($fcPlugin);
    }
    if (!empty($settings['options']['themeSystem']) && $settings['options']['themeSystem'] === 'bootstrap') {
      $attached['library'][] = 'fullcalendar/fullcalendar.bootstrap';
    }
    $attached['drupalSettings']['fullcalendar'] = [
      'js-view-dom-id-' . $this->view->dom_id => $settings,
    ];
    return $attached;
  }

  /**
   * Prepare JavaScript settings.
   *
   * @throws \Exception
   */
  protected function prepareSettings() {
    $settings =& drupal_static(__METHOD__, []);
    if (empty($settings)) {

      /* @var \Drupal\fullcalendar\Plugin\fullcalendar\type\FullCalendar $plugin */
      foreach ($this
        ->getPlugins() as $plugin_id => $plugin) {
        $plugin
          ->process($settings);
      }
    }

    // Google Calendar events.
    if (!empty($settings['options']['googleCalendarApiKey'])) {
      $ids = array_map('trim', explode(',', trim($settings['googleCalendarId'])));
      foreach ($ids as $id) {
        $settings['options']['eventSources'][] = [
          'googleCalendarId' => $id,
          'className' => 'fc-event-default',
        ];
      }
    }

    // Drupal events.
    $events = $this
      ->prepareEvents();
    if ($events) {
      $settings['options']['eventSources'][] = $events;
    }
    return $settings;
  }

  /**
   * Prepare events for calendar.
   *
   * @return array
   *   Array of events ready for fullcalendar.
   *
   * @throws \Exception
   */
  protected function prepareEvents() {
    $events = [];
    foreach ($this->view->result as $delta => $row) {

      /** @var \Drupal\Core\Entity\EntityInterface $entity */
      $entity = $row->_entity;

      // Collect all fields for the customize options.
      $fields = [];

      // Collect only date fields.
      $date_fields = [];

      // Collect prepared events.
      $event = [];

      /* @var \Drupal\views\Plugin\views\field\EntityField $field */
      foreach ($this->view->field as $field_name => $field) {
        $fields[$field_name] = $this
          ->getField($delta, $field_name);
        if (fullcalendar_field_is_date($field)) {
          $field_storage_definitions = $this->fieldManager
            ->getFieldStorageDefinitions($field->definition['entity_type']);
          $field_definition = $field_storage_definitions[$field->definition['field_name']];
          $values = $field
            ->getItems($row);
          if (!empty($values)) {
            $date_fields[$field_name] = [
              'value' => $values,
              'field_alias' => $field->field_alias,
              'field_name' => $field_definition
                ->getName(),
              'field_info' => $field_definition,
              'timezone_override' => $field->options['settings']['timezone_override'],
            ];
          }
        }
      }

      // @todo: custom date field?
      // If using a custom date field, filter the fields to process.
      if (!empty($this->options['fields']['date'])) {
        $date_fields = array_intersect_key($date_fields, $this->options['fields']['date_field']);
      }

      // If there are no date fields, return.
      if (empty($date_fields)) {
        return $events;
      }
      foreach ($date_fields as $field) {

        // Filter fields without value.
        if (empty($field['value'])) {
          continue;
        }

        /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_definition */
        $field_definition = $field['field_info'];

        // Get 'min' and 'max' dates appear in the Calendar.
        $date_range = $this
          ->getExposedDates($field['field_name']);

        // "date_recur" field (with recurring date).
        if ($field_definition
          ->getType() == 'date_recur') {

          /** @var \Drupal\date_recur\Plugin\Field\FieldType\DateRecurFieldItemList $field_items */
          $field_items = $row->_entity->{$field['field_name']};
          $isRecurring = FALSE;

          /** @var \Drupal\date_recur\Plugin\Field\FieldType\DateRecurItem $item */
          foreach ($field_items as $index => $item) {

            // Get DateRecur Occurrence Handler.
            $occurrenceHandler = $item
              ->getOccurrenceHandler();

            // If this field is a DateRecur field.
            if ($occurrenceHandler
              ->isRecurring()) {

              // Get a list of occurrences for display.
              $occurrences = $occurrenceHandler
                ->getOccurrencesForDisplay($date_range['min'], $date_range['max']);
              foreach ($occurrences as $occurrence) {

                /** @var \DateTime $start */
                $start = $occurrence['value'];

                /** @var \DateTime $end */
                $end = $occurrence['end_value'];
                $event = $this
                  ->prepareEvent($entity, $field, $index);
              }
              $isRecurring = TRUE;
            }
          }
          if ($isRecurring === TRUE) {

            // At this point, all DateRecur occurrences are merged into $rows
            // so we can continue adding date items with the next field.
            continue;
          }
        }

        // "datetime" and "daterange" fields or "date_recur" field (without
        // recurring date).
        foreach ($field['value'] as $index => $item) {

          // Start time is required!
          if (empty($item['raw']->value)) {
            continue;
          }
          $event = $this
            ->prepareEvent($entity, $date_fields, $index);
        }
      }
      if (!empty($event)) {
        $events[$delta] = $event;
      }
    }
    return $events;
  }

  /**
   * Helper method to prepare an event.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   Event entity.
   * @param $fields
   *
   * @param int $delta
   *   Field delta.
   *
   * @return array
   * @throws \Exception
   */
  private function prepareEvent($entity, $fields, $delta) {
    $classes = $this->moduleHandler
      ->invokeAll('fullcalendar_classes', [
      $entity,
    ]);
    $this->moduleHandler
      ->alter('fullcalendar_classes', $classes, $entity);
    $classes = array_map([
      '\\Drupal\\Component\\Utility\\Html',
      'getClass',
    ], $classes);
    $class = count($classes) ? implode(' ', array_unique($classes)) : '';
    $palette = $this->moduleHandler
      ->invokeAll('fullcalendar_palette', [
      $entity,
    ]);
    $this->moduleHandler
      ->alter('fullcalendar_palette', $palette, $entity);

    // Start/end dates.
    $event_start_end = $this
      ->getEventStartEndDates($fields);
    $event_start = $event_start_end['start'];
    $event_end = $event_start_end['end'];
    $all_day = $this
      ->isAllDayEvent($event_start_end);
    $request_time = \Drupal::time()
      ->getRequestTime();
    $current_time = new DateTime();
    $current_time
      ->setTimestamp($request_time)
      ->format(DateTime::ATOM);

    // Add a class if the event was in the past or is in the future, based
    // on the end time. We can't do this in hook_fullcalendar_classes()
    // because the date hasn't been processed yet.
    if ($all_day && $event_start < $current_time || !$all_day && $event_end < $current_time) {
      $time_class = 'fc-event-past';
    }
    elseif ($event_start > $current_time) {
      $time_class = 'fc-event-future';
    }
    else {
      $time_class = 'fc-event-now';
    }
    $editable = !empty($settings['fullcalendar']['editable']) ? $entity
      ->access('update', NULL, TRUE)
      ->isAllowed() : FALSE;
    return [
      'id' => $entity
        ->id(),
      'groupId' => $entity
        ->getEntityTypeId(),
      'allDay' => $all_day,
      'start' => $event_start,
      'end' => $event_end,
      'editable' => $editable,
      'className' => $class . ' ' . $time_class,
      'title' => strip_tags(htmlspecialchars_decode($entity
        ->label(), ENT_QUOTES)),
      'url' => $entity
        ->toUrl('canonical', [
        'language' => \Drupal::languageManager()
          ->getCurrentLanguage(),
      ])
        ->toString(),
      'backgroundColor' => !empty($palette['backgroundColor']) ? $palette['backgroundColor'] : '',
      'borderColor' => !empty($palette['borderColor']) ? $palette['borderColor'] : '',
      'textColor' => !empty($palette['textColor']) ? $palette['textColor'] : '',
    ];
  }

  /**
   * Get 'min' and 'max' dates appear in the calendar.
   *
   * @param string $field_name
   *   Field machine name.
   *
   * @return mixed
   * @throws \Exception
   */
  public function getExposedDates($field_name) {
    $dates =& drupal_static(__METHOD__, []);
    if (empty($dates[$field_name])) {
      $entity_type = $this->view
        ->getBaseEntityType();
      $entity_type_id = $entity_type
        ->id();
      $settings = $this->view->style_plugin->options;

      /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
      $field_manager = \Drupal::getContainer()
        ->get('entity_field.manager');

      /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storages */
      $field_storages = $field_manager
        ->getFieldStorageDefinitions($entity_type_id);

      /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage */
      $field_storage = $field_storages[$field_name];
      $field_value = $field_storage
        ->getName() . '_value';
      $exposed_input = $this->view
        ->getExposedInput();

      // Min and Max dates for exposed filter.
      $dateMin = new DateTime();
      $dateMax = new DateTime();

      // First, we try to set initial Min and Max date values based on the
      // exposed form values.
      if (isset($exposed_input[$field_value])) {
        $dateMin
          ->setTimestamp(strtotime($exposed_input[$field_value]['min']));
        $dateMax
          ->setTimestamp(strtotime($exposed_input[$field_value]['max']));
      }
      elseif (!empty($settings['date']['month']) && !empty($settings['date']['year'])) {
        $ts = mktime(0, 0, 0, $settings['date']['month'] + 1, 1, $settings['date']['year']);
        $dateMin
          ->setTimestamp($ts);
        $dateMax
          ->setTimestamp($ts);
        $dateMin
          ->modify('first day of this month');
        $dateMax
          ->modify('first day of next month');
      }
      else {
        $dateMin
          ->modify('first day of this month');
        $dateMax
          ->modify('first day of next month');
      }
      $dates[$field_name] = [
        'min' => $dateMin,
        'max' => $dateMax,
      ];
    }
    return $dates[$field_name];
  }

  /**
   * Get start/end dates for an event.
   *
   * @param array $fields
   *  Array of date fields for the event.
   *
   * @return array $event_start_end_date
   *   The array of dates with 'start' and 'end' keys.
   * @throws \Exception
   */
  public function getEventStartEndDates(array $fields) {
    $event_start_end_date = [];
    $field = current($fields);

    /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_info */
    $field_info = $field['field_info'];
    switch ($field_info
      ->getType()) {
      case 'datetime':
        $field_names = array_keys($fields);
        if (count($field_names) == 1) {
          $event_start_end_date['start'] = $this
            ->updateEventTimezone($field['value'][0]['raw']->value, $field['timezone_override']);
          $event_start_end_date['end'] = '';
        }
        else {
          $first = $this
            ->updateEventTimezone($fields[$field_names[0]]['value'][0]['raw']->value, $fields[$field_names[0]]['timezone_override']);
          $second = $this
            ->updateEventTimezone($fields[$field_names[1]]['value'][0]['raw']->value, $fields[$field_names[1]]['timezone_override']);
          if ($first > $second) {
            $event_start_end_date['start'] = $second;
            $event_start_end_date['end'] = $first;
          }
          else {
            $event_start_end_date['start'] = $first;
            $event_start_end_date['end'] = $second;
          }
        }
        break;
      case 'daterange':
        $event_start_end_date['start'] = $this
          ->updateEventTimezone($field['value'][0]['raw']->value, $field['timezone_override']);
        $end = $field['value'][0]['raw']->end_value;
        $event_start_end_date['end'] = !empty($end) ? $this
          ->updateEventTimezone($end, $field['timezone_override']) : '';
        break;
      case 'date_recur':

        // @todo:
        break;
    }
    return $event_start_end_date;
  }

  /**
   * Update a date with timezone.
   *
   * @param string $datetime
   *   A datetime string.
   * @param string $tz_override
   *   Any timezone override for the date.
   *
   * @return string Formatted datetime with timezone applied.
   *   Formatted datetime with timezone applied.
   * @throws \Exception
   */
  public function updateEventTimezone($datetime, $tz_override) {
    $tz = !empty($tz_override) ? $tz_override : date_default_timezone_get();
    $timezone = new \DateTimeZone($tz);
    $dateTimezone = new DateTime($datetime, new \DateTimeZone('UTC'));
    $dateTimezone
      ->setTimezone($timezone);
    return $dateTimezone
      ->format(DateTime::ATOM);
  }

  /**
   * Check whether this is an all-day event.
   *
   * @param array $start_end_date
   *  Array of the start/end dates for the event.
   *
   * @return bool
   *   TRUE, if all day, otherwise FALSE.
   * @throws \Exception
   */
  public function isAllDayEvent(array $start_end_date) {
    if (empty($start_end_date['end'])) {
      $allDay = TRUE;
    }
    else {

      // End of start day
      $end = new DateTime($start_end_date['end']);
      $start = new DateTime($start_end_date['start']);
      $start_ymd = $start
        ->format('Y-m-d');
      $start_day_endtime = new DateTime($start_ymd . 'T23:59:59');
      $allDay = $end
        ->getTimestamp() > $start_day_endtime
        ->getTimestamp();
    }
    return $allDay;
  }

}

Classes

Namesort descending Description
FullCalendar Plugin annotation @ViewsStyle( id = "fullcalendar", title = @Translation("FullCalendar"), help = @Translation("Displays items on a calendar."), theme = "views_view--fullcalendar", display_types = {"normal"} )