View source
<?php
declare (strict_types=1);
namespace Drupal\date_recur_modular\Plugin\Field\FieldWidget;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\date_recur\DateRecurPartGrid;
use Drupal\date_recur\DateRecurRuleInterface;
use Drupal\date_recur\Exception\DateRecurHelperArgumentException;
use Drupal\date_recur\Plugin\Field\FieldType\DateRecurItem;
use Drupal\date_recur_modular\DateRecurModularWidgetFieldsTrait;
use Drupal\date_recur_modular\DateRecurModularWidgetOptions;
class DateRecurModularAlphaWidget extends DateRecurModularWidgetBase {
use DateRecurModularWidgetFieldsTrait;
protected const MODE_ONCE = 'once';
protected const MODE_MULTIDAY = 'multiday';
protected const MODE_WEEKLY = 'weekly';
protected const MODE_FORTNIGHTLY = 'fortnightly';
protected const MODE_MONTHLY = 'monthly';
protected $partGrid;
protected function getModes() : array {
return [
static::MODE_ONCE => $this
->t('Once'),
static::MODE_MULTIDAY => $this
->t('Multiple days'),
static::MODE_WEEKLY => $this
->t('Weekly'),
static::MODE_FORTNIGHTLY => $this
->t('Fortnightly'),
static::MODE_MONTHLY => $this
->t('Monthly'),
];
}
protected function getMode(DateRecurItem $item) : ?string {
try {
$helper = $item
->getHelper();
} catch (DateRecurHelperArgumentException $e) {
return NULL;
}
$rules = $helper
->getRules();
$rule = reset($rules);
if (FALSE === $rule) {
return NULL;
}
$frequency = $rule
->getFrequency();
$parts = $rule
->getParts();
if ('DAILY' === $frequency) {
$count = $parts['COUNT'] ?? NULL;
return $count && $count > 1 ? static::MODE_MULTIDAY : static::MODE_ONCE;
}
elseif ('WEEKLY' === $frequency) {
$interval = $parts['INTERVAL'] ?? NULL;
return [
1 => static::MODE_WEEKLY,
2 => static::MODE_FORTNIGHTLY,
][$interval] ?? NULL;
}
elseif ('MONTHLY' === $frequency) {
return static::MODE_MONTHLY;
}
return NULL;
}
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) : array {
$elementParents = array_merge($element['#field_parents'], [
$this->fieldDefinition
->getName(),
$delta,
]);
$element['#element_validate'][] = [
static::class,
'validateModularWidget',
];
$element['#after_build'][] = [
static::class,
'afterBuildModularWidget',
];
$element['#theme'] = 'date_recur_modular_alpha_widget';
$element['#theme_wrappers'][] = 'form_element';
$item = $items[$delta];
$grid = $items
->getPartGrid();
$rule = $this
->getRule($item);
$parts = $rule ? $rule
->getParts() : [];
$count = $parts['COUNT'] ?? NULL;
$timeZone = $this
->getDefaultTimeZone($item);
$endsDate = NULL;
try {
$until = $parts['UNTIL'] ?? NULL;
if (is_string($until)) {
$endsDate = new \DateTime($until);
}
elseif ($until instanceof \DateTimeInterface) {
$endsDate = $until;
}
if ($endsDate) {
$endsDate
->setTimezone(new \DateTimeZone($timeZone));
}
} catch (\Exception $e) {
}
$fieldModes = $this
->getFieldModes($grid);
$element['start'] = [
'#type' => 'datetime',
'#title' => $this
->t('Starts on'),
'#title_display' => 'invisible',
'#default_value' => $item->start_date,
'#date_timezone' => $timeZone,
];
$element['end'] = [
'#title' => $this
->t('Ends on'),
'#title_display' => 'invisible',
'#type' => 'datetime',
'#default_value' => $item->end_date,
'#date_timezone' => $timeZone,
];
$element['mode'] = $this
->getFieldMode($item);
$element['mode']['#title_display'] = 'invisible';
$element['daily_count'] = [
'#type' => 'number',
'#title' => $this
->t('Days'),
'#title_display' => 'invisible',
'#field_suffix' => $this
->t('days'),
'#default_value' => $count ?? 1,
'#min' => 1,
'#access' => count($fieldModes['daily_count'] ?? []) > 0,
];
$element['daily_count']['#states'] = $this
->getVisibilityStates($element, $fieldModes['daily_count'] ?? []);
$element['weekdays'] = $this
->getFieldByDay($rule);
$element['weekdays']['#states'] = $this
->getVisibilityStates($element, $fieldModes['weekdays'] ?? []);
$element['weekdays']['#title_display'] = 'invisible';
$element['ordinals'] = $this
->getFieldMonthlyByDayOrdinals($element, $rule);
$element['ordinals']['#states'] = $this
->getVisibilityStates($element, $fieldModes['ordinals'] ?? []);
$element['ordinals']['#title_display'] = 'invisible';
$element['time_zone'] = $this
->getFieldTimeZone($timeZone);
$endsModeDefault = $endsDate ? DateRecurModularWidgetOptions::ENDS_MODE_ON_DATE : ($count > 0 ? DateRecurModularWidgetOptions::ENDS_MODE_OCCURRENCES : DateRecurModularWidgetOptions::ENDS_MODE_INFINITE);
$element['ends_mode'] = $this
->getFieldEndsMode();
$element['ends_mode']['#states'] = $this
->getVisibilityStates($element, $fieldModes['ends_mode'] ?? []);
$element['ends_mode']['#title_display'] = 'before';
$element['ends_mode']['#default_value'] = $endsModeDefault;
$element['ends_mode'][DateRecurModularWidgetOptions::ENDS_MODE_OCCURRENCES]['#states'] = $this
->getVisibilityStates($element, $fieldModes['ends_count'] ?? []);
$element['ends_mode'][DateRecurModularWidgetOptions::ENDS_MODE_ON_DATE]['#states'] = $this
->getVisibilityStates($element, $fieldModes['ends_date'] ?? []);
$element['ends_count'] = [
'#type' => 'number',
'#title' => $this
->t('End after number of occurrences'),
'#title_display' => 'invisible',
'#field_prefix' => $this
->t('after'),
'#field_suffix' => $this
->t('occurrences'),
'#default_value' => $count ?? 1,
'#min' => 1,
'#access' => count($fieldModes['ends_count'] ?? []) > 0,
];
$nameMode = $this
->getName($element, [
'mode',
]);
$nameEndsMode = $this
->getName($element, [
'ends_mode',
]);
$element['ends_count']['#states']['visible'] = [];
foreach ($fieldModes['ends_count'] ?? [] as $mode) {
$element['ends_count']['#states']['visible'][] = [
':input[name="' . $nameMode . '"]' => [
'value' => $mode,
],
':input[name="' . $nameEndsMode . '"]' => [
'value' => DateRecurModularWidgetOptions::ENDS_MODE_OCCURRENCES,
],
];
}
$element['ends_date'] = [
'#type' => 'container',
];
$element['ends_date']['ends_date'] = [
'#type' => 'datetime',
'#title' => $this
->t('End before this date'),
'#title_display' => 'invisible',
'#description' => $this
->t('No occurrences can begin after this date.'),
'#default_value' => $endsDate ? DrupalDateTime::createFromDateTime($endsDate) : NULL,
'#parents' => array_merge($elementParents, [
'ends_date',
]),
'#date_timezone' => $timeZone,
];
$element['ends_date']['#states']['visible'] = [];
foreach ($fieldModes['ends_date'] ?? [] as $mode) {
$element['ends_date']['#states']['visible'][] = [
':input[name="' . $nameMode . '"]' => [
'value' => $mode,
],
':input[name="' . $nameEndsMode . '"]' => [
'value' => DateRecurModularWidgetOptions::ENDS_MODE_ON_DATE,
],
];
}
return $element;
}
public static function validateModularWidget(array &$element, FormStateInterface $form_state, array &$complete_form) : void {
$start = $form_state
->getValue(array_merge($element['#parents'], [
'start',
]));
$end = $form_state
->getValue(array_merge($element['#parents'], [
'end',
]));
$timeZone = $form_state
->getValue(array_merge($element['#parents'], [
'time_zone',
]));
if ($start && !$timeZone) {
$form_state
->setError($element['start'], \t('Time zone must be set if start date is set.'));
}
if ($end && !$timeZone) {
$form_state
->setError($element['end'], \t('Time zone must be set if end date is set.'));
}
if (($start instanceof DrupalDateTime || $end instanceof DrupalDateTime) && (!$start instanceof DrupalDateTime || !$end instanceof DrupalDateTime)) {
$form_state
->setError($element, \t('Start date and end date must be provided.'));
}
$zoneLess = 'Y-m-d H:i:s';
$timeZoneObj = new \DateTimeZone($timeZone);
if ($start instanceof DrupalDateTime && $timeZone) {
$start = DrupalDateTime::createFromFormat($zoneLess, $start
->format($zoneLess), $timeZoneObj);
$form_state
->setValueForElement($element['start'], $start);
}
if ($end instanceof DrupalDateTime && $timeZone) {
$end = DrupalDateTime::createFromFormat($zoneLess, $end
->format($zoneLess), $timeZoneObj);
$form_state
->setValueForElement($element['end'], $end);
}
$endsDate = $form_state
->getValue(array_merge($element['#parents'], [
'ends_date',
]));
if ($endsDate instanceof DrupalDateTime && $timeZone) {
$endsDate = DrupalDateTime::createFromFormat($zoneLess, $endsDate
->format($zoneLess), $timeZoneObj);
$form_state
->setValueForElement($element['ends_date'], $endsDate);
}
}
public static function afterBuildModularWidget(array $element, FormStateInterface $form_state) {
$weekdaysId = $element['weekdays']['#id'];
$element['ordinals']['#states']['visible'][0]['#' . $weekdaysId . ' input[type="checkbox"]'] = [
'checked' => TRUE,
];
$element['weekdays']['#attributes']['class'][] = 'container-inline';
$element['ordinals']['#attributes']['class'][] = 'container-inline';
return $element;
}
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
$this->partGrid = $items
->getPartGrid();
parent::extractFormValues(...func_get_args());
unset($this->partGrid);
}
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$values = parent::massageFormValues($values, $form, $form_state);
$dateStorageFormat = $this->fieldDefinition
->getSetting('datetime_type') == DateRecurItem::DATETIME_TYPE_DATE ? DateRecurItem::DATE_STORAGE_FORMAT : DateRecurItem::DATETIME_STORAGE_FORMAT;
$dateStorageTimeZone = new \DateTimezone(DateRecurItem::STORAGE_TIMEZONE);
$grid = $this->partGrid;
$returnValues = [];
foreach ($values as $delta => $value) {
if (empty($value)) {
continue;
}
$item = [];
$start = $value['start'] ?? NULL;
assert(!isset($start) || $start instanceof DrupalDateTime);
$end = $value['end'] ?? NULL;
assert(!isset($end) || $end instanceof DrupalDateTime);
$timeZone = $value['time_zone'];
assert(is_string($timeZone));
$mode = $value['mode'] ?? NULL;
$endsMode = $value['ends_mode'] ?? NULL;
$endsDate = $value['ends_date'] ?? NULL;
$start
->setTimezone($dateStorageTimeZone);
$item['value'] = $start
->format($dateStorageFormat);
$end
->setTimezone($dateStorageTimeZone);
$item['end_value'] = $end
->format($dateStorageFormat);
$item['timezone'] = $timeZone;
$weekDays = array_values(array_filter($value['weekdays']));
$byDayStr = implode(',', $weekDays);
$rule = [];
if ($mode === static::MODE_MULTIDAY) {
$rule['FREQ'] = 'DAILY';
$rule['INTERVAL'] = 1;
$rule['COUNT'] = $value['daily_count'];
}
elseif ($mode === static::MODE_WEEKLY) {
$rule['FREQ'] = 'WEEKLY';
$rule['INTERVAL'] = 1;
$rule['BYDAY'] = $byDayStr;
}
elseif ($mode === static::MODE_FORTNIGHTLY) {
$rule['FREQ'] = 'WEEKLY';
$rule['INTERVAL'] = 2;
$rule['BYDAY'] = $byDayStr;
}
elseif ($mode === static::MODE_MONTHLY) {
$rule['FREQ'] = 'MONTHLY';
$rule['INTERVAL'] = 1;
$rule['BYDAY'] = $byDayStr;
$ordinalCheckboxes = array_filter($value['ordinals']);
$ordinals = [];
if (count($ordinalCheckboxes) && count($weekDays)) {
$weekdayCount = count($weekDays);
foreach ($ordinalCheckboxes as $ordinal) {
$end = $ordinal * $weekdayCount;
$diff = $weekdayCount - 1;
$start = $end > 0 ? $end - $diff : $end + $diff;
$range = range($start, $end);
array_push($ordinals, ...$range);
}
sort($ordinals);
$rule['BYSETPOS'] = implode(',', $ordinals);
}
}
if ($endsMode === DateRecurModularWidgetOptions::ENDS_MODE_OCCURRENCES && $mode !== static::MODE_MULTIDAY) {
$rule['COUNT'] = (int) $value['ends_count'];
}
elseif ($endsMode === DateRecurModularWidgetOptions::ENDS_MODE_ON_DATE && $endsDate instanceof DrupalDateTime) {
$endsDateUtcAdjusted = (clone $endsDate)
->setTimezone(new \DateTimeZone('UTC'));
$rule['UNTIL'] = $endsDateUtcAdjusted
->format('Ymd\\THis\\Z');
}
if (isset($rule['FREQ'])) {
$rule = array_filter($rule);
$item['rrule'] = $this
->buildRruleString($rule, $grid);
}
$returnValues[] = $item;
}
return $returnValues;
}
protected function getFieldMonthlyByDayOrdinals(array $element, ?DateRecurRuleInterface $rule) : array {
$parts = $rule ? $rule
->getParts() : [];
$ordinals = [];
$bySetPos = !empty($parts['BYSETPOS']) ? explode(',', $parts['BYSETPOS']) : [];
if (count($bySetPos) > 0) {
$weekdayCount = count($element['weekdays']['#default_value']);
sort($bySetPos);
$chunks = array_chunk($bySetPos, $weekdayCount);
foreach ($chunks as $chunk) {
$first = reset($chunk);
$end = $first < 0 ? min($chunk) : max($chunk);
$ordinals[] = $end / $weekdayCount;
}
}
return [
'#type' => 'checkboxes',
'#title' => $this
->t('Ordinals'),
'#options' => [
1 => $this
->t('First'),
2 => $this
->t('Second'),
3 => $this
->t('Third'),
4 => $this
->t('Fourth'),
5 => $this
->t('Fifth'),
-1 => $this
->t('Last'),
-2 => $this
->t('2nd to last'),
],
'#default_value' => $ordinals,
];
}
protected function getFieldModes(DateRecurPartGrid $grid) : array {
$fieldModes = [];
if ($grid
->isPartAllowed('DAILY', 'COUNT')) {
$fieldModes['daily_count'][] = static::MODE_MULTIDAY;
}
if ($grid
->isPartAllowed('WEEKLY', 'BYDAY')) {
$fieldModes['weekdays'][] = static::MODE_WEEKLY;
$fieldModes['weekdays'][] = static::MODE_FORTNIGHTLY;
}
$count = $grid
->isPartAllowed('WEEKLY', 'COUNT');
$until = $grid
->isPartAllowed('WEEKLY', 'UNTIL');
if ($count || $until) {
$fieldModes['ends_mode'][] = static::MODE_WEEKLY;
$fieldModes['ends_mode'][] = static::MODE_FORTNIGHTLY;
if ($count) {
$fieldModes['ends_count'][] = static::MODE_WEEKLY;
$fieldModes['ends_count'][] = static::MODE_FORTNIGHTLY;
}
if ($until) {
$fieldModes['ends_date'][] = static::MODE_WEEKLY;
$fieldModes['ends_date'][] = static::MODE_FORTNIGHTLY;
}
}
if ($grid
->isPartAllowed('MONTHLY', 'BYSETPOS')) {
$fieldModes['ordinals'][] = static::MODE_MONTHLY;
}
if ($grid
->isPartAllowed('MONTHLY', 'BYDAY')) {
$fieldModes['weekdays'][] = static::MODE_MONTHLY;
}
$count = $grid
->isPartAllowed('MONTHLY', 'COUNT');
$until = $grid
->isPartAllowed('MONTHLY', 'UNTIL');
if ($count || $until) {
$fieldModes['ends_mode'][] = static::MODE_MONTHLY;
if ($count) {
$fieldModes['ends_count'][] = static::MODE_MONTHLY;
}
if ($until) {
$fieldModes['ends_date'][] = static::MODE_MONTHLY;
}
}
return $fieldModes;
}
}