Commit 7db58395 authored by John Voskuilen's avatar John Voskuilen
Browse files

Issue #1998266 by johnv, SpadXIII, ethomas08, megadesk3000, idflood, hswong3i,...

Issue #1998266 by johnv, SpadXIII, ethomas08, megadesk3000, idflood, hswong3i, josh.fabean, bartzaalberg, drugan, Tess Bakker, yusufhm, Lukas von Blarer, ebremner, tvhung, zenimagine: Add 'Exception day' feature
parent f2d639b7
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -11,6 +11,9 @@ use Drupal\Core\Form\FormStateInterface;
// Add theme.api.php hooks.
\Drupal::moduleHandler()->loadInclude('office_hours', 'inc', 'office_hours.theme');

// Add ExceptionItem third party settings.
\Drupal::moduleHandler()->loadInclude('office_hours', 'inc', 'office_hours.exceptions.third_party');

/**
 * Implements hook_form_FORM_ID_alter().
 *
+65 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\office_hours\Element;

use Drupal\Core\Form\FormStateInterface;
use Drupal\office_hours\OfficeHoursDateHelper;

/**
 * Provides a one-line text field form element for Exception days.
 *
 * @FormElement("office_hours_exceptions_slot")
 */
class OfficeHoursExceptionsSlot extends OfficeHoursWeekSlot {

  /**
   * {@inheritdoc}
   */
  public static function processOfficeHoursSlot(&$element, FormStateInterface $form_state, &$complete_form) {

    // Update $element['#value'] with default data and prepare $element widget.
    parent::processOfficeHoursSlot($element, $form_state, $complete_form);

    // Facilitate Exception day specific things, such as changing date.
    $value = $element['#value'];
    $day = $value['day'];
    $day_delta = $element['#daydelta'];
    $default_day = (is_numeric($day)) ? date('Y-m-d', $day) : '';
    $label = OfficeHoursDateHelper::getLabel('l', $value, $day_delta);

    if ($day_delta == 0) {
      // For first time slot of a day, set a 'date' select element + day name,
      // overriding the hidden (Week widget) or select (List widget) 'day'.
      // Override (hide) the 'day' select-field.
      $element['day'] = [
        '#type' => 'date',
        '#prefix' => $day_delta
          ? "<div class='office-hours-more-label'>$label</div>"
          : "<div class='office-hours-label'>$label</div>",
        '#default_value' => $default_day,
      ];
    }
    else {
      // Leave 'more slots' as-is, but overriding the value,
      // so all slots have same day number.
      $element['day'] = [
        '#type' => 'hidden',
        '#prefix' => $day_delta
          ? "<div class='office-hours-more-label'>$label</div>"
          : "<div class='office-hours-label'>$label</div>",
        '#default_value' => $default_day,
        '#value' => $default_day,
      ];

    }
    // Add 'day_delta' to facilitate ExceptionsSlot.
    // @todo This adds a loose column to the widget. Fix it, avoiding colspan.
    $element['day_delta'] = [
      '#type' => 'value', // 'hidden',
      '#value' => $day_delta,
    ];

    return $element;
  }

}
+335 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\office_hours;

use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\PluginSettingsBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;

/**
 * A unit-testable wrapper class around third party settings hook logic.
 */
class OfficeHoursExceptionsThirdPartySettings {

  use StringTranslationTrait;

  /**
   * The plugin_id.
   *
   * @var string
   */
  protected $pluginId;

  /**
   * The plugin (formatter, widget).
   *
   * @var \Drupal\office_hours\Plugin\Field\FieldFormatter\OfficeHoursFormatterBase
   */
  protected $plugin;

  /**
   * The plugin implementation definition.
   *
   * @var array
   */
  protected $pluginDefinition;

  /**
   * Configuration information passed into the plugin.
   *
   * When using an interface like
   * \Drupal\Component\Plugin\ConfigurableInterface, this is where the
   * configuration should be stored.
   *
   * Plugin configuration is optional, so plugin implementations must provide
   * their own setters and getters.
   *
   * @var array
   */
  protected $configuration;

  /**
   * The field definition.
   *
   * @var \Drupal\Core\Field\FieldDefinitionInterface
   */
  protected $fieldDefinition;

  /**
   * The viewMode.
   */
  protected $viewMode;

  /**
   * The plugin (formatter/widget) settings.
   *
   * @var array
   */
  protected $settings;

