You are here

class DateRecurModularSierraModalOccurrencesForm in Recurring Date Field Modular Widgets 8

Same name and namespace in other branches
  1. 3.x src/Form/DateRecurModularSierraModalOccurrencesForm.php \Drupal\date_recur_modular\Form\DateRecurModularSierraModalOccurrencesForm
  2. 2.x src/Form/DateRecurModularSierraModalOccurrencesForm.php \Drupal\date_recur_modular\Form\DateRecurModularSierraModalOccurrencesForm

Generate a form to excluding occurrences, designed for display in modal.

Hierarchy

Expanded class hierarchy of DateRecurModularSierraModalOccurrencesForm

1 file declares its use of DateRecurModularSierraModalOccurrencesForm
DateRecurModularSierraWidget.php in src/Plugin/Field/FieldWidget/DateRecurModularSierraWidget.php
1 string reference to 'DateRecurModularSierraModalOccurrencesForm'
date_recur_modular.routing.yml in ./date_recur_modular.routing.yml
date_recur_modular.routing.yml

File

src/Form/DateRecurModularSierraModalOccurrencesForm.php, line 29

Namespace

Drupal\date_recur_modular\Form
View source
class DateRecurModularSierraModalOccurrencesForm extends FormBase {
  use DateRecurModularWidgetFieldsTrait;
  use DateRecurModularUtilityTrait;

  /**
   * Date format for exclusion dates.
   */
  protected const UTC_FORMAT = 'Ymd\\THis\\Z';

  /**
   * The PrivateTempStore factory.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
   */
  protected $tempStoreFactory;

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

  /**
   * Constructs a new DateRecurModularSierraModalOccurrencesForm.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   A config factory for retrieving required config objects.
   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempStoreFactory
   *   The PrivateTempStore factory.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
   *   The date formatter service.
   */
  public function __construct(ConfigFactoryInterface $configFactory, PrivateTempStoreFactory $tempStoreFactory, DateFormatterInterface $dateFormatter) {
    $this->configFactory = $configFactory;
    $this->tempStoreFactory = $tempStoreFactory;
    $this->dateFormatter = $dateFormatter;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('config.factory'), $container
      ->get('tempstore.private'), $container
      ->get('date.formatter'));
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'date_recur_modular_sierra_occurrences_modal';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['#attached']['library'][] = 'date_recur_modular/sierra_modal_occurrences_form';
    $form['#attached']['library'][] = 'core/drupal.ajax';
    $form['#theme'] = 'date_recur_modular_sierra_widget_modal_occurrences_form';
    $collection = $this->tempStoreFactory
      ->get(DateRecurModularSierraWidget::COLLECTION_MODAL_STATE);

    /** @var string|null $rrule */
    $rrule = $collection
      ->get(DateRecurModularSierraWidget::COLLECTION_MODAL_STATE_KEY);

    /** @var string $dateFormat */
    $dateFormatId = $collection
      ->get(DateRecurModularSierraWidget::COLLECTION_MODAL_DATE_FORMAT);
    $multiplier = $form_state
      ->get('occurrence_multiplier');
    if (!isset($multiplier)) {
      $form_state
        ->set('occurrence_multiplier', 1);
      $multiplier = 1;
    }
    $form['original_string'] = [
      '#type' => 'value',
      '#value' => $rrule,
    ];
    $dtStartString = $collection
      ->get(DateRecurModularSierraWidget::COLLECTION_MODAL_STATE_DTSTART);
    if (!empty($dtStartString)) {
      $dtStart = \DateTime::createFromFormat(DateRecurModularSierraWidget::COLLECTION_MODAL_STATE_DTSTART_FORMAT, $dtStartString);
    }
    else {

      // Use current date if there is no valid starting date from handoff.
      $dtStart = new \DateTime();
    }
    $form['date_start'] = [
      '#type' => 'value',
      '#value' => $dtStart,
    ];
    if (isset($rrule)) {
      try {
        $helper = DateRecurHelper::create($rrule, $dtStart);
      } catch (\Exception $e) {
      }
    }

