diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php index 45f9f3fa01..7c70089836 100644 --- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php @@ -1,106 +1,123 @@ getUser(); $username = $user->getUsername(); $pref_time = PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT; + $pref_date = PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT; $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $preferences = $user->loadPreferences(); $errors = array(); if ($request->isFormPost()) { $new_timezone = $request->getStr('timezone'); if (in_array($new_timezone, DateTimeZone::listIdentifiers(), true)) { $user->setTimezoneIdentifier($new_timezone); } else { $errors[] = pht('The selected timezone is not a valid timezone.'); } - $preferences->setPreference( - $pref_time, - $request->getStr($pref_time)); - $preferences->setPreference( - $pref_week_start, - $request->getStr($pref_week_start)); + $preferences + ->setPreference( + $pref_time, + $request->getStr($pref_time)) + ->setPreference( + $pref_date, + $request->getStr($pref_date)) + ->setPreference( + $pref_week_start, + $request->getStr($pref_week_start)); if (!$errors) { $preferences->save(); $user->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } } $timezone_ids = DateTimeZone::listIdentifiers(); $timezone_id_map = array_fuse($timezone_ids); $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Timezone')) ->setName('timezone') ->setOptions($timezone_id_map) ->setValue($user->getTimezoneIdentifier())) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Time-of-Day Format')) ->setName($pref_time) ->setOptions(array( 'g:i A' => pht('12-hour (2:34 PM)'), 'H:i' => pht('24-hour (14:34)'), )) ->setCaption( pht('Format used when rendering a time of day.')) ->setValue($preferences->getPreference($pref_time))) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Date Format')) + ->setName($pref_date) + ->setOptions(array( + 'Y-m-d' => pht('ISO 8601 (2000-02-28)'), + 'n/j/Y' => pht('US (2/28/2000)'), + 'd-m-Y' => pht('European (28-02-2000)'), + )) + ->setCaption( + pht('Format used when rendering a date.')) + ->setValue($preferences->getPreference($pref_date))) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Week Starts On')) ->setOptions($this->getWeekDays()) ->setName($pref_week_start) ->setCaption( pht('Calendar weeks will start with this day.')) ->setValue($preferences->getPreference($pref_week_start, 0))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Account Settings'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Date and Time Settings')) ->setFormSaved($request->getStr('saved')) ->setFormErrors($errors) ->setForm($form); return array( $form_box, ); } private function getWeekDays() { return array( pht('Sunday'), pht('Monday'), pht('Tuesday'), pht('Wednesday'), pht('Thursday'), pht('Friday'), pht('Saturday'), ); } } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index d54cf6cb46..cb04f59392 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -1,112 +1,113 @@ array( 'preferences' => self::SERIALIZATION_JSON, ), self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'userPHID' => array( 'columns' => array('userPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPreference($key, $default = null) { return idx($this->preferences, $key, $default); } public function setPreference($key, $value) { $this->preferences[$key] = $value; return $this; } public function unsetPreference($key) { unset($this->preferences[$key]); return $this; } public function getPinnedApplications(array $apps, PhabricatorUser $viewer) { $pref_pinned = self::PREFERENCE_APP_PINNED; $pinned = $this->getPreference($pref_pinned); if ($pinned) { return $pinned; } $pref_tiles = self::PREFERENCE_APP_TILES; $tiles = $this->getPreference($pref_tiles, array()); $full_tile = 'full'; $large = array(); foreach ($apps as $app) { $show = $app->isPinnedByDefault($viewer); // TODO: This is legacy stuff, clean it up eventually. This approximately // retains the old "tiles" preference. if (isset($tiles[get_class($app)])) { $show = ($tiles[get_class($app)] == $full_tile); } if ($show) { $large[] = get_class($app); } } return $large; } public static function filterMonospacedCSSRule($monospaced) { // Prevent the user from doing dangerous things. return preg_replace('/[^a-z0-9 ,".]+/i', '', $monospaced); } } diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php index 6f826cf830..4bf7619875 100644 --- a/src/view/form/control/AphrontFormDateControl.php +++ b/src/view/form/control/AphrontFormDateControl.php @@ -1,339 +1,355 @@ allowNull = $allow_null; return $this; } public function setIsTimeDisabled($is_disabled) { $this->isTimeDisabled = $is_disabled; return $this; } public function setIsDisabled($is_datepicker_disabled) { $this->isDisabled = $is_datepicker_disabled; return $this; } public function setEndDateID($value) { $this->endDateID = $value; return $this; } const TIME_START_OF_DAY = 'start-of-day'; const TIME_END_OF_DAY = 'end-of-day'; const TIME_START_OF_BUSINESS = 'start-of-business'; const TIME_END_OF_BUSINESS = 'end-of-business'; public function setInitialTime($time) { $this->initialTime = $time; return $this; } public function readValueFromRequest(AphrontRequest $request) { $date = $request->getStr($this->getDateInputName()); $time = $request->getStr($this->getTimeInputName()); $enabled = $request->getBool($this->getCheckboxInputName()); if ($this->allowNull && !$enabled) { $this->setError(null); $this->setValue(null); return; } $err = $this->getError(); if ($date || $time) { $this->valueDate = $date; $this->valueTime = $time; // Assume invalid. $err = pht('Invalid'); $zone = $this->getTimezone(); try { $datetime = new DateTime("{$date} {$time}", $zone); $value = $datetime->format('U'); } catch (Exception $ex) { $value = null; } if ($value) { $this->setValue($value); $err = null; } else { $this->setValue(null); } } else { $value = $this->getInitialValue(); if ($value) { $this->setValue($value); } else { $this->setValue(null); } } $this->setError($err); return $this->getValue(); } protected function getCustomControlClass() { return 'aphront-form-control-date'; } public function setValue($epoch) { if ($epoch instanceof AphrontFormDateControlValue) { $this->continueOnInvalidDate = true; $this->valueDate = $epoch->getValueDate(); $this->valueTime = $epoch->getValueTime(); $this->allowNull = $epoch->getOptional(); $this->isDisabled = $epoch->isDisabled(); return parent::setValue($epoch->getEpoch()); } $result = parent::setValue($epoch); if ($epoch === null) { return $result; } $readable = $this->formatTime($epoch, 'Y!m!d!g:i A'); $readable = explode('!', $readable, 4); $year = $readable[0]; $month = $readable[1]; $day = $readable[2]; $this->valueDate = $month.'/'.$day.'/'.$year; $this->valueTime = $readable[3]; return $result; } private function getDateInputValue() { - return $this->valueDate; + $date_format = $this->getDateFormat(); + $timezone = $this->getTimezone(); + + $datetime = new DateTime($this->valueDate, $timezone); + $date = $datetime->format($date_format); + + return $date; + } + + private function getDateFormat() { + $viewer = $this->getUser(); + $preferences = $viewer->loadPreferences(); + $pref_date_format = PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT; + + return $preferences->getPreference($pref_date_format, 'Y-m-d'); } private function getTimeInputValue() { return $this->valueTime; } private function formatTime($epoch, $fmt) { return phabricator_format_local_time( $epoch, $this->user, $fmt); } private function getDateInputName() { return $this->getName().'_d'; } private function getTimeInputName() { return $this->getName().'_t'; } private function getCheckboxInputName() { return $this->getName().'_e'; } protected function renderInput() { $disabled = null; if ($this->getValue() === null && !$this->continueOnInvalidDate) { $this->setValue($this->getInitialValue()); if ($this->allowNull) { $disabled = 'disabled'; } } if ($this->isDisabled) { $disabled = 'disabled'; } $checkbox = null; if ($this->allowNull) { $checkbox = javelin_tag( 'input', array( 'type' => 'checkbox', 'name' => $this->getCheckboxInputName(), 'sigil' => 'calendar-enable', 'class' => 'aphront-form-date-enabled-input', 'value' => 1, 'checked' => ($disabled === null ? 'checked' : null), )); } $date_sel = javelin_tag( 'input', array( 'autocomplete' => 'off', 'name' => $this->getDateInputName(), 'sigil' => 'date-input', 'value' => $this->getDateInputValue(), 'type' => 'text', 'class' => 'aphront-form-date-input', ), ''); $date_div = javelin_tag( 'div', array( 'class' => 'aphront-form-date-input-container', ), $date_sel); $cicon = id(new PHUIIconView()) ->setIconFont('fa-calendar'); $cal_icon = javelin_tag( 'a', array( 'href' => '#', 'class' => 'calendar-button', 'sigil' => 'calendar-button', ), $cicon); $values = $this->getTimeTypeaheadValues(); $time_id = celerity_generate_unique_node_id(); Javelin::initBehavior('time-typeahead', array( 'startTimeID' => $time_id, 'endTimeID' => $this->endDateID, 'timeValues' => $values, )); $time_sel = javelin_tag( 'input', array( 'autocomplete' => 'off', 'name' => $this->getTimeInputName(), 'sigil' => 'time-input', 'value' => $this->getTimeInputValue(), 'type' => 'text', 'class' => 'aphront-form-time-input', ), ''); $time_div = javelin_tag( 'div', array( 'id' => $time_id, 'class' => 'aphront-form-time-input-container', ), $time_sel); - Javelin::initBehavior('fancy-datepicker', array()); + Javelin::initBehavior('fancy-datepicker', array( + 'format' => $this->getDateFormat(), + )); $classes = array(); $classes[] = 'aphront-form-date-container'; if ($disabled) { $classes[] = 'datepicker-disabled'; } if ($this->isTimeDisabled) { $classes[] = 'no-time'; } return javelin_tag( 'div', array( 'class' => implode(' ', $classes), 'sigil' => 'phabricator-date-control', 'meta' => array( 'disabled' => (bool)$disabled, ), 'id' => $this->getID(), ), array( $checkbox, $date_div, $cal_icon, $time_div, )); } private function getTimezone() { if ($this->zone) { return $this->zone; } $user = $this->getUser(); if (!$this->getUser()) { throw new PhutilInvalidStateException('setUser'); } $user_zone = $user->getTimezoneIdentifier(); $this->zone = new DateTimeZone($user_zone); return $this->zone; } private function getInitialValue() { $zone = $this->getTimezone(); // TODO: We could eventually allow these to be customized per install or // per user or both, but let's wait and see. switch ($this->initialTime) { case self::TIME_START_OF_DAY: default: $time = '12:00 AM'; break; case self::TIME_START_OF_BUSINESS: $time = '9:00 AM'; break; case self::TIME_END_OF_BUSINESS: $time = '5:00 PM'; break; case self::TIME_END_OF_DAY: $time = '11:59 PM'; break; } $today = $this->formatTime(time(), 'Y-m-d'); try { $date = new DateTime("{$today} {$time}", $zone); $value = $date->format('U'); } catch (Exception $ex) { $value = null; } return $value; } private function getTimeTypeaheadValues() { $times = array(); $am_pm_list = array('AM', 'PM'); foreach ($am_pm_list as $am_pm) { for ($hour = 0; $hour < 12; $hour++) { $actual_hour = ($hour == 0) ? 12 : $hour; $times[] = $actual_hour.':00 '.$am_pm; $times[] = $actual_hour.':30 '.$am_pm; } } foreach ($times as $key => $time) { $times[$key] = array($key, $time); } return $times; } } diff --git a/src/view/form/control/AphrontFormDateControlValue.php b/src/view/form/control/AphrontFormDateControlValue.php index 43fb42664e..c084eecf2a 100644 --- a/src/view/form/control/AphrontFormDateControlValue.php +++ b/src/view/form/control/AphrontFormDateControlValue.php @@ -1,212 +1,266 @@ valueDate; } public function getValueTime() { return $this->valueTime; } public function isValid() { if ($this->isDisabled()) { return true; } return ($this->getEpoch() !== null); } public function isEmpty() { if ($this->valueDate) { return false; } if ($this->valueTime) { return false; } return true; } public function isDisabled() { return ($this->optional && !$this->valueEnabled); } public function setEnabled($enabled) { $this->valueEnabled = $enabled; return $this; } public function setOptional($optional) { $this->optional = $optional; return $this; } public function getOptional() { return $this->optional; } public static function newFromParts( PhabricatorUser $viewer, $year, $month, $day, $time = null, $enabled = true) { $value = new AphrontFormDateControlValue(); $value->viewer = $viewer; - $value->valueDate = $month.'/'.$day.'/'.$year; + $value->valueDate = $value->getFormattedDateFromParts( + $year, + $month, + $day, + $value); $value->valueTime = coalesce($time, '12:00 AM'); $value->valueEnabled = $enabled; return $value; } public static function newFromRequest(AphrontRequest $request, $key) { $value = new AphrontFormDateControlValue(); $value->viewer = $request->getViewer(); - $value->valueDate = $request->getStr($key.'_d'); + + $value->valueDate = $value->getFormattedDateFromDate( + $request->getStr($key.'_d'), + $value); + $value->valueTime = $request->getStr($key.'_t'); $value->valueEnabled = $request->getStr($key.'_e'); - return $value; } public static function newFromEpoch(PhabricatorUser $viewer, $epoch) { $value = new AphrontFormDateControlValue(); $value->viewer = $viewer; $readable = $value->formatTime($epoch, 'Y!m!d!g:i A'); $readable = explode('!', $readable, 4); $year = $readable[0]; $month = $readable[1]; $day = $readable[2]; - $value->valueDate = $month.'/'.$day.'/'.$year; + $value->valueDate = $value->getFormattedDateFromParts( + $year, + $month, + $day, + $value); $value->valueTime = $readable[3]; return $value; } public static function newFromDictionary( PhabricatorUser $viewer, array $dictionary) { $value = new AphrontFormDateControlValue(); $value->viewer = $viewer; - $value->valueDate = idx($dictionary, 'd'); + $value->valueDate = $value->getFormattedDateFromDate( + idx($dictionary, 'd'), + $value); + $value->valueTime = idx($dictionary, 't'); $value->valueEnabled = idx($dictionary, 'e'); return $value; } public static function newFromWild(PhabricatorUser $viewer, $wild) { if (is_array($wild)) { return self::newFromDictionary($viewer, $wild); } else if (is_numeric($wild)) { return self::newFromEpoch($viewer, $wild); } else { throw new Exception( pht( 'Unable to construct a date value from value of type "%s".', gettype($wild))); } } public function getDictionary() { return array( 'd' => $this->valueDate, 't' => $this->valueTime, 'e' => $this->valueEnabled, ); } public function getValueAsFormat($format) { return phabricator_format_local_time( $this->getEpoch(), $this->viewer, $format); } private function formatTime($epoch, $format) { return phabricator_format_local_time( $epoch, $this->viewer, $format); } public function getEpoch() { if ($this->isDisabled()) { return null; } $date = $this->valueDate; $time = $this->valueTime; $zone = $this->getTimezone(); if (!strlen($time)) { return null; } $colloquial = array( 'elevenses' => '11:00 AM', 'morning tea' => '11:00 AM', 'noon' => '12:00 PM', 'high noon' => '12:00 PM', 'lunch' => '12:00 PM', 'tea time' => '3:00 PM', 'witching hour' => '12:00 AM', 'midnight' => '12:00 AM', ); $normalized = phutil_utf8_strtolower($time); if (isset($colloquial[$normalized])) { $time = $colloquial[$normalized]; } try { $datetime = new DateTime("{$date} {$time}", $zone); $value = $datetime->format('U'); } catch (Exception $ex) { $value = null; } return $value; } + private function getDateFormat() { + $preferences = $this->viewer->loadPreferences(); + $pref_date_format = PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT; + + return $preferences->getPreference($pref_date_format, 'Y-m-d'); + } + + private function getFormattedDateFromDate($date, $value) { + $original_input = $date; + $zone = $value->getTimezone(); + $separator = $value->getFormatSeparator(); + $parts = preg_split('@[,./:-]@', $date); + $date = implode($separator, $parts); + $date = id(new DateTime($date, $zone)); + + if ($date) { + return $date->format($value->getDateFormat()); + } else { + return $original_input; + } + } + + private function getFormattedDateFromParts($year, $month, $day, $value) { + $zone = $value->getTimezone(); + + return id(new DateTime("{$year}-{$month}-{$day}", $zone)) + ->format($value->getDateFormat()); + } + + private function getFormatSeparator() { + $format = $this->getDateFormat(); + switch ($format) { + case 'n/j/Y': + return '/'; + default: + return '-'; + } + } + public function getDateTime() { $epoch = $this->getEpoch(); $date = null; if ($epoch) { $zone = $this->getTimezone(); $date = new DateTime('@'.$epoch); $date->setTimeZone($zone); } return $date; } private function getTimezone() { if ($this->zone) { return $this->zone; } $viewer_zone = $this->viewer->getTimezoneIdentifier(); $this->zone = new DateTimeZone($viewer_zone); return $this->zone; } } diff --git a/webroot/rsrc/js/core/behavior-fancy-datepicker.js b/webroot/rsrc/js/core/behavior-fancy-datepicker.js index d00f6cc77f..99feaa31f2 100644 --- a/webroot/rsrc/js/core/behavior-fancy-datepicker.js +++ b/webroot/rsrc/js/core/behavior-fancy-datepicker.js @@ -1,307 +1,395 @@ /** * @provides javelin-behavior-fancy-datepicker * @requires javelin-behavior * javelin-util * javelin-dom * javelin-stratcom * javelin-vector */ -JX.behavior('fancy-datepicker', function() { +JX.behavior('fancy-datepicker', function(config, statics) { + if (statics.initialized) { + return; + } + statics.initialized = true; var picker; var root; var value_y; var value_m; var value_d; + var get_format_separator = function() { + var format = get_format(); + switch (format.toLowerCase()) { + case 'n/j/y': + return '/'; + default: + return '-'; + } + }; + + var get_key_maps = function() { + var format = get_format(); + var regex = new RegExp('[./ -]'); + return format.split(regex); + }; + + var get_format = function() { + var format = config.format; + + if (format === null) { + format = 'Y-m-d'; + } + + return format; + }; + var onopen = function(e) { e.kill(); // If you click the calendar icon while the date picker is open, close it // without writing the change. if (picker) { if (root == e.getNode('phabricator-date-control')) { // If the user clicked the same control, just close it. onclose(e); return; } else { // If the user clicked a different control, close the old one but then // open the new one. onclose(e); } } root = e.getNode('phabricator-date-control'); picker = JX.$N( 'div', {className: 'fancy-datepicker', sigil: 'phabricator-datepicker'}, JX.$N('div', {className: 'fancy-datepicker-core'})); document.body.appendChild(picker); var button = e.getNode('calendar-button'); var p = JX.$V(button); var d = JX.Vector.getDim(picker); picker.style.left = (p.x - d.x - 2) + 'px'; picker.style.top = (p.y) + 'px'; JX.DOM.alterClass(root, 'picker-open', true); read_date(); render(); }; var onclose = function(e) { if (!picker) { return; } JX.DOM.remove(picker); picker = null; JX.DOM.alterClass(root, 'picker-open', false); if (e) { e.kill(); } root = null; }; var ontoggle = function(e) { var box = e.getTarget(); root = e.getNode('phabricator-date-control'); JX.Stratcom.getData(root).disabled = !box.checked; redraw_inputs(); }; var get_inputs = function() { return { d: JX.DOM.find(root, 'input', 'date-input'), t: JX.DOM.find(root, 'input', 'time-input') }; }; - var read_date = function() { - var i = get_inputs(); - var date = i.d.value; - var parts = date.split('/'); - value_y = +parts[2]; - value_m = +parts[0]; - value_d = +parts[1]; + var read_date = function(){ + var inputs = get_inputs(); + var date = inputs.d.value; + var regex = new RegExp('[./ -]'); + var date_parts = date.split(regex); + var map = get_key_maps(); + + for (var i=0; i < date_parts.length; i++) { + var key = map[i].toLowerCase(); + + switch (key) { + case 'y': + value_y = date_parts[i]; + break; + case 'm': + value_m = date_parts[i]; + break; + case 'd': + value_d = date_parts[i]; + break; + } + } }; var write_date = function() { - var i = get_inputs(); - i.d.value = value_m + '/' + value_d + '/' + value_y; + var inputs = get_inputs(); + var map = get_key_maps(); + var arr_values = []; + + for(var i=0; i < map.length; i++) { + switch (map[i].toLowerCase()) { + case 'y': + arr_values[i] = value_y; + break; + case 'm': + arr_values[i] = value_m; + break; + case 'n': + arr_values[i] = value_m; + break; + case 'd': + arr_values[i] = value_d; + break; + case 'j': + arr_values[i] = value_d; + break; + } + } + + var text_value = ''; + var separator = get_format_separator(); + + for(var j=0; j < arr_values.length; j++) { + var element = arr_values[j]; + var format = get_format(); + + if ((format.toLowerCase() === 'd-m-y' || + format.toLowerCase() === 'y-m-d') && + element < 10) { + element = '0' + element; + } + + if (text_value.length === 0) { + text_value += element; + } else { + text_value = text_value + separator + element; + } + } + + inputs.d.value = text_value; }; var render = function() { JX.DOM.setContent( picker.firstChild, [ render_month(), render_day() ]); }; var redraw_inputs = function() { var disabled = JX.Stratcom.getData(root).disabled; JX.DOM.alterClass(root, 'datepicker-disabled', disabled); var box = JX.DOM.scry(root, 'input', 'calendar-enable'); if (box.length) { box[0].checked = !disabled; } }; // Render a cell for the date picker. var cell = function(label, value, selected, class_name) { class_name = class_name || ''; if (selected) { class_name += ' datepicker-selected'; } if (!value) { class_name += ' novalue'; } return JX.$N('td', {meta: {value: value}, className: class_name}, label); }; // Render the top bar which allows you to pick a month and year. var render_month = function() { var valid_date = getValidDate(); var month = valid_date.getMonth(); var year = valid_date.getYear() + 1900; var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; var buttons = [ cell('\u25C0', 'm:-1', false, 'lrbutton'), cell(months[month] + ' ' + year, null), cell('\u25B6', 'm:1', false, 'lrbutton')]; return JX.$N( 'table', {className: 'month-table'}, JX.$N('tr', {}, buttons)); }; function getValidDate() { var written_date = new Date(value_y, value_m-1, value_d); if (isNaN(written_date.getTime())) { return new Date(); } else { //year 01 should be 2001, not 1901 if (written_date.getYear() < 70) { value_y += 2000; written_date = new Date(value_y, value_m-1, value_d); } return written_date; } } // Render the day-of-week and calendar views. var render_day = function() { var today = new Date(); var valid_date = getValidDate(); var weeks = []; // First, render the weekday names. var weekdays = 'SMTWTFS'; var weekday_names = []; var ii; for (ii = 0; ii < weekdays.length; ii++) { weekday_names.push(cell(weekdays.charAt(ii), null, false, 'day-name')); } weeks.push(JX.$N('tr', {}, weekday_names)); // Render the calendar itself. NOTE: Javascript uses 0-based month indexes // while we use 1-based month indexes, so we have to adjust for that. var days = []; var start = new Date( valid_date.getYear() + 1900, valid_date.getMonth(), 1).getDay(); while (start--) { days.push(cell('', null, false, 'day-placeholder')); } for (ii = 1; ii <= 31; ii++) { var date = new Date( valid_date.getYear() + 1900, valid_date.getMonth(), ii); if (date.getMonth() != (valid_date.getMonth())) { // We've spilled over into the next month, so stop rendering. break; } var is_today = (today.getYear() == date.getYear() && today.getMonth() == date.getMonth() && today.getDate() == date.getDate()); var classes = []; classes.push('day'); if (is_today) { classes.push('today'); } if (date.getDay() === 0 || date.getDay() == 6) { classes.push('weekend'); } days.push(cell( ii, 'd:'+ii, valid_date.getDate() == ii, classes.join(' '))); } // Slice the days into weeks. for (ii = 0; ii < days.length; ii += 7) { weeks.push(JX.$N('tr', {}, days.slice(ii, ii + 7))); } return JX.$N('table', {className: 'day-table'}, weeks); }; JX.Stratcom.listen('click', 'calendar-button', onopen); JX.Stratcom.listen('change', 'calendar-enable', ontoggle); JX.Stratcom.listen( 'click', ['phabricator-datepicker', 'tag:td'], function(e) { e.kill(); var data = e.getNodeData('tag:td'); if (!data.value) { return; } var valid_date = getValidDate(); value_y = valid_date.getYear() + 1900; value_m = valid_date.getMonth() + 1; value_d = valid_date.getDate(); var p = data.value.split(':'); switch (p[0]) { case 'm': // User clicked left or right month selection buttons. value_m = value_m + parseInt(p[1], 10); if (value_m > 12) { value_m -= 12; value_y++; } else if (value_m <= 0) { value_m += 12; value_y--; } break; case 'd': // User clicked a day. value_d = parseInt(p[1], 10); write_date(); // Wait a moment to close the selector so they can see the effect // of their action. setTimeout(JX.bind(null, onclose, e), 150); break; } // Enable the control. JX.Stratcom.getData(root).disabled = false; redraw_inputs(); render(); }); JX.Stratcom.listen('click', null, function(e){ if (e.getNode('phabricator-datepicker')) { return; } onclose(); }); });