1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LeKoala\FormElements; |
4
|
|
|
|
5
|
|
|
use SilverStripe\i18n\i18n; |
6
|
|
|
use SilverStripe\Forms\TextField; |
7
|
|
|
use SilverStripe\View\Requirements; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* @link https://chmln.github.io/flatpickr |
11
|
|
|
*/ |
12
|
|
|
class FlatpickrField extends TextField implements LocalizableField |
13
|
|
|
{ |
14
|
|
|
use BaseElement; |
15
|
|
|
use Localize; |
16
|
|
|
use HasDateTimeFormat; |
17
|
|
|
|
18
|
|
|
// Formats |
19
|
|
|
const DEFAULT_DATE_FORMAT = 'Y-m-d'; |
20
|
|
|
const DEFAULT_TIME_FORMAT = 'H:i'; |
21
|
|
|
const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i'; |
22
|
|
|
const DEFAULT_ALT_DATE_FORMAT = 'l j F Y'; |
23
|
|
|
const DEFAULT_ALT_TIME_FORMAT = 'H:i'; |
24
|
|
|
const DEFAULT_ALT_DATETIME_FORMAT = 'l j F Y H:i'; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Override locale. If empty will default to current locale |
28
|
|
|
* |
29
|
|
|
* @var string |
30
|
|
|
*/ |
31
|
|
|
protected $locale = null; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Disable description |
35
|
|
|
* |
36
|
|
|
* @var boolean |
37
|
|
|
*/ |
38
|
|
|
protected $disableDescription = false; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Array of plugins |
42
|
|
|
* |
43
|
|
|
* @var array |
44
|
|
|
*/ |
45
|
|
|
protected $plugins = []; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
protected $hooks = []; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var string |
54
|
|
|
*/ |
55
|
|
|
protected $theme; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @config |
59
|
|
|
* @var boolean |
60
|
|
|
*/ |
61
|
|
|
private static $enable_requirements = true; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @config |
65
|
|
|
* @link https://flatpickr.js.org/options/ |
66
|
|
|
* @var array |
67
|
|
|
*/ |
68
|
|
|
private static $default_config = [ |
69
|
|
|
'defaultDate' => '', |
70
|
|
|
'time_24hr' => true, |
71
|
|
|
]; |
72
|
|
|
|
73
|
|
|
public function __construct($name, $title = null, $value = '', $maxLength = null, $form = null) |
74
|
|
|
{ |
75
|
|
|
parent::__construct($name, $title, $value, $maxLength, $form); |
76
|
|
|
|
77
|
|
|
$this->config = self::config()->default_config; |
78
|
|
|
$this->setDatetimeFormat($this->convertDatetimeFormat(self::DEFAULT_ALT_DATE_FORMAT)); |
79
|
|
|
$this->setAltFormat(self::DEFAULT_ALT_DATE_FORMAT); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Get the value of theme |
84
|
|
|
* |
85
|
|
|
* @return string |
86
|
|
|
*/ |
87
|
|
|
public function getTheme() |
88
|
|
|
{ |
89
|
|
|
return $this->theme; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Set the value of theme |
94
|
|
|
* |
95
|
|
|
* @param string $theme |
96
|
|
|
* |
97
|
|
|
* @return $this |
98
|
|
|
*/ |
99
|
|
|
public function setTheme($theme) |
100
|
|
|
{ |
101
|
|
|
$this->theme = $theme; |
102
|
|
|
return $this; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Convert a datetime format from Flatpickr to CLDR |
107
|
|
|
* |
108
|
|
|
* This allow to display the right format in php |
109
|
|
|
* |
110
|
|
|
* @see https://flatpickr.js.org/formatting/ |
111
|
|
|
* @param string $format |
112
|
|
|
* @return string |
113
|
|
|
*/ |
114
|
|
|
protected function convertDatetimeFormat($format) |
115
|
|
|
{ |
116
|
|
|
return str_replace( |
117
|
|
|
['F', 'l', 'j', 'd', 'H', 'i', 's'], |
118
|
|
|
['MMMM', 'cccc', 'd', 'dd', 'HH', 'mm', 'ss'], |
119
|
|
|
$format |
120
|
|
|
); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
public function Type() |
124
|
|
|
{ |
125
|
|
|
return 'flatpickr'; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
public function extraClass() |
129
|
|
|
{ |
130
|
|
|
return 'text ' . parent::extraClass(); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
public function getEnableTime() |
134
|
|
|
{ |
135
|
|
|
return $this->getConfig('enableTime'); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
public function setEnableTime($value) |
139
|
|
|
{ |
140
|
|
|
$this->setDatetimeFormat($this->convertDatetimeFormat(self::DEFAULT_ALT_DATETIME_FORMAT)); |
141
|
|
|
$this->setAltFormat(self::DEFAULT_ALT_DATETIME_FORMAT); |
142
|
|
|
$this->setConfirmDate(true); |
143
|
|
|
return $this->setConfig('enableTime', $value); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
public function getNoCalendar() |
147
|
|
|
{ |
148
|
|
|
return $this->getConfig('noCalendar'); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
public function setNoCalendar($value) |
152
|
|
|
{ |
153
|
|
|
$this->setDatetimeFormat($this->convertDatetimeFormat(self::DEFAULT_ALT_TIME_FORMAT)); |
154
|
|
|
$this->setAltFormat(self::DEFAULT_ALT_TIME_FORMAT); |
155
|
|
|
return $this->setConfig('noCalendar', $value); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Show the user a readable date (as per altFormat), but return something totally different to the server. |
160
|
|
|
* |
161
|
|
|
* @return string |
162
|
|
|
*/ |
163
|
|
|
public function getAltInput() |
164
|
|
|
{ |
165
|
|
|
return $this->getConfig('altInput'); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
public function setAltInput($value) |
169
|
|
|
{ |
170
|
|
|
return $this->setConfig('altInput', $value); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Exactly the same as date format, but for the altInput field |
175
|
|
|
* |
176
|
|
|
* @return string |
177
|
|
|
*/ |
178
|
|
|
public function getAltFormat() |
179
|
|
|
{ |
180
|
|
|
return $this->getConfig('altFormat'); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Please note that altFormat should match the format for the database |
185
|
|
|
* |
186
|
|
|
* @param string $value |
187
|
|
|
* @return $this |
188
|
|
|
*/ |
189
|
|
|
public function setAltFormat($value) |
190
|
|
|
{ |
191
|
|
|
return $this->setConfig('altFormat', $value); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
public function getMinDate() |
195
|
|
|
{ |
196
|
|
|
return $this->getConfig('minDate'); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
public function setMinDate($value) |
200
|
|
|
{ |
201
|
|
|
return $this->setConfig('minDate', $value); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
public function getMaxDate() |
205
|
|
|
{ |
206
|
|
|
return $this->getConfig('maxDate'); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
public function setMaxDate($value) |
210
|
|
|
{ |
211
|
|
|
return $this->setConfig('maxDate', $value); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
public function getInline() |
215
|
|
|
{ |
216
|
|
|
return $this->getConfig('inline'); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
public function setInline($value) |
220
|
|
|
{ |
221
|
|
|
return $this->setConfig('inline', (bool)$value); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
public function getDefaultDate() |
225
|
|
|
{ |
226
|
|
|
return $this->getConfig('defaultDate'); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
public function setDefaultDate($value) |
230
|
|
|
{ |
231
|
|
|
return $this->setConfig('defaultDate', $value); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
public function getDateFormat() |
235
|
|
|
{ |
236
|
|
|
return $this->getConfig('dateFormat'); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
public function setDateFormat($value) |
240
|
|
|
{ |
241
|
|
|
return $this->setConfig('dateFormat', $value); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
public function getDisabledDates() |
245
|
|
|
{ |
246
|
|
|
return $this->getConfig('disable'); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Accepts: |
251
|
|
|
* - an array of values: ["2025-01-30", "2025-02-21", "2025-03-08"] |
252
|
|
|
* - an array of ranges: [["from" => "2025-01-30", "to" => "2025-02-10]] |
253
|
|
|
* Js functions are not supported at this time |
254
|
|
|
* |
255
|
|
|
* @param array $value |
256
|
|
|
* @return $this |
257
|
|
|
*/ |
258
|
|
|
public function setDisabledDates($value) |
259
|
|
|
{ |
260
|
|
|
return $this->setConfig('disable', $value); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
public function getEnabledDates() |
264
|
|
|
{ |
265
|
|
|
return $this->getConfig('enable'); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Accepts: |
270
|
|
|
* - an array of values: ["2025-01-30", "2025-02-21", "2025-03-08"] |
271
|
|
|
* - an array of ranges: [["from" => "2025-01-30", "to" => "2025-02-10]] |
272
|
|
|
* Js functions are not supported at this time |
273
|
|
|
* |
274
|
|
|
* @param array $value |
275
|
|
|
* @return $this |
276
|
|
|
*/ |
277
|
|
|
public function setEnabledDates($value) |
278
|
|
|
{ |
279
|
|
|
return $this->setConfig('enable', $value); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Get id of the second element |
284
|
|
|
* |
285
|
|
|
* @return string |
286
|
|
|
*/ |
287
|
|
|
public function getRange() |
288
|
|
|
{ |
289
|
|
|
return $this->getElementAttribute('data-range'); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Set id of the second element |
294
|
|
|
* |
295
|
|
|
* eg: #Form_ItemEditForm_EndDate |
296
|
|
|
* |
297
|
|
|
* @param string $range Id of the second element |
298
|
|
|
* @param bool $confirm |
299
|
|
|
* @return $this |
300
|
|
|
*/ |
301
|
|
|
public function setRange($range, $confirm = true) |
302
|
|
|
{ |
303
|
|
|
$this->setElementAttribute('data-range', $range); |
304
|
|
|
if ($confirm) { |
305
|
|
|
$this->setConfirmDate(true); |
306
|
|
|
} |
307
|
|
|
return $this; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Get add confirm box |
312
|
|
|
* |
313
|
|
|
* @return bool |
314
|
|
|
*/ |
315
|
|
|
public function getConfirmDate() |
316
|
|
|
{ |
317
|
|
|
return $this->getElementAttribute('data-confirm-date'); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Set add confirm box |
322
|
|
|
* |
323
|
|
|
* @param bool $confirmDate Add confirm box |
324
|
|
|
* |
325
|
|
|
* @return $this |
326
|
|
|
*/ |
327
|
|
|
public function setConfirmDate($confirmDate) |
328
|
|
|
{ |
329
|
|
|
return $this->setElementAttribute('data-confirm-date', $confirmDate); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @return bool |
334
|
|
|
*/ |
335
|
|
|
public function getMonthSelect() |
336
|
|
|
{ |
337
|
|
|
return $this->getElementAttribute('data-month-select'); |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* @param bool $monthSelect |
342
|
|
|
* |
343
|
|
|
* @return $this |
344
|
|
|
*/ |
345
|
|
|
public function setMonthSelect($monthSelect) |
346
|
|
|
{ |
347
|
|
|
return $this->setElementAttribute('data-month-select', $monthSelect); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* @param string $hook |
352
|
|
|
* @return string |
353
|
|
|
*/ |
354
|
|
|
public function getHook($hook) |
355
|
|
|
{ |
356
|
|
|
return $this->hooks[$hook] ?? ''; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* @param string $hook |
361
|
|
|
* @param string $callbackName |
362
|
|
|
* @return $this |
363
|
|
|
*/ |
364
|
|
|
public function setHook($hook, $callbackName) |
365
|
|
|
{ |
366
|
|
|
$this->hooks[$hook] = $callbackName; |
367
|
|
|
return $this; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
public function setDescription($description) |
371
|
|
|
{ |
372
|
|
|
// Allows blocking scaffolded UI desc that has no uses |
373
|
|
|
if ($this->disableDescription) { |
374
|
|
|
return $this; |
375
|
|
|
} |
376
|
|
|
return parent::setDescription($description); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
public function Field($properties = array()) |
380
|
|
|
{ |
381
|
|
|
// Set lang based on locale |
382
|
|
|
$lang = substr($this->getLocale(), 0, 2); |
383
|
|
|
if ($lang != 'en') { |
384
|
|
|
$this->setConfig('locale', $lang); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
if ($this->hooks) { |
|
|
|
|
388
|
|
|
// Use replace callback format |
389
|
|
|
foreach ($this->hooks as $k => $v) { |
390
|
|
|
$this->setConfig($k, [ |
391
|
|
|
"__fn" => $v |
392
|
|
|
]); |
393
|
|
|
} |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
if (self::config()->enable_requirements) { |
397
|
|
|
self::requirements($lang); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
if ($this->readonly) { |
401
|
|
|
if ($this->getNoCalendar() && $this->getEnableTime()) { |
402
|
|
|
$this->setAttribute('placeholder', _t('FlatpickrField.NO_TIME_SELECTED', 'No time')); |
403
|
|
|
} else { |
404
|
|
|
$this->setAttribute('placeholder', _t('FlatpickrField.NO_DATE_SELECTED', 'No date')); |
405
|
|
|
} |
406
|
|
|
} else { |
407
|
|
|
$this->setAttribute('placeholder', _t('FlatpickrField.SELECT_A_DATE', 'Select a date...')); |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
// Time formatting can cause value change for no reasons |
411
|
|
|
$this->addExtraClass('no-change-track'); |
412
|
|
|
|
413
|
|
|
return $this->wrapInElement('flatpickr-input', $properties); |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* Add requirements |
418
|
|
|
* |
419
|
|
|
* @param string $lang |
420
|
|
|
* @return void |
421
|
|
|
*/ |
422
|
|
|
public static function requirements($lang = null) |
423
|
|
|
{ |
424
|
|
|
if ($lang === null) { |
425
|
|
|
$lang = substr(i18n::get_locale(), 0, 2); |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
// We still need a copy of the cdn js files to load l10n |
429
|
|
|
$langResource = self::moduleResource("client/cdn/flatpickr/l10n/fr.js"); |
430
|
|
|
Requirements::javascript("lekoala/silverstripe-form-elements: client/custom-elements/flatpickr-input.min.js"); |
431
|
|
|
|
432
|
|
|
// Load lang (leverage waitDefined from custom element) |
433
|
|
|
if ($lang != 'en') { |
434
|
|
|
$basePath = dirname($langResource->getPath()); |
435
|
|
|
if (!is_file("$basePath/$lang.js")) { |
436
|
|
|
$lang = 'en'; // revert to en |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
if ($lang != 'en') { |
440
|
|
|
//eg: https://cdn.jsdelivr.net/npm/flatpickr@4/dist/l10n/fr.js |
441
|
|
|
Requirements::javascript("lekoala/silverstripe-form-elements: client/cdn/flatpickr/l10n/$lang.js"); |
442
|
|
|
} |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Get disable description |
447
|
|
|
* |
448
|
|
|
* @return boolean |
449
|
|
|
*/ |
450
|
|
|
public function getDisableDescription() |
451
|
|
|
{ |
452
|
|
|
return $this->disableDescription; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* Set disable description |
457
|
|
|
* |
458
|
|
|
* @param boolean $disableDescription |
459
|
|
|
* |
460
|
|
|
* @return $this |
461
|
|
|
*/ |
462
|
|
|
public function setDisableDescription($disableDescription) |
463
|
|
|
{ |
464
|
|
|
$this->disableDescription = $disableDescription; |
465
|
|
|
return $this; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
public function setReadonly($readonly) |
469
|
|
|
{ |
470
|
|
|
$this->setConfig('clickOpens', !$readonly); |
471
|
|
|
$this->setConfig('allowInput', !$readonly); |
472
|
|
|
return parent::setReadonly($readonly); |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
/** |
476
|
|
|
* Returns a read-only version of this field. |
477
|
|
|
* |
478
|
|
|
* @return FormField |
|
|
|
|
479
|
|
|
*/ |
480
|
|
|
public function performReadonlyTransformation() |
481
|
|
|
{ |
482
|
|
|
$clone = $this->castedCopy(self::class); |
483
|
|
|
$clone->replaceConfig($this->config); |
484
|
|
|
$clone->setReadonly(true); |
485
|
|
|
return $clone; |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* Set typical options for a DateTime field |
490
|
|
|
* @return $this |
491
|
|
|
*/ |
492
|
|
|
public function setDateTimeOptions() |
493
|
|
|
{ |
494
|
|
|
$this->setEnableTime(true); |
495
|
|
|
$this->setDisableDescription(true); |
496
|
|
|
return $this; |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
/** |
500
|
|
|
* Set typical options for a Time field |
501
|
|
|
* @return $this |
502
|
|
|
*/ |
503
|
|
|
public function setTimeOptions() |
504
|
|
|
{ |
505
|
|
|
$this->setEnableTime(true); |
506
|
|
|
$this->setNoCalendar(true); |
507
|
|
|
return $this; |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.