    // Rebuild using Rset because we want to be able to iterate over occurrences
    // without considering any existing EXDATEs.
    $rset = new RSet();

    /** @var \DateTime[] $excluded */
    $excludes = [];
    if (isset($helper)) {
      foreach ($helper
        ->getRules() as $rule) {
        $rset
          ->addRRule($rule
          ->getParts());
      }
      $excludes = $helper
        ->getExcluded();
    }
    sort($excludes);

    // Initial limit is 1024, with 128 per page thereafter, with an absolute
    // maximum of 64000. Limit prevents performance issues and abuse.
    $limit = min(1024 + 128 * ($multiplier - 1), 64000);

    // 6 months from now plus 4 months thereafter.
    $limitDate = (new \DateTime())
      ->modify(sprintf('+%d months', 6 + ($multiplier - 1) * 4));
    $occurrences = [];
    $matchedExcludes = [];
    $unmatchedExcludes = [];
    $iteration = 0;
    foreach ($rset as $occurrenceDate) {
      if ($iteration > $limit || $limitDate < $occurrenceDate) {
        break;
      }
      $occurrences[$iteration] = [
        'date' => $occurrenceDate,
        'excluded' => FALSE,
      ];

      // After each occurrence evaluate if there were any excludes that fit
      // between this occurrence and last occurrence.
      foreach ($excludes as $k => $exDate) {
        if ($exDate < $occurrenceDate) {

          // Occurrence was between this and last occurrence, so likely no
          // longer matches against the RRULE.
          // Its done progessively like this instead of comparing occurrences to
          // EXDATEs as some EXDATEs may fall outside of the date/count limits.
          $unmatchedExcludes[] = $exDate;
          unset($excludes[$k]);
        }
        elseif ($exDate == $occurrenceDate) {

          // Occurrence matches an exclude date exactly.
          $matchedExcludes[] = $exDate;
          $occurrences[$iteration]['excluded'] = TRUE;
          unset($excludes[$k]);
        }
      }
      $iteration++;
    }

    /** @var \DateTime[] $outOfLimitExcludes */
    $outOfLimitExcludes = $excludes;
    unset($excludes);

