@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class AphrontFormDateControl extends AphrontFormControl {
4
5 private $initialTime;
6 private $zone;
7
8 private $valueDate;
9 private $valueTime;
10 private $allowNull;
11 private $continueOnInvalidDate = false;
12 private $isTimeDisabled;
13 private $isDisabled;
14 private $endDateID;
15
16 public function setAllowNull($allow_null) {
17 $this->allowNull = $allow_null;
18 return $this;
19 }
20
21 public function setIsTimeDisabled($is_disabled) {
22 $this->isTimeDisabled = $is_disabled;
23 return $this;
24 }
25
26 public function setIsDisabled($is_datepicker_disabled) {
27 $this->isDisabled = $is_datepicker_disabled;
28 return $this;
29 }
30
31 public function setEndDateID($value) {
32 $this->endDateID = $value;
33 return $this;
34 }
35
36 const TIME_START_OF_DAY = 'start-of-day';
37 const TIME_END_OF_DAY = 'end-of-day';
38 const TIME_START_OF_BUSINESS = 'start-of-business';
39 const TIME_END_OF_BUSINESS = 'end-of-business';
40
41 public function setInitialTime($time) {
42 $this->initialTime = $time;
43 return $this;
44 }
45
46 public function readValueFromRequest(AphrontRequest $request) {
47 $date = $request->getStr($this->getDateInputName());
48 $time = $request->getStr($this->getTimeInputName());
49 $enabled = $request->getBool($this->getCheckboxInputName());
50
51 if ($this->allowNull && !$enabled) {
52 $this->setError(null);
53 $this->setValue(null);
54 return;
55 }
56
57 $err = $this->getError();
58
59 if ($date || $time) {
60 $this->valueDate = $date;
61 $this->valueTime = $time;
62
63 // Assume invalid.
64 $err = pht('Invalid');
65
66 $zone = $this->getTimezone();
67
68 try {
69 $datetime = new DateTime("{$date} {$time}", $zone);
70 $value = $datetime->format('U');
71 } catch (Exception $ex) {
72 $value = null;
73 }
74
75 if ($value) {
76 $this->setValue($value);
77 $err = null;
78 } else {
79 $this->setValue(null);
80 }
81 } else {
82 $value = $this->getInitialValue();
83 if ($value) {
84 $this->setValue($value);
85 } else {
86 $this->setValue(null);
87 }
88 }
89
90 $this->setError($err);
91
92 return $this->getValue();
93 }
94
95 protected function getCustomControlClass() {
96 return 'aphront-form-control-date';
97 }
98
99 public function setValue($epoch) {
100 if ($epoch instanceof AphrontFormDateControlValue) {
101 $this->continueOnInvalidDate = true;
102 $this->valueDate = $epoch->getValueDate();
103 $this->valueTime = $epoch->getValueTime();
104 $this->allowNull = $epoch->getOptional();
105 $this->isDisabled = $epoch->isDisabled();
106
107 return parent::setValue($epoch->getEpoch());
108 }
109
110 $result = parent::setValue($epoch);
111
112 if ($epoch === null) {
113 return $result;
114 }
115
116 $readable = $this->formatTime($epoch, 'Y!m!d!'.$this->getTimeFormat());
117 $readable = explode('!', $readable, 4);
118
119 $year = $readable[0];
120 $month = $readable[1];
121 $day = $readable[2];
122
123 $this->valueDate = $month.'/'.$day.'/'.$year;
124 $this->valueTime = $readable[3];
125
126 return $result;
127 }
128
129 private function getDateInputValue() {
130 $date_format = $this->getDateFormat();
131 $timezone = $this->getTimezone();
132
133 try {
134 $datetime = new DateTime($this->valueDate, $timezone);
135 } catch (Exception $ex) {
136 return $this->valueDate;
137 }
138
139 return $datetime->format($date_format);
140 }
141
142 private function getTimeFormat() {
143 $viewer = $this->getViewer();
144 $time_key = PhabricatorTimeFormatSetting::SETTINGKEY;
145 return $viewer->getUserSetting($time_key);
146 }
147
148 private function getDateFormat() {
149 $viewer = $this->getViewer();
150 $date_key = PhabricatorDateFormatSetting::SETTINGKEY;
151 return $viewer->getUserSetting($date_key);
152 }
153
154 private function getTimeInputValue() {
155 return $this->valueTime;
156 }
157
158 private function formatTime($epoch, $fmt) {
159 return phabricator_format_local_time(
160 $epoch,
161 $this->getViewer(),
162 $fmt);
163 }
164
165 private function getDateInputName() {
166 return $this->getName().'_d';
167 }
168
169 private function getTimeInputName() {
170 return $this->getName().'_t';
171 }
172
173 private function getCheckboxInputName() {
174 return $this->getName().'_e';
175 }
176
177 protected function renderInput() {
178
179 $disabled = null;
180 if ($this->getValue() === null && !$this->continueOnInvalidDate) {
181 $this->setValue($this->getInitialValue());
182 if ($this->allowNull) {
183 $disabled = 'disabled';
184 }
185 }
186
187 if ($this->isDisabled) {
188 $disabled = 'disabled';
189 }
190
191 $checkbox = null;
192 if ($this->allowNull) {
193 $checkbox = javelin_tag(
194 'input',
195 array(
196 'type' => 'checkbox',
197 'name' => $this->getCheckboxInputName(),
198 'sigil' => 'calendar-enable',
199 'class' => 'aphront-form-date-enabled-input',
200 'value' => 1,
201 'checked' => ($disabled === null ? 'checked' : null),
202 ));
203 }
204
205 $date_sel = javelin_tag(
206 'input',
207 array(
208 'autocomplete' => 'off',
209 'name' => $this->getDateInputName(),
210 'sigil' => 'date-input',
211 'value' => $this->getDateInputValue(),
212 'type' => 'text',
213 'class' => 'aphront-form-date-input',
214 ),
215 '');
216
217 $date_div = javelin_tag(
218 'div',
219 array(
220 'class' => 'aphront-form-date-input-container',
221 ),
222 $date_sel);
223
224 $cicon = id(new PHUIIconView())
225 ->setIcon('fa-calendar');
226
227 $cal_icon = javelin_tag(
228 'a',
229 array(
230 'href' => '#',
231 'class' => 'calendar-button',
232 'sigil' => 'calendar-button',
233 'aria-label' => pht('Calendar date picker'),
234 ),
235 $cicon);
236
237 $values = $this->getTimeTypeaheadValues();
238
239 $time_id = celerity_generate_unique_node_id();
240 Javelin::initBehavior('time-typeahead', array(
241 'startTimeID' => $time_id,
242 'endTimeID' => $this->endDateID,
243 'timeValues' => $values,
244 'format' => $this->getTimeFormat(),
245 ));
246
247 $time_sel = javelin_tag(
248 'input',
249 array(
250 'autocomplete' => 'off',
251 'name' => $this->getTimeInputName(),
252 'sigil' => 'time-input',
253 'value' => $this->getTimeInputValue(),
254 'type' => 'text',
255 'class' => 'aphront-form-time-input',
256 ),
257 '');
258
259 $time_div = javelin_tag(
260 'div',
261 array(
262 'id' => $time_id,
263 'class' => 'aphront-form-time-input-container',
264 ),
265 $time_sel);
266
267 $viewer = $this->getViewer();
268 $week_key = PhabricatorWeekStartDaySetting::SETTINGKEY;
269 $week_start = $viewer->getUserSetting($week_key);
270
271 $date_pht = array(
272 'S|M|T|W|T|F|S' => pht('S|M|T|W|T|F|S'),
273 'January' => pht('January'),
274 'February' => pht('February'),
275 'March' => pht('March'),
276 'April' => pht('April'),
277 'May' => pht('May'),
278 'June' => pht('June'),
279 'July' => pht('July'),
280 'August' => pht('August'),
281 'September' => pht('September'),
282 'October' => pht('October'),
283 'November' => pht('November'),
284 'December' => pht('December'),
285 );
286 Javelin::initBehavior('fancy-datepicker', array(
287 'format' => $this->getDateFormat(),
288 'weekStart' => $week_start,
289 'pht' => $date_pht,
290 ));
291
292 $classes = array();
293 $classes[] = 'aphront-form-date-container';
294 if ($disabled) {
295 $classes[] = 'datepicker-disabled';
296 }
297 if ($this->isTimeDisabled) {
298 $classes[] = 'no-time';
299 }
300
301 return javelin_tag(
302 'div',
303 array(
304 'class' => implode(' ', $classes),
305 'sigil' => 'phabricator-date-control',
306 'meta' => array(
307 'disabled' => (bool)$disabled,
308 ),
309 'id' => $this->getID(),
310 ),
311 array(
312 $checkbox,
313 $date_div,
314 $cal_icon,
315 $time_div,
316 ));
317 }
318
319 private function getTimezone() {
320 if ($this->zone) {
321 return $this->zone;
322 }
323
324 $viewer = $this->getViewer();
325
326 $user_zone = $viewer->getTimezoneIdentifier();
327 $this->zone = new DateTimeZone($user_zone);
328 return $this->zone;
329 }
330
331 private function getInitialValue() {
332 $zone = $this->getTimezone();
333
334 // TODO: We could eventually allow these to be customized per install or
335 // per user or both, but let's wait and see.
336 switch ($this->initialTime) {
337 case self::TIME_START_OF_DAY:
338 default:
339 $time = '12:00 AM';
340 break;
341 case self::TIME_START_OF_BUSINESS:
342 $time = '9:00 AM';
343 break;
344 case self::TIME_END_OF_BUSINESS:
345 $time = '5:00 PM';
346 break;
347 case self::TIME_END_OF_DAY:
348 $time = '11:59 PM';
349 break;
350 }
351
352 $today = $this->formatTime(time(), 'Y-m-d');
353 try {
354 $date = new DateTime("{$today} {$time}", $zone);
355 $value = $date->format('U');
356 } catch (Exception $ex) {
357 $value = null;
358 }
359
360 return $value;
361 }
362
363 private function getTimeTypeaheadValues() {
364 $time_format = $this->getTimeFormat();
365 $times = array();
366
367 if ($time_format == 'g:i A') {
368 $am_pm_list = array('AM', 'PM');
369
370 foreach ($am_pm_list as $am_pm) {
371 for ($hour = 0; $hour < 12; $hour++) {
372 $actual_hour = ($hour == 0) ? 12 : $hour;
373 $times[] = $actual_hour.':00 '.$am_pm;
374 $times[] = $actual_hour.':30 '.$am_pm;
375 }
376 }
377 } else if ($time_format == 'H:i') {
378 for ($hour = 0; $hour < 24; $hour++) {
379 $written_hour = ($hour > 9) ? $hour : '0'.$hour;
380 $times[] = $written_hour.':00';
381 $times[] = $written_hour.':30';
382 }
383 }
384
385 foreach ($times as $key => $time) {
386 $times[$key] = array($key, $time);
387 }
388
389 return $times;
390 }
391
392}