diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php index d829a2909..ca6d87b26 100644 --- a/src/view/form/control/AphrontFormDateControl.php +++ b/src/view/form/control/AphrontFormDateControl.php @@ -1,405 +1,334 @@ allowNull = $allow_null; return $this; } public function setIsTimeDisabled($is_disabled) { $this->isTimeDisabled = $is_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) { - $day = $request->getInt($this->getDayInputName()); - $month = $request->getInt($this->getMonthInputName()); - $year = $request->getInt($this->getYearInputName()); + $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 ($day || $month || $year || $time) { - $this->valueDay = $day; - $this->valueMonth = $month; - $this->valueYear = $year; + if ($date || $time) { + $this->valueDate = $date; $this->valueTime = $time; // Assume invalid. $err = 'Invalid'; $zone = $this->getTimezone(); try { - $date = new DateTime("{$year}-{$month}-{$day} {$time}", $zone); - $value = $date->format('U'); + $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->valueYear = $epoch->getValueYear(); - $this->valueMonth = $epoch->getValueMonth(); - $this->valueDay = $epoch->getValueDay(); + $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); - $this->valueYear = $readable[0]; - $this->valueMonth = $readable[1]; - $this->valueDay = $readable[2]; + $year = $readable[0]; + $month = $readable[1]; + $day = $readable[2]; + + $this->valueDate = $month.'/'.$day.'/'.$year; $this->valueTime = $readable[3]; return $result; } - private function getMinYear() { - $cur_year = $this->formatTime( - time(), - 'Y'); - $val_year = $this->getYearInputValue(); - - return min($cur_year, $val_year) - 3; - } - - private function getMaxYear() { - $cur_year = $this->formatTime( - time(), - 'Y'); - $val_year = $this->getYearInputValue(); - - return max($cur_year, $val_year) + 3; - } - - private function getDayInputValue() { - return $this->valueDay; - } - - private function getMonthInputValue() { - return $this->valueMonth; - } - - private function getYearInputValue() { - return $this->valueYear; + private function getDateInputValue() { + return $this->valueDate; } private function getTimeInputValue() { return $this->valueTime; } private function formatTime($epoch, $fmt) { return phabricator_format_local_time( $epoch, $this->user, $fmt); } - private function getDayInputName() { + private function getDateInputName() { return $this->getName().'_d'; } - private function getMonthInputName() { - return $this->getName().'_m'; - } - - private function getYearInputName() { - return $this->getName().'_y'; - } - 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'; } - $min_year = $this->getMinYear(); - $max_year = $this->getMaxYear(); - - $days = range(1, 31); - $days = array_fuse($days); - - $months = array( - 1 => pht('Jan'), - 2 => pht('Feb'), - 3 => pht('Mar'), - 4 => pht('Apr'), - 5 => pht('May'), - 6 => pht('Jun'), - 7 => pht('Jul'), - 8 => pht('Aug'), - 9 => pht('Sep'), - 10 => pht('Oct'), - 11 => pht('Nov'), - 12 => pht('Dec'), - ); - $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), )); } - $years = range($this->getMinYear(), $this->getMaxYear()); - $years = array_fuse($years); - - $days_sel = AphrontFormSelectControl::renderSelectTag( - $this->getDayInputValue(), - $days, - array( - 'name' => $this->getDayInputName(), - 'sigil' => 'day-input', - )); - - $months_sel = AphrontFormSelectControl::renderSelectTag( - $this->getMonthInputValue(), - $months, + $date_sel = javelin_tag( + 'input', array( - 'name' => $this->getMonthInputName(), - 'sigil' => 'month-input', - )); + 'autocomplete' => 'off', + 'name' => $this->getDateInputName(), + 'sigil' => 'date-input', + 'value' => $this->getDateInputValue(), + 'type' => 'text', + 'class' => 'aphront-form-date-time-input', + ), + ''); - $years_sel = AphrontFormSelectControl::renderSelectTag( - $this->getYearInputValue(), - $years, + $date_div = javelin_tag( + 'div', array( - 'name' => $this->getYearInputName(), - 'sigil' => 'year-input', - )); + 'class' => 'aphront-form-date-time-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-date-time-input', ), ''); $time_div = javelin_tag( 'div', array( 'id' => $time_id, 'class' => 'aphront-form-date-time-input-container', ), $time_sel); Javelin::initBehavior('fancy-datepicker', array()); $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, - $days_sel, - $months_sel, - $years_sel, + $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 28d52371a..43fb42664 100644 --- a/src/view/form/control/AphrontFormDateControlValue.php +++ b/src/view/form/control/AphrontFormDateControlValue.php @@ -1,238 +1,212 @@ valueDay; - } - - public function getValueMonth() { - return $this->valueMonth; - } - - public function getValueYear() { - return $this->valueYear; + public function getValueDate() { + return $this->valueDate; } public function getValueTime() { return $this->valueTime; } public function isValid() { if ($this->isDisabled()) { return true; } return ($this->getEpoch() !== null); } public function isEmpty() { - if ($this->valueDay) { - return false; - } - - if ($this->valueMonth) { - return false; - } - - if ($this->valueYear) { + 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->valueYear = $year; - $value->valueMonth = $month; - $value->valueDay = $day; + $value->valueDate = $month.'/'.$day.'/'.$year; $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->valueDay = $request->getInt($key.'_d'); - $value->valueMonth = $request->getInt($key.'_m'); - $value->valueYear = $request->getInt($key.'_y'); + $value->valueDate = $request->getStr($key.'_d'); $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); - $value->valueYear = $readable[0]; - $value->valueMonth = $readable[1]; - $value->valueDay = $readable[2]; + $year = $readable[0]; + $month = $readable[1]; + $day = $readable[2]; + + $value->valueDate = $month.'/'.$day.'/'.$year; $value->valueTime = $readable[3]; + return $value; } public static function newFromDictionary( PhabricatorUser $viewer, array $dictionary) { $value = new AphrontFormDateControlValue(); $value->viewer = $viewer; - $value->valueYear = idx($dictionary, 'y'); - $value->valueMonth = idx($dictionary, 'm'); - $value->valueDay = idx($dictionary, 'd'); + $value->valueDate = idx($dictionary, 'd'); $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( - 'y' => $this->valueYear, - 'm' => $this->valueMonth, - 'd' => $this->valueDay, + '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; } - $year = $this->valueYear; - $month = $this->valueMonth; - $day = $this->valueDay; + $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 { - $date = new DateTime("{$year}-{$month}-{$day} {$time}", $zone); - $value = $date->format('U'); + $datetime = new DateTime("{$date} {$time}", $zone); + $value = $datetime->format('U'); } catch (Exception $ex) { $value = null; } return $value; } 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/src/view/phui/calendar/PHUICalendarDayView.php b/src/view/phui/calendar/PHUICalendarDayView.php index 9bdab3fc4..1975d6610 100644 --- a/src/view/phui/calendar/PHUICalendarDayView.php +++ b/src/view/phui/calendar/PHUICalendarDayView.php @@ -1,484 +1,481 @@ events[] = $event; return $this; } public function setBrowseURI($browse_uri) { $this->browseURI = $browse_uri; return $this; } private function getBrowseURI() { return $this->browseURI; } public function setQuery($query) { $this->query = $query; return $this; } private function getQuery() { return $this->query; } public function __construct( $range_start, $range_end, $year, $month, $day = null) { $this->rangeStart = $range_start; $this->rangeEnd = $range_end; $this->day = $day; $this->month = $month; $this->year = $year; } public function render() { require_celerity_resource('phui-calendar-day-css'); $viewer = $this->getUser(); $hours = $this->getHoursOfDay(); $js_hours = array(); $js_today_events = array(); foreach ($hours as $hour) { $js_hours[] = array( 'hour' => $hour->format('G'), 'hour_meridian' => $hour->format('g A'), ); } $first_event_hour = null; $js_today_all_day_events = array(); $all_day_events = $this->getAllDayEvents(); $day_start = $this->getDateTime(); $day_end = id(clone $day_start)->modify('+1 day'); $day_start_epoch = $day_start->format('U'); $day_end_epoch = $day_end->format('U') - 1; foreach ($all_day_events as $all_day_event) { $all_day_start = $all_day_event->getEpochStart(); $all_day_end = $all_day_event->getEpochEnd(); if ($all_day_start < $day_end_epoch && $all_day_end > $day_start_epoch) { $js_today_all_day_events[] = array( 'name' => $all_day_event->getName(), 'id' => $all_day_event->getEventID(), 'viewerIsInvited' => $all_day_event->getViewerIsInvited(), 'uri' => $all_day_event->getURI(), ); } } $this->events = msort($this->events, 'getEpochStart'); - - if (!$this->events) { - $first_event_hour = $this->getDateTime()->setTime(8, 0, 0); - } + $first_event_hour = $this->getDateTime()->setTime(8, 0, 0); foreach ($this->events as $event) { if ($event->getIsAllDay()) { continue; } if ($event->getEpochStart() <= $day_end_epoch && $event->getEpochEnd() > $day_start_epoch) { if ($first_event_hour === null) { $first_event_hour = PhabricatorTime::getDateTimeFromEpoch( $event->getEpochStart(), $viewer); $midnight = $this->getDateTime()->setTime(0, 0, 0); if ($first_event_hour->format('U') < $midnight->format('U')) { $first_event_hour = clone $midnight; } $eight_am = $this->getDateTime()->setTime(8, 0, 0); if ($eight_am->format('U') < $first_event_hour->format('U')) { $first_event_hour = clone $eight_am; } } $event_start = max($event->getEpochStart(), $day_start_epoch); $event_end = min($event->getEpochEnd(), $day_end_epoch); $day_duration = ($day_end_epoch - $first_event_hour->format('U')) / 60; $top = (($event_start - $first_event_hour->format('U')) / ($day_end_epoch - $first_event_hour->format('U'))) * $day_duration; $top = max(0, $top); $height = (($event_end - $event_start) / ($day_end_epoch - $first_event_hour->format('U'))) * $day_duration; $height = min($day_duration, $height); $js_today_events[] = array( 'eventStartEpoch' => $event->getEpochStart(), 'eventEndEpoch' => $event->getEpochEnd(), 'eventName' => $event->getName(), 'eventID' => $event->getEventID(), 'viewerIsInvited' => $event->getViewerIsInvited(), 'uri' => $event->getURI(), 'offset' => '0', 'width' => '100%', 'top' => $top.'px', 'height' => $height.'px', 'canEdit' => $event->getCanEdit(), ); } } $header = $this->renderDayViewHeader(); $sidebar = $this->renderSidebar(); $warnings = $this->getQueryRangeWarning(); $table_id = celerity_generate_unique_node_id(); $table_wrapper = phutil_tag( 'div', array( 'id' => $table_id, ), ''); Javelin::initBehavior( 'day-view', array( 'year' => $first_event_hour->format('Y'), 'month' => $first_event_hour->format('m'), 'day' => $first_event_hour->format('d'), 'query' => $this->getQuery(), 'allDayEvents' => $js_today_all_day_events, 'todayEvents' => $js_today_events, 'hours' => $js_hours, 'firstEventHour' => $first_event_hour->format('G'), 'firstEventHourEpoch' => $first_event_hour->format('U'), 'tableID' => $table_id, )); $table_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($table_wrapper) ->setFormErrors($warnings) ->setFlush(true); $layout = id(new AphrontMultiColumnView()) ->addColumn($sidebar, 'third') ->addColumn($table_box, 'thirds phui-day-view-column') ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); return phutil_tag( 'div', array( 'class' => 'ml', ), $layout); } private function getAllDayEvents() { $all_day_events = array(); foreach ($this->events as $event) { if ($event->getIsAllDay()) { $all_day_events[] = $event; } } $all_day_events = array_values(msort($all_day_events, 'getEpochStart')); return $all_day_events; } private function getQueryRangeWarning() { $errors = array(); $range_start_epoch = $this->rangeStart->getEpoch(); $range_end_epoch = $this->rangeEnd->getEpoch(); $day_start = $this->getDateTime(); $day_end = id(clone $day_start)->modify('+1 day'); $day_start = $day_start->format('U'); $day_end = $day_end->format('U') - 1; if (($range_start_epoch != null && $range_start_epoch < $day_end && $range_start_epoch > $day_start) || ($range_end_epoch != null && $range_end_epoch < $day_end && $range_end_epoch > $day_start)) { $errors[] = pht('Part of the day is out of range'); } if (($this->rangeEnd->getEpoch() != null && $this->rangeEnd->getEpoch() < $day_start) || ($this->rangeStart->getEpoch() != null && $this->rangeStart->getEpoch() > $day_end)) { $errors[] = pht('Day is out of query range'); } return $errors; } private function renderSidebar() { $this->events = msort($this->events, 'getEpochStart'); $week_of_boxes = $this->getWeekOfBoxes(); $filled_boxes = array(); foreach ($week_of_boxes as $day_box) { $box_start = $day_box['start']; $box_end = id(clone $box_start)->modify('+1 day'); $box_start = $box_start->format('U'); $box_end = $box_end->format('U'); $box_events = array(); foreach ($this->events as $event) { $event_start = $event->getEpochStart(); $event_end = $event->getEpochEnd(); if ($event_start < $box_end && $event_end > $box_start) { $box_events[] = $event; } } $filled_boxes[] = $this->renderSidebarBox( $box_events, $day_box['title']); } return $filled_boxes; } private function renderSidebarBox($events, $title) { $widget = id(new PHUICalendarWidgetView()) ->addClass('calendar-day-view-sidebar'); $list = id(new PHUICalendarListView()) ->setUser($this->user) ->setView('day'); if (count($events) == 0) { $list->showBlankState(true); } else { $sorted_events = msort($events, 'getEpochStart'); foreach ($sorted_events as $event) { $list->addEvent($event); } } $widget ->setCalendarList($list) ->setHeader($title); return $widget; } private function getWeekOfBoxes() { $sidebar_day_boxes = array(); $display_start_day = $this->getDateTime(); $display_end_day = id(clone $display_start_day)->modify('+6 day'); $box_start_time = clone $display_start_day; $today_time = PhabricatorTime::getTodayMidnightDateTime($this->user); $tomorrow_time = clone $today_time; $tomorrow_time->modify('+1 day'); while ($box_start_time <= $display_end_day) { if ($box_start_time == $today_time) { $title = pht('Today'); } else if ($box_start_time == $tomorrow_time) { $title = pht('Tomorrow'); } else { $title = $box_start_time->format('l'); } $sidebar_day_boxes[] = array( 'title' => $title, 'start' => clone $box_start_time, ); $box_start_time->modify('+1 day'); } return $sidebar_day_boxes; } private function renderDayViewHeader() { $button_bar = null; $uri = $this->getBrowseURI(); if ($uri) { list($prev_year, $prev_month, $prev_day) = $this->getPrevDay(); $prev_uri = $uri.$prev_year.'/'.$prev_month.'/'.$prev_day.'/'; list($next_year, $next_month, $next_day) = $this->getNextDay(); $next_uri = $uri.$next_year.'/'.$next_month.'/'.$next_day.'/'; $button_bar = new PHUIButtonBarView(); $left_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-left bluegrey'); $left = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($prev_uri) ->setTitle(pht('Previous Day')) ->setIcon($left_icon); $right_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-right bluegrey'); $right = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($next_uri) ->setTitle(pht('Next Day')) ->setIcon($right_icon); $button_bar->addButton($left); $button_bar->addButton($right); } $display_day = $this->getDateTime(); $header_text = $display_day->format('l, F j, Y'); $header = id(new PHUIHeaderView()) ->setHeader($header_text); if ($button_bar) { $header->setButtonBar($button_bar); } return $header; } private function updateEventsFromCluster($cluster, $hourly_events) { $cluster_size = count($cluster); $n = 0; foreach ($cluster as $cluster_member) { $event_id = $cluster_member->getEventID(); $offset = (($n / $cluster_size) * 100).'%'; $width = ((1 / $cluster_size) * 100).'%'; if (isset($hourly_events[$event_id])) { $hourly_events[$event_id]['offset'] = $offset; $hourly_events[$event_id]['width'] = $width; } $n++; } return $hourly_events; } // returns DateTime of each hour in the day private function getHoursOfDay() { $included_datetimes = array(); $day_datetime = $this->getDateTime(); $day_epoch = $day_datetime->format('U'); $day_datetime->modify('+1 day'); $next_day_epoch = $day_datetime->format('U'); $included_time = $day_epoch; $included_datetime = $this->getDateTime(); while ($included_time < $next_day_epoch) { $included_datetimes[] = clone $included_datetime; $included_datetime->modify('+1 hour'); $included_time = $included_datetime->format('U'); } return $included_datetimes; } private function getPrevDay() { $prev = $this->getDateTime(); $prev->modify('-1 day'); return array( $prev->format('Y'), $prev->format('m'), $prev->format('d'), ); } private function getNextDay() { $next = $this->getDateTime(); $next->modify('+1 day'); return array( $next->format('Y'), $next->format('m'), $next->format('d'), ); } private function getDateTime() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $day = $this->day; $month = $this->month; $year = $this->year; $date = new DateTime("{$year}-{$month}-{$day} ", $timezone); return $date; } private function findTodayClusters() { $events = msort($this->todayEvents, 'getEpochStart'); $clusters = array(); foreach ($events as $event) { $destination_cluster_key = null; $event_start = $event->getEpochStart() - (30 * 60); $event_end = $event->getEpochEnd() + (30 * 60); foreach ($clusters as $key => $cluster) { foreach ($cluster as $clustered_event) { $compare_event_start = $clustered_event->getEpochStart(); $compare_event_end = $clustered_event->getEpochEnd(); if ($event_start < $compare_event_end && $event_end > $compare_event_start) { $destination_cluster_key = $key; break; } } } if ($destination_cluster_key !== null) { $clusters[$destination_cluster_key][] = $event; } else { $next_cluster = array(); $next_cluster[] = $event; $clusters[] = $next_cluster; } } return $clusters; } } diff --git a/src/view/phui/calendar/PHUICalendarListView.php b/src/view/phui/calendar/PHUICalendarListView.php index e1f37fa1a..fa998f11a 100644 --- a/src/view/phui/calendar/PHUICalendarListView.php +++ b/src/view/phui/calendar/PHUICalendarListView.php @@ -1,188 +1,189 @@ view; } public function setView($view) { $this->view = $view; return $this; } public function addEvent(AphrontCalendarEventView $event) { $this->events[] = $event; return $this; } public function showBlankState($state) { $this->blankState = $state; return $this; } protected function getTagName() { return 'div'; } protected function getTagAttributes() { require_celerity_resource('phui-calendar-css'); require_celerity_resource('phui-calendar-list-css'); return array('class' => 'phui-calendar-event-list'); } protected function getTagContent() { if (!$this->blankState && empty($this->events)) { return ''; } $singletons = array(); $allday = false; foreach ($this->events as $event) { $start_epoch = $event->getEpochStart(); if ($event->getIsAllDay()) { $timelabel = pht('All Day'); } else { $timelabel = phabricator_time( $event->getEpochStart(), $this->getUser()); } if ($event->getViewerIsInvited()) { $icon_color = 'green'; } else { $icon_color = null; } $dot = id(new PHUIIconView()) ->setIconFont($event->getIcon(), $icon_color) ->addClass('phui-calendar-list-item-icon'); $title = phutil_tag( 'span', array( 'class' => 'phui-calendar-list-title', ), $this->getEventTitle($event, $allday)); $time = phutil_tag( 'span', array( 'class' => 'phui-calendar-list-time', ), $timelabel); $class = 'phui-calendar-list-item'; if ($event->getViewerIsInvited()) { $class = $class.' phui-calendar-viewer-invited'; } if ($event->getIsAllDay()) { $class = $class.' all-day'; } $tip = $this->getEventTooltip($event); $tip_align = ($this->getView() == 'day') ? 'E' : 'N'; $content = javelin_tag( 'a', array( 'href' => '/E'.$event->getEventID(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tip, 'size' => 200, 'align' => $tip_align, ), ), array( $dot, $time, $title, )); $singletons[] = phutil_tag( 'li', array( 'class' => $class, ), $content); } if (empty($singletons)) { $singletons[] = phutil_tag( 'li', array( 'class' => 'phui-calendar-list-item-empty', ), pht('Clear sailing ahead.')); } $list = phutil_tag( 'ul', array( 'class' => 'phui-calendar-list', ), $singletons); return $list; } private function getEventTitle($event) { $class = 'phui-calendar-item'; return phutil_tag( 'span', array( 'class' => $class, ), $event->getName()); } private function getEventTooltip(AphrontCalendarEventView $event) { Javelin::initBehavior('phabricator-tooltips'); $start = id(AphrontFormDateControlValue::newFromEpoch( $this->getUser(), $event->getEpochStart())); $end = id(AphrontFormDateControlValue::newFromEpoch( $this->getUser(), $event->getEpochEnd())); + $start_date = $start->getDateTime()->format('m d Y'); + $end_date = $end->getDateTime()->format('m d Y'); + if ($event->getIsAllDay()) { - if ($start->getValueDay() == $end->getValueDay()) { + if ($start_date == $end_date) { $tip = pht('All day'); } else { $tip = pht( 'All day, %s - %s', $start->getValueAsFormat('M j, Y'), $end->getValueAsFormat('M j, Y')); } } else { - if ($start->getValueDay() == $end->getValueDay() && - $start->getValueMonth() == $end->getValueMonth() && - $start->getValueYear() == $end->getValueYear()) { + if ($start->getValueDate() == $end->getValueDate()) { $tip = pht( '%s - %s', $start->getValueAsFormat('g:i A'), $end->getValueAsFormat('g:i A')); } else { $tip = pht( '%s - %s', $start->getValueAsFormat('M j, Y, g:i A'), $end->getValueAsFormat('M j, Y, g:i A')); } } return $tip; } public function getIsViewerInvitedOnList() { foreach ($this->events as $event) { if ($event->getViewerIsInvited()) { return true; } } return false; } } diff --git a/webroot/rsrc/js/core/behavior-fancy-datepicker.js b/webroot/rsrc/js/core/behavior-fancy-datepicker.js index f2d092fae..c4b335aef 100644 --- a/webroot/rsrc/js/core/behavior-fancy-datepicker.js +++ b/webroot/rsrc/js/core/behavior-fancy-datepicker.js @@ -1,266 +1,295 @@ /** * @provides javelin-behavior-fancy-datepicker * @requires javelin-behavior * javelin-util * javelin-dom * javelin-stratcom * javelin-vector */ JX.behavior('fancy-datepicker', function() { var picker; var root; var value_y; var value_m; var value_d; 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); 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 { - y: JX.DOM.find(root, 'select', 'year-input'), - m: JX.DOM.find(root, 'select', 'month-input'), - d: JX.DOM.find(root, 'select', 'day-input'), + d: JX.DOM.find(root, 'input', 'date-input'), t: JX.DOM.find(root, 'input', 'time-input') }; }; var read_date = function() { var i = get_inputs(); - value_y = +i.y.value; - value_m = +i.m.value; - value_d = +i.d.value; + var date = i.d.value; + var parts = date.split('/'); + value_y = +parts[2]; + value_m = +parts[0]; + value_d = +parts[1]; }; var write_date = function() { var i = get_inputs(); - i.y.value = value_y; - i.m.value = value_m; - i.d.value = value_d; + i.d.value = value_m + '/' + value_d + '/' + value_y; }; 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[value_m - 1] + ' ' + value_y, null), + 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 { + 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(value_y, value_m - 1, 1).getDay(); + var start = new Date( + valid_date.getYear() + 1900, + valid_date.getMonth(), + 1).getDay(); + while (start--) { days.push(cell('', null, false, 'day-placeholder')); } - var today = new Date(); - for (ii = 1; ii <= 31; ii++) { - var date = new Date(value_y, value_m - 1, ii); - if (date.getMonth() != (value_m - 1)) { + 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, value_d == ii, classes.join(' '))); + 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 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(); + }); + });