diff --git a/src/view/phui/calendar/PHUICalendarDayView.php b/src/view/phui/calendar/PHUICalendarDayView.php index 7a96100b2a..cbf1023f61 100644 --- a/src/view/phui/calendar/PHUICalendarDayView.php +++ b/src/view/phui/calendar/PHUICalendarDayView.php @@ -1,577 +1,579 @@ events[] = $event; return $this; } public function setBrowseURI($browse_uri) { $this->browseURI = $browse_uri; return $this; } private function getBrowseURI() { return $this->browseURI; } 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'); $hours = $this->getHoursOfDay(); $js_hourly_events = array(); $hourly_events = array(); $first_event_hour = null; $all_day_events = $this->getAllDayEvents(); $today_all_day_events = array(); $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) { $today_all_day_events[] = $all_day_event; } } foreach ($hours as $hour) { $current_hour_events = array(); $hour_start = $hour->format('U'); $hour_end = id(clone $hour)->modify('+1 hour')->format('U'); foreach ($this->events as $event) { if ($event->getIsAllDay()) { continue; } if (($hour == $day_start && $event->getEpochStart() <= $hour_start && $event->getEpochEnd() > $day_start_epoch) || ($event->getEpochStart() >= $hour_start && $event->getEpochStart() < $hour_end)) { $current_hour_events[] = $event; $this->todayEvents[] = $event; $this->jsTodayEvents[] = array( 'eventStartEpoch' => $event->getEpochStart(), 'eventEndEpoch' => $event->getEpochEnd(), 'eventName' => $event->getName(), + 'eventID' => $event->getEventID(), ); } } foreach ($current_hour_events as $event) { $day_start_epoch = $this->getDateTime()->format('U'); $event_start = max($event->getEpochStart(), $day_start_epoch); $event_end = min($event->getEpochEnd(), $day_end_epoch); $top = (($event_start - $hour_start) / ($hour_end - $hour_start)) * 100; $top = max(0, $top); $height = (($event_end - $event_start) / ($hour_end - $hour_start)) * 100; $height = min(2400, $height); if ($first_event_hour === null) { $first_event_hour = $hour; } - $js_hourly_events[$event->getEventID()] = array( + $js_hourly_events[] = array( 'eventStartEpoch' => $event->getEpochStart(), 'eventEndEpoch' => $event->getEpochEnd(), 'eventName' => $event->getName(), + 'eventID' => $event->getEventID(), 'hour' => $hour, 'offset' => '0', 'width' => '100%', 'top' => $top.'%', 'height' => $height.'%', ); $hourly_events[$event->getEventID()] = array( 'hour' => $hour, 'event' => $event, 'offset' => '0', 'width' => '100%', 'top' => $top.'%', 'height' => $height.'%', ); } } $clusters = $this->findTodayClusters(); foreach ($clusters as $cluster) { $hourly_events = $this->updateEventsFromCluster( $cluster, $hourly_events); } $rows = array(); foreach ($hours as $hour) { $early_hours = array(8); if ($first_event_hour) { $early_hours[] = $first_event_hour->format('G'); } if ($hour->format('G') < min($early_hours)) { continue; } $drawn_hourly_events = array(); $cell_time = phutil_tag( 'td', array('class' => 'phui-calendar-day-hour'), $hour->format('g A')); foreach ($hourly_events as $hourly_event) { if ($hourly_event['hour'] == $hour) { $drawn_hourly_events[] = $this->drawEvent( $hourly_event['event'], $hourly_event['offset'], $hourly_event['width'], $hourly_event['top'], $hourly_event['height']); } } $cell_event = phutil_tag( 'td', array('class' => 'phui-calendar-day-events'), $drawn_hourly_events); $row = phutil_tag( 'tr', array(), array($cell_time, $cell_event)); $rows[] = $row; } $table = phutil_tag( 'table', array('class' => 'phui-calendar-day-view'), $rows); $all_day_event_box = new PHUIBoxView(); foreach ($today_all_day_events as $all_day_event) { $all_day_event_box->appendChild( $this->drawAllDayEvent($all_day_event)); } $header = $this->renderDayViewHeader(); $sidebar = $this->renderSidebar(); $warnings = $this->getQueryRangeWarning(); Javelin::initBehavior( 'day-view', array( 'todayEvents' => $this->jsTodayEvents, 'hourlyEvents' => $js_hourly_events, )); $table_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($all_day_event_box) ->appendChild($table) ->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; } private function drawAllDayEvent(AphrontCalendarEventView $event) { $class = 'day-view-all-day'; if ($event->getViewerIsInvited()) { $class = $class.' viewer-invited-day-event'; } $name = phutil_tag( 'a', array( 'class' => $class, 'href' => $event->getURI(), ), $event->getName()); $all_day_label = phutil_tag( 'span', array( 'class' => 'phui-calendar-all-day-label', ), pht('All Day')); $div = phutil_tag( 'div', array( 'class' => 'phui-calendar-day-event', ), array( $all_day_label, $name, )); return $div; } private function drawEvent( AphrontCalendarEventView $event, $offset, $width, $top, $height) { $class = 'phui-calendar-day-event-link'; if ($event->getViewerIsInvited()) { $class = $class.' viewer-invited-day-event'; } $name = phutil_tag( 'a', array( 'class' => $class, 'href' => $event->getURI(), ), $event->getName()); $div = phutil_tag( 'div', array( 'class' => 'phui-calendar-day-event', 'style' => 'left: '.$offset .'; width: '.$width .'; top: '.$top .'; height: '.$height .';', ), $name); return $div; } // 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/webroot/rsrc/js/application/calendar/behavior-day-view.js b/webroot/rsrc/js/application/calendar/behavior-day-view.js index f527de748f..bcd87912cc 100644 --- a/webroot/rsrc/js/application/calendar/behavior-day-view.js +++ b/webroot/rsrc/js/application/calendar/behavior-day-view.js @@ -1,59 +1,85 @@ /** * @provides javelin-behavior-day-view */ JX.behavior('day-view', function(config) { var hourly_events = config.hourlyEvents; var today_events = config.todayEvents; function findTodayClusters() { var events = today_events.sort(function(x, y){ return (x.eventStartEpoch - y.eventStartEpoch); }); var clusters = []; for (var i=0; i < events.length; i++) { var today_event = events[i]; var destination_cluster_index = null; var event_start = today_event.eventStartEpoch - (30*60); var event_end = today_event.eventEndEpoch + (30*60); for (var j=0; j < clusters.length; j++) { var cluster = clusters[j]; for(var k=0; k < cluster.length; k++) { var clustered_event = cluster[k]; var compare_event_start = clustered_event.eventStartEpoch; var compare_event_end = clustered_event.eventEndEpoch; if (event_start < compare_event_end && event_end > compare_event_start) { destination_cluster_index = j; break; } } if (destination_cluster_index !== null) { break; } } if (destination_cluster_index !== null) { clusters[destination_cluster_index].push(today_event); destination_cluster_index = null; } else { var next_cluster = []; next_cluster.push(today_event); clusters.push(next_cluster); } } return clusters; } + function updateEventsFromCluster(cluster, hourly_events) { + var cluster_size = cluster.length; + var n = 0; + for(var i=0; i < cluster.length; i++) { + var cluster_member = cluster[i]; + + var event_id = cluster_member.eventID; + var offset = ((n / cluster_size) * 100) + '%'; + var width = ((1 / cluster_size) * 100) + '%'; + + for (var j=0; j < hourly_events.length; j++) { + if (hourly_events[j].eventID == event_id) { + + hourly_events[j]['offset'] = offset; + hourly_events[j]['width'] = width; + } + } + n++; + } + + return hourly_events; + } + var today_clusters = findTodayClusters(); + for(var i=0; i < today_clusters.length; i++) { + hourly_events = updateEventsFromCluster(today_clusters[i], hourly_events); + } });