    // Any remaining excludes are out of range. We dont know if they are matched
    // to the rule or not. Keep them and save.
    $form['excludes_out_of_limit'] = [
      '#type' => 'value',
      '#value' => $outOfLimitExcludes,
    ];
    if (count($unmatchedExcludes)) {
      $form['invalid_excludes'] = [
        '#type' => 'details',
        '#title' => $this
          ->formatPlural(count($unmatchedExcludes), '@count invalid exclude', '@count invalid excludes'),
      ];
      $form['invalid_excludes']['help'] = [
        '#type' => 'inline_template',
        '#template' => '<p>{{ message }}</p>',
        '#context' => [
          'message' => $this
            ->formatPlural(count($unmatchedExcludes), 'This invalid excluded occurrence will be automatically removed.', 'These invalid excluded occurrences will be automatically removed.'),
        ],
      ];
      $form['invalid_excludes']['table'] = [
        '#type' => 'table',
        '#header' => [
          'date' => $this
            ->t('Date'),
        ],
      ];
      $form['invalid_excludes']['table']['#rows'] = array_map(function (\DateTime $date) use ($dateFormatId, $dtStart) : array {
        return [
          'date' => $this->dateFormatter
            ->format($date
            ->getTimestamp(), $dateFormatId, '', $dtStart
            ->getTimezone()
            ->getName()),
        ];
      }, $unmatchedExcludes);
    }
    $form['occurrences'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Occurrences'),
      '#open' => TRUE,
    ];
    $form['occurrences']['help'] = [
      '#type' => 'inline_template',
      '#template' => '<p>{{ message }}</p>',
      '#context' => [
        'message' => $this
          ->t('This table shows a selection of occurrences. Occurrences may be removed individually. Times are displayed in <em>@time_zone</em> time zone.', [
          '@time_zone' => $dtStart
            ->getTimezone()
            ->getName(),
        ]),
      ],
    ];
    $form['occurrences']['table'] = [
      '#type' => 'table',
      '#header' => [
        'exclude' => $this
          ->t('Exclude'),
        'date' => $this
          ->t('Date'),
      ],
      '#empty' => $this
        ->t('There are no occurrences.'),
      '#prefix' => '<div id="occurrences-table">',
      '#suffix' => '</div>',
    ];
    $i = 0;
    foreach ($occurrences as $occurrence) {

      /** @var \DateTime $date */

      /** @var bool $excluded */
      [
        'date' => $date,
        'excluded' => $excluded,
      ] = $occurrence;
      $date
        ->setTimezone($dtStart
        ->getTimezone());
      $row = [];
      $row['exclude'] = [
        '#type' => 'checkbox',
        '#return_value' => $i,
        '#default_value' => $excluded,
      ];
      $row['date']['#markup'] = $this->dateFormatter
        ->format($date
        ->getTimestamp(), $dateFormatId, '', $dtStart
        ->getTimezone()
        ->getName());
      $row['#date_object'] = $date;
      $form['occurrences']['table'][$i] = $row;
      $i++;
    }
    $form['occurrences']['show_more'] = [
      '#type' => 'container',
    ];
    $form['occurrences']['show_more']['count_message'] = [
      '#type' => 'inline_template',
      '#template' => '<p>{{ count_message }}</p>',
      '#context' => [
        'count_message' => $this
          ->formatPlural(count($outOfLimitExcludes), 'There is @count more hidden excluded occurrence.', 'There are @count more hidden excluded occurrences.'),
      ],
    ];
    if (count($outOfLimitExcludes) > 0) {
      $nextExclude = reset($outOfLimitExcludes);
      $form['occurrences']['show_more']['next_message'] = [
        '#type' => 'inline_template',
        '#template' => '<p>{{ next_message }}</p>',
        '#context' => [
          'next_message' => $this
            ->t('Next hidden excluded occurrence is at @date', [
            '@date' => $this->dateFormatter
              ->format($nextExclude
              ->getTimestamp(), $dateFormatId, '', $dtStart
              ->getTimezone()
              ->getName()),
          ]),
        ],
      ];
    }
    $form['occurrences']['show_more']['show_more'] = [
      '#type' => 'submit',
      '#value' => $this
        ->t('Show more'),
      '#ajax' => [
        'event' => 'click',
        // Need 'url' and 'options' for this submission button to use this
        // controller not the caller.
        'url' => Url::fromRoute('date_recur_modular_widget.sierra_modal_occurrences_form'),
        'options' => [
          'query' => [
            FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
          ],
        ],
        'callback' => [
          $this,
          'ajaxShowMore',
        ],
      ],
    ];
    $form['actions'] = [
      '#type' => 'actions',
    ];
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this
        ->t('Done'),
      '#button_type' => 'primary',
      '#ajax' => [
        'event' => 'click',
        // Need 'url' and 'options' for this submission button to use this
        // controller not the caller.
        'url' => Url::fromRoute('date_recur_modular_widget.sierra_modal_occurrences_form'),
        'options' => [
          'query' => [
            FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
          ],
        ],
        'callback' => [
          $this,
          'ajaxSubmitForm',
        ],
      ],
    ];
    return $form;
  }

  /**
   * Callback to reload modal with more occurrences.
   */
  public function ajaxShowMore(array &$form, FormStateInterface $form_state) : AjaxResponse {
    $form_state
      ->setRebuild();
    $multiplier = $form_state
      ->get('occurrence_multiplier');
    $form_state
      ->set('occurrence_multiplier', $multiplier + 1);
    $response = new AjaxResponse();
    $form = \Drupal::formBuilder()
      ->rebuildForm($this
      ->getFormId(), $form_state, $form);
    $response
      ->addCommand(new OpenModalDialogCommand($this
      ->t('Occurrences'), $form, [
      'width' => '575',
    ]));
    return $response;
  }

  /**
   * Callback to submit modal modified exclusions.
   */
  public function ajaxSubmitForm(array &$form, FormStateInterface $form_state) {
    $response = new AjaxResponse();
    if ($form_state
      ->getErrors()) {

      // Inspired by \Drupal\form_api_example\Form\ModalForm::ajaxSubmitForm.
      $form['status_messages'] = [
        '#type' => 'status_messages',
      ];

      // Open the form again as a modal.
      return $response
        ->addCommand(new OpenModalDialogCommand($this
        ->t('Errors'), $form, [
        'width' => '575',
      ]));
    }
    $originalString = $form_state
      ->getValue('original_string');

    /** @var \DateTime $dtStart */
    $dtStart = $form_state
      ->getValue('date_start');
    try {
      $helper = DateRecurHelper::create($originalString, $dtStart);
    } catch (\Exception $e) {
    }

    // Rebuild original set without EXDATES.
    $rset = new RSet();
    if (isset($helper)) {
      array_walk($helper
        ->getRules(), function (DateRecurRuleInterface $rule) use ($rset) {
        $parts = $rule
          ->getParts();
        unset($parts['DTSTART']);
        $rset
          ->addRRule($parts);
      });
    }

    // Add checked excluded dates.
    foreach ($form_state
      ->getValue('table') as $i => $row) {
      if ($row['exclude'] !== 0) {
        $date = $form['occurrences']['table'][$i]['#date_object'];
        $rset
          ->addExDate($date);
      }
    }

    // Add out of range excluded dates.
    foreach ($form_state
      ->getValue('excludes_out_of_limit') as $exDate) {

      /** @var \DateTime $exDate */
      $rset
        ->addExDate($exDate);
    }
    $lines = [];
    foreach ($rset
      ->getRRules() as $rule) {

      /** @var \RRule\RRule $rule */
      $lines[] = 'RRULE:' . $rule
        ->rfcString(FALSE);
    }
    $utc = new \DateTimeZone('UTC');
    $exDates = array_map(function (\DateTime $exDate) use ($utc) {
      $exDate
        ->setTimezone($utc);
      return $exDate
        ->format(static::UTC_FORMAT);
    }, $rset
      ->getExDates());
    if (count($exDates) > 0) {
      $lines[] = 'EXDATE:' . implode(',', $exDates);
    }
    $collection = $this->tempStoreFactory
      ->get(DateRecurModularSierraWidget::COLLECTION_MODAL_STATE);
    $collection
      ->set(DateRecurModularSierraWidget::COLLECTION_MODAL_STATE_KEY, implode("\n", $lines));
    $refreshBtnName = sprintf('[name="%s"]', $collection
      ->get(DateRecurModularSierraWidget::COLLECTION_MODAL_STATE_REFRESH_BUTTON));
    $response
      ->addCommand(new CloseDialogCommand())
      ->addCommand(new InvokeCommand($refreshBtnName, 'trigger', [
      'click',
    ]));
    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    return new AjaxResponse();
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DateRecurModularSierraModalOccurrencesForm::$dateFormatter protected property The date formatter service.
DateRecurModularSierraModalOccurrencesForm::$tempStoreFactory protected property The PrivateTempStore factory.
DateRecurModularSierraModalOccurrencesForm::ajaxShowMore public function Callback to reload modal with more occurrences.
DateRecurModularSierraModalOccurrencesForm::ajaxSubmitForm public function Callback to submit modal modified exclusions.
DateRecurModularSierraModalOccurrencesForm::buildForm public function Form constructor. Overrides FormInterface::buildForm
DateRecurModularSierraModalOccurrencesForm::create public static function Instantiates a new instance of this class. Overrides FormBase::create
DateRecurModularSierraModalOccurrencesForm::getFormId public function Returns a unique string identifying the form. Overrides FormInterface::getFormId
DateRecurModularSierraModalOccurrencesForm::submitForm public function Form submission handler. Overrides FormInterface::submitForm
DateRecurModularSierraModalOccurrencesForm::UTC_FORMAT protected constant Date format for exclusion dates.
DateRecurModularSierraModalOccurrencesForm::__construct public function Constructs a new DateRecurModularSierraModalOccurrencesForm.
DateRecurModularUtilityTrait::buildDatesFromFields public static function Build a datetime object by getting the date and time from two fields.
DateRecurModularUtilityTrait::buildRruleString protected function Builds RRULE string from an array of parts, stripping disallowed parts.
DateRecurModularUtilityTrait::getCurrentUserTimeZone protected function Get the time zone associated with the current user.
DateRecurModularUtilityTrait::getDefaultTimeZone protected function Determines a default time zone for a field item.
DateRecurModularUtilityTrait::getMonthWeekdayNth public static function Determine nth weekday into a month for a date.
DateRecurModularUtilityTrait::getName protected function Build the name for a sub element.
DateRecurModularUtilityTrait::getRule protected function Attempts to get the first valid rule from a date recur field item.
DateRecurModularUtilityTrait::getTimeZoneOptions protected function Get a list of time zones suitable for a select field.
DateRecurModularUtilityTrait::isAllDay protected function Determine whether a field item represents a full day.
DateRecurModularWidgetFieldsTrait::getFieldByDay protected function Get a BYDAY element.
DateRecurModularWidgetFieldsTrait::getFieldEndsMode protected function Get an radios element for toggling between common end modes.
DateRecurModularWidgetFieldsTrait::getFieldMode protected function Get a select element for toggling between common modes.
DateRecurModularWidgetFieldsTrait::getFieldMonth protected function Get a BYMONTH element.
DateRecurModularWidgetFieldsTrait::getFieldTimeZone protected function Get a time zone element.
DateRecurModularWidgetFieldsTrait::getVisibilityStates protected function Builds a #states array for an element dependant on mode selected.
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormBase::$configFactory protected property The config factory. 1
FormBase::$requestStack protected property The request stack. 1
FormBase::$routeMatch protected property The route match.
FormBase::config protected function Retrieves a configuration object.
FormBase::configFactory protected function Gets the config factory for this form. 1
FormBase::container private function Returns the service container.
FormBase::currentUser protected function Gets the current user.
FormBase::getRequest protected function Gets the request object.
FormBase::getRouteMatch protected function Gets the route match.
FormBase::logger protected function Gets the logger for a specific channel.
FormBase::redirect protected function Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait::redirect
FormBase::resetConfigFactory public function Resets the configuration factory.
FormBase::setConfigFactory public function Sets the config factory for this form.
FormBase::setRequestStack public function Sets the request stack object to use.
FormBase::validateForm public function Form validation handler. Overrides FormInterface::validateForm 62
LinkGeneratorTrait::$linkGenerator protected property The link generator. 1
LinkGeneratorTrait::getLinkGenerator Deprecated protected function Returns the link generator.
LinkGeneratorTrait::l Deprecated protected function Renders a link to a route given a route name and its parameters.
LinkGeneratorTrait::setLinkGenerator Deprecated public function Sets the link generator service.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 1
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
UrlGeneratorTrait::$urlGenerator protected property The url generator.
UrlGeneratorTrait::getUrlGenerator Deprecated protected function Returns the URL generator service.
UrlGeneratorTrait::setUrlGenerator Deprecated public function Sets the URL generator service.
UrlGeneratorTrait::url Deprecated protected function Generates a URL or path for a specific route based on the given parameters.