summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/web/src/FormElement/ScheduleElement
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement.php636
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement/AnnuallyFields.php133
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsProtector.php41
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsUtils.php243
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement/FieldsRadio.php58
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement/MonthlyFields.php191
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement/Recurrence.php89
-rw-r--r--vendor/ipl/web/src/FormElement/ScheduleElement/WeeklyFields.php151
8 files changed, 1542 insertions, 0 deletions
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement.php b/vendor/ipl/web/src/FormElement/ScheduleElement.php
new file mode 100644
index 0000000..f872f49
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement.php
@@ -0,0 +1,636 @@
+<?php
+
+namespace ipl\Web\FormElement;
+
+use DateTime;
+use InvalidArgumentException;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\FieldsetElement;
+use ipl\Html\HtmlElement;
+use ipl\Scheduler\Contract\Frequency;
+use ipl\Scheduler\Cron;
+use ipl\Scheduler\OneOff;
+use ipl\Scheduler\RRule;
+use ipl\Validator\BetweenValidator;
+use ipl\Validator\CallbackValidator;
+use ipl\Web\FormElement\ScheduleElement\AnnuallyFields;
+use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector;
+use ipl\Web\FormElement\ScheduleElement\MonthlyFields;
+use ipl\Web\FormElement\ScheduleElement\Recurrence;
+use ipl\Web\FormElement\ScheduleElement\WeeklyFields;
+use LogicException;
+use Psr\Http\Message\RequestInterface;
+
+class ScheduleElement extends FieldsetElement
+{
+ use FieldsProtector;
+
+ /** @var string Plain cron expressions */
+ protected const CRON_EXPR = 'cron_expr';
+
+ /** @var string Configure the individual expression parts manually */
+ protected const CUSTOM_EXPR = 'custom';
+
+ /** @var string Used to run a one-off task */
+ protected const NO_REPEAT = 'none';
+
+ protected $defaultAttributes = ['class' => 'schedule-element'];
+
+ /** @var array A list of allowed frequencies used to configure custom expressions */
+ protected $customFrequencies = [];
+
+ /** @var array */
+ protected $advanced = [];
+
+ /** @var array */
+ protected $regulars = [];
+
+ /** @var string Schedule frequency of this element */
+ protected $frequency = self::NO_REPEAT;
+
+ /** @var string */
+ protected $customFrequency;
+
+ /** @var DateTime */
+ protected $start;
+
+ /** @var WeeklyFields Weekly parts of this schedule element */
+ protected $weeklyField;
+
+ /** @var MonthlyFields Monthly parts of this schedule element */
+ protected $monthlyFields;
+
+ /** @var AnnuallyFields Annually parts of this schedule element */
+ protected $annuallyFields;
+
+ protected function init(): void
+ {
+ $this->start = new DateTime();
+ $this->weeklyField = new WeeklyFields('weekly-fields', [
+ 'default' => $this->start->format('D'),
+ 'protector' => function (string $day) {
+ return $this->protectId($day);
+ },
+ ]);
+
+ $this->monthlyFields = new MonthlyFields('monthly-fields', [
+ 'default' => $this->start->format('j'),
+ 'availableFields' => (int) $this->start->format('t'),
+ 'protector' => function ($day) {
+ return $this->protectId($day);
+ }
+ ]);
+
+ $this->annuallyFields = new AnnuallyFields('annually-fields', [
+ 'default' => $this->start->format('M'),
+ 'protector' => function ($month) {
+ return $this->protectId($month);
+ }
+ ]);
+
+
+ $this->regulars = [
+ RRule::MINUTELY => $this->translate('Minutely'),
+ RRule::HOURLY => $this->translate('Hourly'),
+ RRule::DAILY => $this->translate('Daily'),
+ RRule::WEEKLY => $this->translate('Weekly'),
+ RRule::MONTHLY => $this->translate('Monthly'),
+ RRule::QUARTERLY => $this->translate('Quarterly'),
+ RRule::YEARLY => $this->translate('Annually'),
+ ];
+
+ $this->customFrequencies = array_slice($this->regulars, 2);
+ unset($this->customFrequencies[RRule::QUARTERLY]);
+
+ $this->advanced = [
+ static::CUSTOM_EXPR => $this->translate('Custom…'),
+ static::CRON_EXPR => $this->translate('Cron Expression…')
+ ];
+ }
+
+ /**
+ * Get whether this element is rendering a cron expression
+ *
+ * @return bool
+ */
+ public function hasCronExpression(): bool
+ {
+ return $this->getFrequency() === static::CRON_EXPR;
+ }
+
+ /**
+ * Get the frequency of this element
+ *
+ * @return string
+ */
+ public function getFrequency(): string
+ {
+ return $this->getPopulatedValue('frequency', $this->frequency);
+ }
+
+ /**
+ * Set the custom frequency of this schedule element
+ *
+ * @param string $frequency
+ *
+ * @return $this
+ */
+ public function setFrequency(string $frequency): self
+ {
+ if (
+ $frequency !== static::NO_REPEAT
+ && ! isset($this->regulars[$frequency])
+ && ! isset($this->advanced[$frequency])
+ ) {
+ throw new InvalidArgumentException(sprintf('Invalid frequency provided: %s', $frequency));
+ }
+
+ $this->frequency = $frequency;
+
+ return $this;
+ }
+
+ /**
+ * Get custom frequency of this element
+ *
+ * @return ?string
+ */
+ public function getCustomFrequency(): ?string
+ {
+ return $this->getValue('custom-frequency', $this->customFrequency);
+ }
+
+ /**
+ * Set custom frequency of this element
+ *
+ * @param string $frequency
+ *
+ * @return $this
+ */
+ public function setCustomFrequency(string $frequency): self
+ {
+ if (! isset($this->customFrequencies[$frequency])) {
+ throw new InvalidArgumentException(sprintf('Invalid custom frequency provided: %s', $frequency));
+ }
+
+ $this->customFrequency = $frequency;
+
+ return $this;
+ }
+
+ /**
+ * Set start time of the parsed expressions
+ *
+ * @param DateTime $start
+ *
+ * @return $this
+ */
+ public function setStart(DateTime $start): self
+ {
+ $this->start = $start;
+
+ // Forward the start time update to the sub elements as well!
+ $this->weeklyField->setDefault($start->format('D'));
+ $this->annuallyFields->setDefault($start->format('M'));
+ $this->monthlyFields
+ ->setDefault((int) $start->format('j'))
+ ->setAvailableFields((int) $start->format('t'));
+
+ return $this;
+ }
+
+ public function getValue($name = null, $default = null)
+ {
+ if ($name !== null || ! $this->hasBeenValidated()) {
+ return parent::getValue($name, $default);
+ }
+
+ $frequency = $this->getFrequency();
+ $start = parent::getValue('start');
+ switch ($frequency) {
+ case static::NO_REPEAT:
+ return new OneOff($start);
+ case static::CRON_EXPR:
+ $rule = new Cron(parent::getValue('cron_expression'));
+
+ break;
+ case RRule::MINUTELY:
+ case RRule::HOURLY:
+ case RRule::DAILY:
+ case RRule::WEEKLY:
+ case RRule::MONTHLY:
+ case RRule::QUARTERLY:
+ case RRule::YEARLY:
+ $rule = RRule::fromFrequency($frequency);
+
+ break;
+ default: // static::CUSTOM_EXPR
+ $interval = parent::getValue('interval', 1);
+ $customFrequency = parent::getValue('custom-frequency', RRule::DAILY);
+ switch ($customFrequency) {
+ case RRule::DAILY:
+ if ($interval === '*') {
+ $interval = 1;
+ }
+
+ $rule = new RRule("FREQ=DAILY;INTERVAL=$interval");
+
+ break;
+ case RRule::WEEKLY:
+ $byDay = implode(',', $this->weeklyField->getSelectedWeekDays());
+
+ $rule = new RRule("FREQ=WEEKLY;INTERVAL=$interval;BYDAY=$byDay");
+
+ break;
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case RRule::MONTHLY:
+ $runsOn = $this->monthlyFields->getValue('runsOn', MonthlyFields::RUNS_EACH);
+ if ($runsOn === MonthlyFields::RUNS_EACH) {
+ $byMonth = implode(',', $this->monthlyFields->getSelectedDays());
+
+ $rule = new RRule("FREQ=MONTHLY;INTERVAL=$interval;BYMONTHDAY=$byMonth");
+
+ break;
+ }
+ // Fall-through to the next switch case
+ case RRule::YEARLY:
+ $rule = "FREQ=MONTHLY;INTERVAL=$interval;";
+ if ($customFrequency === RRule::YEARLY) {
+ $runsOn = $this->annuallyFields->getValue('runsOnThe', 'n');
+ $month = $this->annuallyFields->getValue('month', (int) $this->start->format('m'));
+ if (is_string($month)) {
+ $datetime = DateTime::createFromFormat('!M', $month);
+ if (! $datetime) {
+ throw new InvalidArgumentException(sprintf('Invalid month provided: %s', $month));
+ }
+
+ $month = (int) $datetime->format('m');
+ }
+
+ $rule = "FREQ=YEARLY;INTERVAL=1;BYMONTH=$month;";
+ if ($runsOn === 'n') {
+ $rule = new RRule($rule);
+
+ break;
+ }
+ }
+
+ $element = $this->monthlyFields;
+ if ($customFrequency === RRule::YEARLY) {
+ $element = $this->annuallyFields;
+ }
+
+ $runDay = $element->getValue('day', $element::$everyDay);
+ $ordinal = $element->getValue('ordinal', $element::$first);
+ $position = $element->getOrdinalAsInteger($ordinal);
+
+ if ($runDay === $element::$everyDay) {
+ $rule .= "BYDAY=MO,TU,WE,TH,FR,SA,SU;BYSETPOS=$position";
+ } elseif ($runDay === $element::$everyWeekday) {
+ $rule .= "BYDAY=MO,TU,WE,TH,FR;BYSETPOS=$position";
+ } elseif ($runDay === $element::$everyWeekend) {
+ $rule .= "BYDAY=SA,SU;BYSETPOS=$position";
+ } else {
+ $rule .= sprintf('BYDAY=%d%s', $position, $runDay);
+ }
+
+ $rule = new RRule($rule);
+
+ break;
+ default:
+ throw new LogicException(sprintf('Custom frequency %s is not supported!', $customFrequency));
+ }
+ }
+
+ $rule->startAt($start);
+ if (parent::getValue('use-end-time', 'n') === 'y') {
+ $rule->endAt(parent::getValue('end'));
+ }
+
+ // Sync the start time and first recurrence of the rule
+ if (! $this->hasCronExpression() && $this->getFrequency() !== static::NO_REPEAT) {
+ $nextDue = $rule->getNextRecurrences($start)->current() ?? $start;
+ $rule->startAt($nextDue);
+ }
+
+ return $rule;
+ }
+
+ public function setValue($value)
+ {
+ $values = $value;
+ $rule = $value;
+ if ($rule instanceof Frequency) {
+ if ($rule->getStart()) {
+ $this->setStart($rule->getStart());
+ }
+
+ $values = [];
+ if ($rule->getEnd() && ! $rule instanceof OneOff) {
+ $values['use-end-time'] = 'y';
+ $values['end'] = $rule->getEnd();
+ }
+
+ if ($rule instanceof OneOff) {
+ $values['frequency'] = static::NO_REPEAT;
+ } elseif ($rule instanceof Cron) {
+ $values['cron_expression'] = $rule->getExpression();
+ $values['frequency'] = static::CRON_EXPR;
+
+ $this->setFrequency(static::CRON_EXPR);
+ } elseif ($rule instanceof RRule) {
+ $values['interval'] = $rule->getInterval();
+ switch ($rule->getFrequency()) {
+ case RRule::DAILY:
+ if ($rule->getInterval() <= 1 && strpos($rule->getString(), 'INTERVAL=') === false) {
+ $this->setFrequency(RRule::DAILY);
+ } else {
+ $this
+ ->setFrequency(static::CUSTOM_EXPR)
+ ->setCustomFrequency(RRule::DAILY);
+ }
+
+ break;
+ case RRule::WEEKLY:
+ if (! $rule->getByDay() || empty($rule->getByDay())) {
+ $this->setFrequency(RRule::WEEKLY);
+ } else {
+ $values['weekly-fields'] = $this->weeklyField->loadWeekDays($rule->getByDay());
+ $this
+ ->setFrequency(static::CUSTOM_EXPR)
+ ->setCustomFrequency(RRule::WEEKLY);
+ }
+
+ break;
+ case RRule::MONTHLY:
+ case RRule::YEARLY:
+ $isMonthly = $rule->getFrequency() === RRule::MONTHLY;
+ if ($rule->getByDay() || $rule->getByMonthDay() || $rule->getByMonth()) {
+ $this->setFrequency(static::CUSTOM_EXPR);
+
+ if ($isMonthly) {
+ $values['monthly-fields'] = $this->monthlyFields->loadRRule($rule);
+ $this->setCustomFrequency(RRule::MONTHLY);
+ } else {
+ $values['annually-fields'] = $this->annuallyFields->loadRRule($rule);
+ $this->setCustomFrequency(RRule::YEARLY);
+ }
+ } elseif ($isMonthly && $rule->getInterval() === 3) {
+ $this->setFrequency(RRule::QUARTERLY);
+ } else {
+ $this->setFrequency($rule->getFrequency());
+ }
+
+ break;
+ default:
+ $this->setFrequency($rule->getFrequency());
+ }
+
+ $values['frequency'] = $this->getFrequency();
+ $values['custom-frequency'] = $this->getCustomFrequency();
+ }
+ }
+
+ return parent::setValue($values);
+ }
+
+ protected function assemble()
+ {
+ $start = $this->getPopulatedValue('start') ?: $this->start;
+ if (! $start instanceof DateTime) {
+ $start = new DateTime($start);
+ }
+ $this->setStart($start);
+
+ $autosubmit = ! $this->hasCronExpression() && $this->getFrequency() !== static::NO_REPEAT;
+ $this->addElement('localDateTime', 'start', [
+ 'class' => $autosubmit ? 'autosubmit' : null,
+ 'required' => true,
+ 'label' => $this->translate('Start'),
+ 'value' => $start,
+ 'description' => $this->translate('Start time of this schedule')
+ ]);
+
+ $this->addElement('checkbox', 'use-end-time', [
+ 'required' => false,
+ 'class' => 'autosubmit',
+ 'disabled' => $this->getPopulatedValue('frequency', static::NO_REPEAT) === static::NO_REPEAT ?: null,
+ 'value' => $this->getPopulatedValue('use-end-time', 'n'),
+ 'label' => $this->translate('Use End Time')
+ ]);
+
+ if ($this->getPopulatedValue('use-end-time', 'n') === 'y') {
+ $end = $this->getPopulatedValue('end', new DateTime());
+ if (! $end instanceof DateTime) {
+ $end = new DateTime($end);
+ }
+
+ $this->addElement('localDateTime', 'end', [
+ 'class' => ! $this->hasCronExpression() ? 'autosubmit' : null,
+ 'required' => true,
+ 'value' => $end,
+ 'label' => $this->translate('End'),
+ 'description' => $this->translate('End time of this schedule')
+ ]);
+ }
+
+ $this->addElement('select', 'frequency', [
+ 'required' => false,
+ 'class' => 'autosubmit',
+ 'label' => $this->translate('Frequency'),
+ 'description' => $this->translate('Specifies how often this job run should be recurring'),
+ 'options' => [
+ static::NO_REPEAT => $this->translate('None'),
+ $this->translate('Regular') => $this->regulars,
+ $this->translate('Advanced') => $this->advanced
+ ],
+ ]);
+
+ if ($this->getFrequency() === static::CUSTOM_EXPR) {
+ $this->addElement('select', 'custom-frequency', [
+ 'required' => false,
+ 'class' => 'autosubmit',
+ 'value' => parent::getValue('custom-frequency'),
+ 'options' => $this->customFrequencies,
+ 'label' => $this->translate('Custom Frequency'),
+ 'description' => $this->translate('Specifies how often this job run should be recurring')
+ ]);
+
+ switch (parent::getValue('custom-frequency', RRule::DAILY)) {
+ case RRule::DAILY:
+ $this->assembleCommonElements();
+
+ break;
+ case RRule::WEEKLY:
+ $this->assembleCommonElements();
+ $this->addElement($this->weeklyField);
+
+ break;
+ case RRule::MONTHLY:
+ $this->assembleCommonElements();
+ $this->addElement($this->monthlyFields);
+
+ break;
+ case RRule::YEARLY:
+ $this->addElement($this->annuallyFields);
+ }
+ } elseif ($this->hasCronExpression()) {
+ $this->addElement('text', 'cron_expression', [
+ 'required' => true,
+ 'label' => $this->translate('Cron Expression'),
+ 'description' => $this->translate('Job cron Schedule'),
+ 'validators' => [
+ new CallbackValidator(function ($value, CallbackValidator $validator) {
+ if ($value && ! Cron::isValid($value)) {
+ $validator->addMessage($this->translate('Invalid CRON expression'));
+
+ return false;
+ }
+
+ return true;
+ })
+ ]
+ ]);
+ }
+
+ if ($this->getFrequency() !== static::NO_REPEAT && ! $this->hasCronExpression()) {
+ $this->addElement(
+ new Recurrence('schedule-recurrences', [
+ 'id' => $this->protectId('schedule-recurrences'),
+ 'label' => $this->translate('Next occurrences'),
+ 'validate' => function (): array {
+ $isValid = $this->isValid();
+ $reason = null;
+ if (! $isValid && $this->getFrequency() === static::CUSTOM_EXPR) {
+ if (
+ $this->getCustomFrequency() !== RRule::YEARLY
+ && ! $this->getElement('interval')->isValid()
+ ) {
+ $reason = current($this->getElement('interval')->getMessages());
+ } else {
+ $frequency = $this->getCustomFrequency();
+ switch ($frequency) {
+ case RRule::WEEKLY:
+ $reason = current($this->weeklyField->getMessages());
+
+ break;
+ case RRule::MONTHLY:
+ $reason = current($this->monthlyFields->getMessages());
+
+ break;
+ default: // annually
+ $reason = current($this->annuallyFields->getMessages());
+
+ break;
+ }
+ }
+ }
+
+ return [$isValid, $reason];
+ },
+ 'frequency' => function (): Frequency {
+ if ($this->getFrequency() === static::CUSTOM_EXPR) {
+ $rule = $this->getValue();
+ } else {
+ $rule = RRule::fromFrequency($this->getFrequency());
+ }
+
+ $now = new DateTime();
+ $start = $this->getValue('start');
+ if ($start < $now) {
+ $now->setTime($start->format('H'), $start->format('i'), $start->format('s'));
+ $start = $now;
+ }
+
+ $rule->startAt($start);
+ if ($this->getPopulatedValue('use-end-time') === 'y') {
+ $rule->endAt($this->getValue('end'));
+ }
+
+ return $rule;
+ }
+ ])
+ );
+ }
+ }
+
+ /**
+ * Assemble common parts for all the frequencies
+ */
+ private function assembleCommonElements(): void
+ {
+ $repeat = $this->getCustomFrequency();
+ if ($repeat === RRule::WEEKLY) {
+ $text = $this->translate('week(s) on');
+ $max = 53;
+ } elseif ($repeat === RRule::MONTHLY) {
+ $text = $this->translate('month(s)');
+ $max = 12;
+ } else {
+ $text = $this->translate('day(s)');
+ $max = 31;
+ }
+
+ $options = ['min' => 1, 'max' => $max];
+ $this->addElement('number', 'interval', [
+ 'class' => 'autosubmit',
+ 'value' => 1,
+ 'min' => 1,
+ 'max' => $max,
+ 'validators' => [new BetweenValidator($options)]
+ ]);
+
+ $numberSpecifier = HtmlElement::create('div', ['class' => 'number-specifier']);
+ $element = $this->getElement('interval');
+ $element->prependWrapper($numberSpecifier);
+
+ $numberSpecifier->prependHtml(HtmlElement::create('span', null, $this->translate('Every')));
+ $numberSpecifier->addHtml($element);
+ $numberSpecifier->addHtml(HtmlElement::create('span', null, $text));
+ }
+
+ /**
+ * Get prepared multipart updates
+ *
+ * @param RequestInterface $request
+ *
+ * @return array
+ */
+ public function prepareMultipartUpdate(RequestInterface $request): array
+ {
+ $autoSubmittedBy = $request->getHeader('X-Icinga-AutoSubmittedBy');
+ $pattern = '/\[(weekly-fields|monthly-fields|annually-fields)]\[(ordinal|month|day(\d+)?|[A-Z]{2})]$/';
+
+ $partUpdates = [];
+ if (
+ $autoSubmittedBy
+ && (
+ preg_match('/\[(start|end)]$/', $autoSubmittedBy[0], $matches)
+ || preg_match($pattern, $autoSubmittedBy[0])
+ || preg_match('/\[interval]/', $autoSubmittedBy[0])
+ )
+ ) {
+ $this->ensureAssembled();
+
+ $partUpdates[] = $this->getElement('schedule-recurrences');
+ if (
+ $this->getFrequency() === static::CUSTOM_EXPR
+ && $this->getCustomFrequency() === RRule::MONTHLY
+ && isset($matches[1])
+ && $matches[1] === 'start'
+ ) {
+ // To update the available fields/days based on the provided start time
+ $partUpdates[] = $this->monthlyFields;
+ }
+ }
+
+ return $partUpdates;
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+
+ $attributes->registerAttributeCallback('protector', null, [$this, 'setIdProtector']);
+ }
+}
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/AnnuallyFields.php b/vendor/ipl/web/src/FormElement/ScheduleElement/AnnuallyFields.php
new file mode 100644
index 0000000..857711a
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement/AnnuallyFields.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace ipl\Web\FormElement\ScheduleElement;
+
+use InvalidArgumentException;
+use ipl\Html\Attributes;
+use ipl\Html\FormattedString;
+use ipl\Html\FormElement\FieldsetElement;
+use ipl\Html\HtmlElement;
+use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector;
+use ipl\Web\FormElement\ScheduleElement\Common\FieldsUtils;
+use ipl\Web\Widget\Icon;
+
+class AnnuallyFields extends FieldsetElement
+{
+ use FieldsUtils;
+ use FieldsProtector;
+
+ /** @var array A list of valid months */
+ protected $months = [];
+
+ /** @var string A month to preselect by default */
+ protected $default = 'JAN';
+
+ public function __construct($name, $attributes = null)
+ {
+ $this->months = [
+ 'JAN' => $this->translate('Jan'),
+ 'FEB' => $this->translate('Feb'),
+ 'MAR' => $this->translate('Mar'),
+ 'APR' => $this->translate('Apr'),
+ 'MAY' => $this->translate('May'),
+ 'JUN' => $this->translate('Jun'),
+ 'JUL' => $this->translate('Jul'),
+ 'AUG' => $this->translate('Aug'),
+ 'SEP' => $this->translate('Sep'),
+ 'OCT' => $this->translate('Oct'),
+ 'NOV' => $this->translate('Nov'),
+ 'DEC' => $this->translate('Dec')
+ ];
+
+ parent::__construct($name, $attributes);
+ }
+
+ protected function init(): void
+ {
+ parent::init();
+ $this->initUtils();
+ }
+
+ /**
+ * Set the default month to be activated
+ *
+ * @param string $default
+ *
+ * @return $this
+ */
+ public function setDefault(string $default): self
+ {
+ if (! isset($this->months[strtoupper($this->default)])) {
+ throw new InvalidArgumentException(sprintf('Invalid month provided: %s', $default));
+ }
+
+ $this->default = strtoupper($default);
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->getAttributes()->set('id', $this->protectId('annually-fields'));
+
+ $fieldsSelector = new FieldsRadio('month', [
+ 'class' => ['autosubmit', 'sr-only'],
+ 'value' => $this->default,
+ 'options' => $this->months,
+ 'protector' => function ($value) {
+ return $this->protectId($value);
+ }
+ ]);
+ $this->registerElement($fieldsSelector);
+
+ $runsOnThe = $this->getPopulatedValue('runsOnThe', 'n');
+ $this->addElement('checkbox', 'runsOnThe', [
+ 'class' => 'autosubmit',
+ 'value' => $runsOnThe
+ ]);
+
+ $checkboxControls = HtmlElement::create('div', ['class' => 'toggle-slider-controls']);
+ $checkbox = $this->getElement('runsOnThe');
+ $checkbox->prependWrapper($checkboxControls);
+ $checkboxControls->addHtml($checkbox, HtmlElement::create('span', null, $this->translate('On the')));
+
+ $annuallyWrapper = HtmlElement::create('div', ['class' => 'annually']);
+ $checkboxControls->prependWrapper($annuallyWrapper);
+ $annuallyWrapper->addHtml($fieldsSelector);
+
+ $notes = HtmlElement::create('div', ['class' => 'note']);
+ $notes->addHtml(
+ FormattedString::create(
+ $this->translate('Use %s / %s keys to choose a month by keyboard.'),
+ new Icon('arrow-left'),
+ new Icon('arrow-right')
+ )
+ );
+ $annuallyWrapper->addHtml($notes);
+
+ $enumerations = $this->createOrdinalElement();
+ $enumerations->getAttributes()->set('disabled', $runsOnThe === 'n');
+ $this->registerElement($enumerations);
+
+ $selectableDays = $this->createOrdinalSelectableDays();
+ $selectableDays->getAttributes()->set('disabled', $runsOnThe === 'n');
+ $this->registerElement($selectableDays);
+
+ $ordinalWrapper = HtmlElement::create('div', ['class' => ['ordinal', 'annually']]);
+ $this
+ ->decorate($enumerations)
+ ->addHtml($enumerations);
+
+ $enumerations->prependWrapper($ordinalWrapper);
+ $ordinalWrapper->addHtml($enumerations, $selectableDays);
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+
+ $attributes
+ ->registerAttributeCallback('default', null, [$this, 'setDefault'])
+ ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']);
+ }
+}
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsProtector.php b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsProtector.php
new file mode 100644
index 0000000..affd519
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsProtector.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace ipl\Web\FormElement\ScheduleElement\Common;
+
+trait FieldsProtector
+{
+ /** @var callable */
+ protected $protector;
+
+ /**
+ * Set callback to protect ids with
+ *
+ * @param ?callable $protector
+ *
+ * @return $this
+ */
+ public function setIdProtector(?callable $protector): self
+ {
+ $this->protector = $protector;
+
+ return $this;
+ }
+
+ /**
+ * Protect the given html id
+ *
+ * The provided id is returned as is, if no protector is specified
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public function protectId(string $id): string
+ {
+ if (is_callable($this->protector)) {
+ return call_user_func($this->protector, $id);
+ }
+
+ return $id;
+ }
+}
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsUtils.php b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsUtils.php
new file mode 100644
index 0000000..bf28255
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement/Common/FieldsUtils.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace ipl\Web\FormElement\ScheduleElement\Common;
+
+use DateInterval;
+use DateTime;
+use Exception;
+use InvalidArgumentException;
+use ipl\Html\Contract\FormElement;
+use ipl\Scheduler\RRule;
+use ipl\Web\FormElement\ScheduleElement\MonthlyFields;
+
+trait FieldsUtils
+{
+ // Non-standard frequency options
+ public static $everyDay = 'day';
+ public static $everyWeekday = 'weekday';
+ public static $everyWeekend = 'weekend';
+
+ // Enumerators for the monthly and annually schedule of a custom frequency
+ public static $first = 'first';
+ public static $second = 'second';
+ public static $third = 'third';
+ public static $fourth = 'fourth';
+ public static $fifth = 'fifth';
+ public static $last = 'last';
+
+ private $regulars = [];
+
+ protected function initUtils(): void
+ {
+ $this->regulars = [
+ 'MO' => $this->translate('Monday'),
+ 'TU' => $this->translate('Tuesday'),
+ 'WE' => $this->translate('Wednesday'),
+ 'TH' => $this->translate('Thursday'),
+ 'FR' => $this->translate('Friday'),
+ 'SA' => $this->translate('Saturday'),
+ 'SU' => $this->translate('Sunday')
+ ];
+ }
+
+ protected function createOrdinalElement(): FormElement
+ {
+ return $this->createElement('select', 'ordinal', [
+ 'class' => 'autosubmit',
+ 'value' => $this->getPopulatedValue('ordinal', static::$first),
+ 'options' => [
+ static::$first => $this->translate('First'),
+ static::$second => $this->translate('Second'),
+ static::$third => $this->translate('Third'),
+ static::$fourth => $this->translate('Fourth'),
+ static::$fifth => $this->translate('Fifth'),
+ static::$last => $this->translate('Last')
+ ]
+ ]);
+ }
+
+ protected function createOrdinalSelectableDays(): FormElement
+ {
+ $select = $this->createElement('select', 'day', [
+ 'class' => 'autosubmit',
+ 'value' => $this->getPopulatedValue('day', static::$everyDay),
+ 'options' => $this->regulars + [
+ 'separator' => '──────────────────────────',
+ static::$everyDay => $this->translate('Day'),
+ static::$everyWeekday => $this->translate('Weekday (Mon - Fri)'),
+ static::$everyWeekend => $this->translate('WeekEnd (Sat or Sun)')
+ ]
+ ]);
+ $select->getOption('separator')->getAttributes()->set('disabled', true);
+
+ return $select;
+ }
+
+ /**
+ * Load the given RRule instance into a list of key=>value pairs
+ *
+ * @param RRule $rule
+ *
+ * @return array
+ */
+ public function loadRRule(RRule $rule): array
+ {
+ $values = [];
+ $isMonthly = $rule->getFrequency() === RRule::MONTHLY;
+ if ($isMonthly && (! empty($rule->getByMonthDay()) || empty($rule->getByDay()))) {
+ $monthDays = $rule->getByMonthDay() ?? [];
+ foreach (range(1, $this->availableFields) as $value) {
+ $values["day$value"] = in_array((string) $value, $monthDays, true) ? 'y' : 'n';
+ }
+
+ $values['runsOn'] = MonthlyFields::RUNS_EACH;
+ } else {
+ $position = $rule->getBySetPosition();
+ $byDay = $rule->getByDay() ?? [];
+
+ if ($isMonthly) {
+ $values['runsOn'] = MonthlyFields::RUNS_ONTHE;
+ } else {
+ $months = $rule->getByMonth();
+ if (empty($months) && $rule->getStart()) {
+ $months[] = $rule->getStart()->format('m');
+ } elseif (empty($months)) {
+ $months[] = date('m');
+ }
+
+ $values['month'] = strtoupper($this->getMonthByNumber((int)$months[0]));
+ $values['runsOnThe'] = ! empty($byDay) ? 'y' : 'n';
+ }
+
+ if (count($byDay) == 1 && preg_match('/^(-?\d)(\w.*)$/', $byDay[0], $matches)) {
+ $values['ordinal'] = $this->getOrdinalString($matches[1]);
+ $values['day'] = $this->getWeekdayName($matches[2]);
+ } elseif (! empty($byDay)) {
+ $values['ordinal'] = $this->getOrdinalString(current($position));
+ switch (count($byDay)) {
+ case MonthlyFields::WEEK_DAYS:
+ $values['day'] = static::$everyDay;
+
+ break;
+ case MonthlyFields::WEEK_DAYS - 2:
+ $values['day'] = static::$everyWeekday;
+
+ break;
+ case 1:
+ $values['day'] = current($byDay);
+
+ break;
+ case 2:
+ $byDay = array_flip($byDay);
+ if (isset($byDay['SA']) && isset($byDay['SU'])) {
+ $values['day'] = static::$everyWeekend;
+ }
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Transform the given expression part into a valid week day string representation
+ *
+ * @param string $day
+ *
+ * @return string
+ */
+ public function getWeekdayName(string $day): string
+ {
+ // Not transformation is needed when the given day is part of the valid weekdays
+ if (isset($this->regulars[strtoupper($day)])) {
+ return $day;
+ }
+
+ try {
+ // Try to figure it out using date time before raising an error
+ $datetime = new DateTime('Sunday');
+ $datetime->add(new DateInterval("P$day" . 'D'));
+
+ return $datetime->format('D');
+ } catch (Exception $_) {
+ throw new InvalidArgumentException(sprintf('Invalid weekday provided: %s', $day));
+ }
+ }
+
+ /**
+ * Transform the given integer enums into something like first,second...
+ *
+ * @param string $ordinal
+ *
+ * @return string
+ */
+ public function getOrdinalString(string $ordinal): string
+ {
+ switch ($ordinal) {
+ case '1':
+ return static::$first;
+ case '2':
+ return static::$second;
+ case '3':
+ return static::$third;
+ case '4':
+ return static::$fourth;
+ case '5':
+ return static::$fifth;
+ case '-1':
+ return static::$last;
+ default:
+ throw new InvalidArgumentException(
+ sprintf('Invalid ordinal string representation provided: %s', $ordinal)
+ );
+ }
+ }
+
+ /**
+ * Get the string representation of the given ordinal to an integer
+ *
+ * This transforms the given ordinal such as (first, second...) into its respective
+ * integral representation. At the moment only (1..5 + the non-standard "last") options
+ * are supported. So if this method returns the character "-1", is meant the last option.
+ *
+ * @param string $ordinal
+ *
+ * @return int
+ */
+ public function getOrdinalAsInteger(string $ordinal): int
+ {
+ switch ($ordinal) {
+ case static::$first:
+ return 1;
+ case static::$second:
+ return 2;
+ case static::$third:
+ return 3;
+ case static::$fourth:
+ return 4;
+ case static::$fifth:
+ return 5;
+ case static::$last:
+ return -1;
+ default:
+ throw new InvalidArgumentException(sprintf('Invalid enumerator provided: %s', $ordinal));
+ }
+ }
+
+ /**
+ * Get a short textual representation of the given month
+ *
+ * @param int $month
+ *
+ * @return string
+ */
+ public function getMonthByNumber(int $month): string
+ {
+ $time = DateTime::createFromFormat('!m', $month);
+ if ($time) {
+ return $time->format('M');
+ }
+
+ throw new InvalidArgumentException(sprintf('Invalid month number provided: %d', $month));
+ }
+}
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/FieldsRadio.php b/vendor/ipl/web/src/FormElement/ScheduleElement/FieldsRadio.php
new file mode 100644
index 0000000..31b77c3
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement/FieldsRadio.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace ipl\Web\FormElement\ScheduleElement;
+
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\InputElement;
+use ipl\Html\FormElement\RadioElement;
+use ipl\Html\HtmlElement;
+use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector;
+
+class FieldsRadio extends RadioElement
+{
+ use FieldsProtector;
+
+ protected function assemble()
+ {
+ $listItems = HtmlElement::create('ul', ['class' => ['schedule-element-fields', 'single-fields']]);
+ foreach ($this->options as $option) {
+ $radio = (new InputElement($this->getValueOfNameAttribute()))
+ ->setValue($option->getValue())
+ ->setType($this->type);
+
+ $radio->setAttributes(clone $this->getAttributes());
+
+ $htmlId = $this->protectId($option->getValue());
+ $radio->getAttributes()
+ ->set('id', $htmlId)
+ ->registerAttributeCallback('checked', function () use ($option) {
+ return (string) $this->getValue() === (string) $option->getValue();
+ })
+ ->registerAttributeCallback('required', [$this, 'getRequiredAttribute'])
+ ->registerAttributeCallback('disabled', function () use ($option) {
+ return $this->getAttributes()->get('disabled')->getValue() || $option->isDisabled();
+ });
+
+ $listItem = HtmlElement::create('li');
+ $listItem->addHtml(
+ $radio,
+ HtmlElement::create('label', [
+ 'for' => $htmlId,
+ 'class' => $option->getLabelCssClass(),
+ 'tabindex' => -1
+ ], $option->getLabel())
+ );
+ $listItems->addHtml($listItem);
+ }
+
+ $this->addHtml($listItems);
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+
+ $attributes
+ ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']);
+ }
+}
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/MonthlyFields.php b/vendor/ipl/web/src/FormElement/ScheduleElement/MonthlyFields.php
new file mode 100644
index 0000000..26329fc
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement/MonthlyFields.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace ipl\Web\FormElement\ScheduleElement;
+
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\FieldsetElement;
+use ipl\Html\HtmlElement;
+use ipl\Validator\CallbackValidator;
+use ipl\Validator\InArrayValidator;
+use ipl\Validator\ValidatorChain;
+use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector;
+use ipl\Web\FormElement\ScheduleElement\Common\FieldsUtils;
+
+class MonthlyFields extends FieldsetElement
+{
+ use FieldsUtils;
+ use FieldsProtector;
+
+ /** @var string Used as radio option to run each selected days/months */
+ public const RUNS_EACH = 'each';
+
+ /** @var string Used as radio option to build complex job schedules */
+ public const RUNS_ONTHE = 'onthe';
+
+ /** @var int Number of days in a week */
+ public const WEEK_DAYS = 7;
+
+ /** @var int Day of the month to preselect by default */
+ protected $default = 1;
+
+ /** @var int Number of fields to render */
+ protected $availableFields;
+
+ protected function init(): void
+ {
+ parent::init();
+ $this->initUtils();
+
+ $this->availableFields = (int) date('t');
+ }
+
+ /**
+ * Set the available fields/days of the month to be rendered
+ *
+ * @param int $fields
+ *
+ * @return $this
+ */
+ public function setAvailableFields(int $fields): self
+ {
+ $this->availableFields = $fields;
+
+ return $this;
+ }
+
+ /**
+ * Set the default field/day to be selected
+ *
+ * @param int $default
+ *
+ * @return $this
+ */
+ public function setDefault(int $default): self
+ {
+ $this->default = $default;
+
+ return $this;
+ }
+
+ /**
+ * Get all the selected weekdays
+ *
+ * @return array
+ */
+ public function getSelectedDays(): array
+ {
+ $selectedDays = [];
+ foreach (range(1, $this->availableFields) as $day) {
+ if ($this->getValue("day$day", 'n') === 'y') {
+ $selectedDays[] = $day;
+ }
+ }
+
+ if (empty($selectedDays)) {
+ $selectedDays[] = $this->default;
+ }
+
+ return $selectedDays;
+ }
+
+ protected function assemble()
+ {
+ $this->getAttributes()->set('id', $this->protectId('monthly-fields'));
+
+ $runsOn = $this->getPopulatedValue('runsOn', static::RUNS_EACH);
+ $this->addElement('radio', 'runsOn', [
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'value' => $runsOn,
+ 'options' => [static::RUNS_EACH => $this->translate('Each')],
+ ]);
+
+ $listItems = HtmlElement::create('ul', ['class' => ['schedule-element-fields', 'multiple-fields']]);
+ if ($runsOn === static::RUNS_ONTHE) {
+ $listItems->getAttributes()->add('class', 'disabled');
+ }
+
+ foreach (range(1, $this->availableFields) as $day) {
+ $checkbox = $this->createElement('checkbox', "day$day", [
+ 'class' => ['autosubmit', 'sr-only'],
+ 'value' => $day === $this->default && $runsOn === static::RUNS_EACH
+ ]);
+ $this->registerElement($checkbox);
+
+ $htmlId = $this->protectId("day$day");
+ $checkbox->getAttributes()->set('id', $htmlId);
+
+ $listItem = HtmlElement::create('li');
+ $listItem->addHtml($checkbox, HtmlElement::create('label', ['for' => $htmlId], $day));
+ $listItems->addHtml($listItem);
+ }
+
+ $monthlyWrapper = HtmlElement::create('div', ['class' => 'monthly']);
+ $runsEach = $this->getElement('runsOn');
+ $runsEach->prependWrapper($monthlyWrapper);
+ $monthlyWrapper->addHtml($runsEach, $listItems);
+
+ $this->addElement('radio', 'runsOn', [
+ 'required' => $runsOn !== static::RUNS_EACH,
+ 'class' => 'autosubmit',
+ 'options' => [static::RUNS_ONTHE => $this->translate('On the')],
+ 'validators' => [
+ new InArrayValidator([
+ 'strict' => true,
+ 'haystack' => [static::RUNS_EACH, static::RUNS_ONTHE]
+ ])
+ ]
+ ]);
+
+ $ordinalWrapper = HtmlElement::create('div', ['class' => 'ordinal']);
+ $runsOnThe = $this->getElement('runsOn');
+ $runsOnThe->prependWrapper($ordinalWrapper);
+ $ordinalWrapper->addHtml($runsOnThe);
+
+ $enumerations = $this->createOrdinalElement();
+ $enumerations->getAttributes()->set('disabled', $runsOn === static::RUNS_EACH);
+ $this->registerElement($enumerations);
+
+ $selectableDays = $this->createOrdinalSelectableDays();
+ $selectableDays->getAttributes()->set('disabled', $runsOn === static::RUNS_EACH);
+ $this->registerElement($selectableDays);
+
+ $ordinalWrapper->addHtml($enumerations, $selectableDays);
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+
+ $attributes
+ ->registerAttributeCallback('default', null, [$this, 'setDefault'])
+ ->registerAttributeCallback('availableFields', null, [$this, 'setAvailableFields'])
+ ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']);
+ }
+
+ protected function addDefaultValidators(ValidatorChain $chain): void
+ {
+ $chain->add(
+ new CallbackValidator(function ($_, CallbackValidator $validator): bool {
+ if ($this->getValue('runsOn', static::RUNS_EACH) !== static::RUNS_EACH) {
+ return true;
+ }
+
+ $valid = false;
+ foreach (range(1, $this->availableFields) as $day) {
+ if ($this->getValue("day$day") === 'y') {
+ $valid = true;
+
+ break;
+ }
+ }
+
+ if (! $valid) {
+ $validator->addMessage($this->translate('You must select at least one of these days'));
+ }
+
+ return $valid;
+ })
+ );
+ }
+}
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/Recurrence.php b/vendor/ipl/web/src/FormElement/ScheduleElement/Recurrence.php
new file mode 100644
index 0000000..8693b20
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement/Recurrence.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace ipl\Web\FormElement\ScheduleElement;
+
+use DateTime;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\BaseFormElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Scheduler\Contract\Frequency;
+use ipl\Scheduler\RRule;
+
+class Recurrence extends BaseFormElement
+{
+ use Translation;
+
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'schedule-recurrences'];
+
+ /** @var callable A callable that generates a frequency instance */
+ protected $frequencyCallback;
+
+ /** @var callable A validation callback for the schedule element */
+ protected $validateCallback;
+
+ /**
+ * Set a validation callback that will be called when assembling this element
+ *
+ * @param callable $callback
+ *
+ * @return $this
+ */
+ public function setValid(callable $callback): self
+ {
+ $this->validateCallback = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Set a callback that generates an {@see Frequency} instance
+ *
+ * @param callable $callback
+ *
+ * @return $this
+ */
+ public function setFrequency(callable $callback): self
+ {
+ $this->frequencyCallback = $callback;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ list($isValid, $reason) = ($this->validateCallback)();
+ if (! $isValid) {
+ // Render why we can't generate the recurrences
+ $this->addHtml(Text::create($reason));
+
+ return;
+ }
+
+ /** @var RRule $frequency */
+ $frequency = ($this->frequencyCallback)();
+ $recurrences = $frequency->getNextRecurrences(new DateTime(), 3);
+ if (! $recurrences->valid()) {
+ // Such a situation can be caused by setting an invalid end time
+ $this->addHtml(HtmlElement::create('p', null, Text::create($this->translate('Never'))));
+
+ return;
+ }
+
+ foreach ($recurrences as $recurrence) {
+ $this->addHtml(HtmlElement::create('p', null, $recurrence->format($this->translate('D, Y/m/d, H:i:s'))));
+ }
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+
+ $attributes
+ ->registerAttributeCallback('frequency', null, [$this, 'setFrequency'])
+ ->registerAttributeCallback('validate', null, [$this, 'setValid']);
+ }
+}
diff --git a/vendor/ipl/web/src/FormElement/ScheduleElement/WeeklyFields.php b/vendor/ipl/web/src/FormElement/ScheduleElement/WeeklyFields.php
new file mode 100644
index 0000000..01933ca
--- /dev/null
+++ b/vendor/ipl/web/src/FormElement/ScheduleElement/WeeklyFields.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace ipl\Web\FormElement\ScheduleElement;
+
+use InvalidArgumentException;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\FieldsetElement;
+use ipl\Html\HtmlElement;
+use ipl\Validator\CallbackValidator;
+use ipl\Validator\ValidatorChain;
+use ipl\Web\FormElement\ScheduleElement\Common\FieldsProtector;
+
+class WeeklyFields extends FieldsetElement
+{
+ use FieldsProtector;
+
+ /** @var array A list of valid week days */
+ protected $weekdays = [];
+
+ /** @var string A valid weekday to be selected by default */
+ protected $default = 'MO';
+
+ public function __construct($name, $attributes = null)
+ {
+ $this->weekdays = [
+ 'MO' => $this->translate('Mon'),
+ 'TU' => $this->translate('Tue'),
+ 'WE' => $this->translate('Wed'),
+ 'TH' => $this->translate('Thu'),
+ 'FR' => $this->translate('Fri'),
+ 'SA' => $this->translate('Sat'),
+ 'SU' => $this->translate('Sun')
+ ];
+
+ parent::__construct($name, $attributes);
+ }
+
+ /**
+ * Set the default weekday to be preselected
+ *
+ * @param string $default
+ *
+ * @return $this
+ */
+ public function setDefault(string $default): self
+ {
+ $weekday = strlen($default) > 2 ? substr($default, 0, -1) : $default;
+ if (! isset($this->weekdays[strtoupper($weekday)])) {
+ throw new InvalidArgumentException(sprintf('Invalid weekday provided: %s', $default));
+ }
+
+ $this->default = strtoupper($weekday);
+
+ return $this;
+ }
+
+ /**
+ * Get all the selected weekdays
+ *
+ * @return array
+ */
+ public function getSelectedWeekDays(): array
+ {
+ $selectedDays = [];
+ foreach ($this->weekdays as $day => $_) {
+ if ($this->getValue($day, 'n') === 'y') {
+ $selectedDays[] = $day;
+ }
+ }
+
+ if (empty($selectedDays)) {
+ $selectedDays[] = $this->default;
+ }
+
+ return $selectedDays;
+ }
+
+ /**
+ * Transform the given weekdays into key=>value array that can be populated
+ *
+ * @param array $weekdays
+ *
+ * @return array
+ */
+ public function loadWeekDays(array $weekdays): array
+ {
+ $values = [];
+ foreach ($this->weekdays as $weekday => $_) {
+ $values[$weekday] = in_array($weekday, $weekdays, true) ? 'y' : 'n';
+ }
+
+ return $values;
+ }
+
+ protected function assemble()
+ {
+ $this->getAttributes()->set('id', $this->protectId('weekly-fields'));
+
+ $fieldsWrapper = HtmlElement::create('div', ['class' => 'weekly']);
+ $listItems = HtmlElement::create('ul', ['class' => ['schedule-element-fields', 'multiple-fields']]);
+
+ foreach ($this->weekdays as $day => $value) {
+ $checkbox = $this->createElement('checkbox', $day, [
+ 'class' => ['autosubmit', 'sr-only'],
+ 'value' => $day === $this->default
+ ]);
+ $this->registerElement($checkbox);
+
+ $htmlId = $this->protectId("weekday-$day");
+ $checkbox->getAttributes()->set('id', $htmlId);
+
+ $listItem = HtmlElement::create('li');
+ $listItem->addHtml($checkbox, HtmlElement::create('label', ['for' => $htmlId], $value));
+ $listItems->addHtml($listItem);
+ }
+
+ $fieldsWrapper->addHtml($listItems);
+ $this->addHtml($fieldsWrapper);
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+
+ $attributes
+ ->registerAttributeCallback('default', null, [$this, 'setDefault'])
+ ->registerAttributeCallback('protector', null, [$this, 'setIdProtector']);
+ }
+
+ protected function addDefaultValidators(ValidatorChain $chain): void
+ {
+ $chain->add(
+ new CallbackValidator(function ($_, CallbackValidator $validator): bool {
+ $valid = false;
+ foreach ($this->weekdays as $weekday => $_) {
+ if ($this->getValue($weekday) === 'y') {
+ $valid = true;
+
+ break;
+ }
+ }
+
+ if (! $valid) {
+ $validator->addMessage($this->translate('You must select at least one of these weekdays'));
+ }
+
+ return $valid;
+ })
+ );
+ }
+}