  /**
   * The third party settings.
   *
   * @var array
   */
  protected $thirdPartySettings;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Office hours exceptions table constructor.
   *
   * @param $plugin string|\Drupal\office_hours\Plugin\Field\FieldFormatter\OfficeHoursFormatterBase
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   * @param array $settings
   * @param $view_mode
   * @param array $third_party_settings
   * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
   */
  public function __construct($plugin,
                              // $plugin_definition,
                              $field_definition,
                              array $settings,
                              // $label,
                              $view_mode,
                              array $third_party_settings,
                              EntityTypeManager $entity_type_manager) {
    // parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);

    // The following from pluginBase.
    // $this->configuration = $configuration;
    // Sometimes the plugin/formatter is not provided, and not needed. Use ID.
    $this->pluginId = is_string($plugin) ? $plugin : $plugin->getPluginId();
    $this->plugin = is_string($plugin) ? NULL : $plugin;
    // $this->pluginDefinition = $plugin_definition;
    //
    // The following from FormatterBase.
    $this->fieldDefinition = $field_definition;
    $this->settings = $settings;
    // $this->label = $label;
    $this->viewMode = $view_mode;
    $this->thirdPartySettings = $third_party_settings;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(array $configuration, $plugin_id, $plugin_definition) {
//public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {

    return new static(
      $plugin_id,
      $configuration['field_definition'] ?? NULL,
      $configuration['settings'] ?? [],
      //$configuration['label'],
      $configuration['view_mode'] ?? [],
      $configuration['third_party_settings'] ?? [],
      \Drupal::service('entity_type.manager') // @todo Use $container->get('entity_type.manager').
    );
  }

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      'exceptions' => [
        'restrict_exceptions_to_num_days' => 7,
        'date_format' => 'long',
        'title' => 'Exception hours',
      ],
    ]; // + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   */
  public function getThirdPartySettings($module = 'office_hours_exceptions') {
    $settings = $this->plugin->getThirdPartySettings($module);
    $settings += $this->defaultSettings()['exceptions'];
    return $settings;
  }

  /**
   * {@inheritdoc}
   */
  public function formatterSettingsForm(PluginSettingsBase $plugin) {
    // public function settingsForm(FormatterInterface $plugin) {.
    // public function settingsForm(array $form, FormStateInterface $form_state) {.
    $settings = $this->getThirdPartySettings();

    // Get the date formats.
    $formats = $this->entityTypeManager->getStorage('date_format')->loadMultiple();
    // Set select list options for the date format. @todo OptionsProviderInterface.
    $options = [];
    foreach ($formats as $format) {
      $options[$format->id()] = $format->get('label');
    }

    $element['exceptions'] = [
      '#title' => $this->t('Exception day handling'),
      '#type' => 'details',
      '#open' => TRUE,
    ];
    $element['exceptions']['restrict_exceptions_to_num_days'] = [
      '#title' => $this->t('Restrict exceptions display to x days in future'),
      '#type' => 'number',
      // '#default_value' => $settings['exceptions']['restrict_exceptions_to_num_days'],
      '#default_value' => $settings['restrict_exceptions_to_num_days'],
      '#min' => 0,
      '#max' => 99,
      '#step' => 1,
      '#description' => $this->t("To enable Exception days, set a non-zero number (to be used in the formatter) and select an 'Exceptions' widget."),
      '#required' => TRUE,
    ];
    // @todo Add link to admin/config/regional/date-time.
    $element['exceptions']['date_format'] = [
      '#title' => $this->t('Date format for exception day'),
      '#type' => 'select',
      '#options' => $options,
      '#default_value' => $settings['date_format'],
      '#description' => $this->t("Maintain additional date formats <a href=':url'>here</a>.", [
        ':url' => Url::fromRoute('entity.date_format.collection')->toString(),
      ]),
      '#required' => TRUE,
    ];
    // @todo Move to field settings, since used in both Formatter and Widget.
    $element['exceptions']['title'] = [
      '#title' => $this->t('Title for exceptions'),
      '#type' => 'textfield',
      '#default_value' => $settings['title'],
      '#description' => $this->t('Leave empty to display no title between weekdays and exception days.'),
      '#required' => FALSE,
    ];
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function formatterSettingsSummary(&$summary) {
    // $summary = parent::settingsSummary();
    $settings = $this->getThirdPartySettings();
    $label = OfficeHoursDateHelper::getLabel($settings['date_format'], ['day' => strtotime('today midnight')]);
    $summary[] = $this->t("Show '@title' until @time days in the future.", [
      '@time' => $settings['restrict_exceptions_to_num_days'],
      '@date' => $settings['date_format'],
      '@title' => $settings['title'] == '' ? $this->t('Exception days') : $this->t($settings['title']),
    ]) . ' ' . $this->t("Example: $label");

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function widgetSettingsForm(PluginSettingsBase $plugin) {
    $element['exceptions'] = [];
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode, array &$elements) {

    if ($items->isEmpty()) {
      return $elements;
    }

    $settings = $this->settings;
    $third_party_settings = $this->thirdPartySettings;

    // Loop over formatters: $elements contains table/status/schema formatter.
    foreach ($elements as $key => &$element) if (is_numeric($key)) {
      switch ($element['content']['#theme']) {

        case 'office_hours_table':
          $formatter_rows = &$element['content']['#table']['#rows'];
          if ($formatter_rows) {
            $exception_exists = FALSE;
            // Check if an exception day exists in the table.
            foreach ($formatter_rows as $day => $item) {
              $is_exception_day = OfficeHoursDateHelper::isExceptionDay(['day' => $day]);
              $exception_exists |= $is_exception_day;
            }

            // If there is an exception, add a title for exceptions.
            // Add an extra row to label the exceptions.
            // Note: may be changed in template_preprocess_office_hours_table().
            $label = $third_party_settings['title'];
            if ($exception_exists && $label) {

              $exception_header['data']['label']['data']['#markup'] = $label;
              $exception_header['class'] = [
                'office-hours__exceptions-label',
              ];
              $exception_header['id'] = ['office-hours-exception__title'];

              // Keys 0-7 are for sorted weekdays, so adding our 8.
              $formatter_rows[8] = $exception_header;

              // Sort, to ensure that the header is before the exceptions.
              $formatter_rows = OfficeHoursDateHelper::weekDaysOrdered($formatter_rows, $settings['office_hours_first_day']);
            }
          }
          break;

        case 'office_hours':

          // Get Plain text formatter in Formatter::viewElements().
          // $formatter_rows = &$element['#office_hours'] ?? NULL;
          // Get Plain text formatter in ThirdPartySettings::viewElements().
          // @todo Perhaps better use new ['#office_hours_field'].
          $formatter_rows = &$element['content']['#office_hours'];
          if ($formatter_rows) {
            $exception_exists = FALSE;
            foreach ($formatter_rows as $day => $item) {
              // @todo rows vs. office_hours.
              $is_exception_day = OfficeHoursDateHelper::isExceptionDay(['day' => $day]);
              $exception_exists |= $is_exception_day;
            }

            // If there is an exception, add an extra row to label the exceptions.
            // Note: may be changed in template_preprocess_office_hours_table().
            $label = $third_party_settings['title'];
            if ($exception_exists && $label) {
              // Set the title for the exceptions.
              $exception_header['title'] = $label;
              $exception_header['label'] = $label;
              // Set everything else to NULL.
              $exception_header['slots'] = NULL;
              $exception_header['formatted_slots'] = NULL;
              $exception_header['index'] = NULL;
              $exception_header['comments'] = NULL;

              // Keys 0-7 are for sorted weekdays, so adding our 8.
              $formatter_rows[8] = $exception_header;

              // Sort, to ensure that the header is before the exceptions.
              $formatter_rows = OfficeHoursDateHelper::weekDaysOrdered($formatter_rows, $settings['office_hours_first_day']);
            }
          }
          break;

        case 'office_hours_schema':
          // @todo Test/Enhance this formatter.
        case 'office_hours_status':
        // @todo Test/Enhance this formatter.
        default:
          break;
      }
    }

    return $elements;
  }

}
+82 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\office_hours\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursItem;

/**
 * Plugin implementation of the 'office_hours' field type.
 *
 * @FieldType(
 *   id = "office_hours_exceptions",
 *   label = @Translation("Office hours exception day"),
 *   list_class = "\Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursItemList",
 * )
 */
class OfficeHoursExceptionsItem extends OfficeHoursItem {

  /**
   * {@inheritdoc}
   */
  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
    // @todo Add random Exception day value in past and in near future.
    $value = [
      'day' => mt_rand(0, 6),
      'starthours' => mt_rand(00, 23) * 100,
      'endhours' => mt_rand(00, 23) * 100,
      'comment' => mt_rand(0, 1) ? 'additional exception text' : '',
    ];
    return $value;
  }

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

  /**
   * Returns if a timestamp is in the past.
   *
   * @return bool
   *   TRUE if the timestamp is in the past.
   */
  public function isExceptionDayInPast() {
    $day = $this->getValue()['day'];
    if ($day < strtotime('today midnight')) {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Returns if a timestamp is in date range of x days to the future.
   *
   * Prerequisite: $item->isExceptionDay() must be TRUE.
   *
   * @param int $rangeInDays
   *   The days into the future we want to check the timestamp against.
   *
   * @return bool
   *   TRUE if the timestamp is in range.
   *   TRUE if $rangeInDays has a negative value.
   */
  public function isExceptionDayInRange($rangeInDays) {
    if ($rangeInDays <= 0) {
      return TRUE;
    }
    if ($this->isExceptionDayInPast()) {
      return FALSE;
    }
    $day = $this->getValue()['day'];
    $maxTime = time() + $rangeInDays * 24 * 60 * 60;
    if ($day > $maxTime) {
      return FALSE;
    }
    return TRUE;
  }

}
+3 −3
Original line number Diff line number Diff line
@@ -49,10 +49,10 @@ class OfficeHoursItemList extends FieldItemList implements OfficeHoursItemListIn
    if (!$exceptions_list) {
      $pluginManager = \Drupal::service('plugin.manager.field.field_type');
      // Get field definition of ExceptionsItem.
      $plugin_id = 'office_hours_exception';
      $plugin_id = 'office_hours_exceptions';
      $field_definition = BaseFieldDefinition::create($plugin_id);
      // Create an ItemList with ExceptionsItems.
      $exceptions_list = new OfficeHoursItemList($field_definition);
      // Create an ItemList with OfficeHoursExceptionsItem items.
      $exceptions_list = OfficeHoursItemList::createInstance($field_definition, $this->name, NULL);
    }
    // Then, add an item to the list with Exception day field definition.
    $item = $pluginManager->createFieldItem($exceptions_list, $offset, $value);
Loading