Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
feat(calendar): imeplement virtual scrolling in calendar
Browse files Browse the repository at this point in the history
  • Loading branch information
jelbourn committed Aug 13, 2015
1 parent 15c7362 commit 34eb48c
Show file tree
Hide file tree
Showing 14 changed files with 712 additions and 441 deletions.
483 changes: 119 additions & 364 deletions src/components/calendar/calendar.js

Large diffs are not rendered by default.

131 changes: 106 additions & 25 deletions src/components/calendar/calendar.scss
Original file line number Diff line number Diff line change
@@ -1,29 +1,91 @@
// Styles for mdCalendar.
$date-cell-size: 44px !default;
$date-cell-emphasis-size: 40px !default;
$calendar-number-of-weeks: 7 !default;

// Style applied to date cells, including day-of-the-week header cells.
@mixin calendar-date-cell() {
height: $date-cell-size;
width: $date-cell-size;
$md-calendar-cell-size: 44px !default;
$md-calendar-header-height: 40px;
$md-calendar-cell-emphasis-size: 40px !default;
$md-calendar-side-padding: 16px !default;
$md-calendar-weeks-to-show: 7 !default;

$md-calendar-month-label-padding: 8px !default;
$md-calendar-month-label-font-size: 13px !default;

$md-calendar-width: (7 * $md-calendar-cell-size) + (2 * $md-calendar-side-padding);
$md-calendar-height:
($md-calendar-weeks-to-show * $md-calendar-cell-size) + $md-calendar-header-height;


// Styles for date cells, including day-of-the-week header cells.
@mixin md-calendar-cell() {
height: $md-calendar-cell-size;
width: $md-calendar-cell-size;

text-align: center;

// Remove all padding and borders so we can completely
// control the size of the table cells.
padding: 0;
border: none;

// The left / right padding is applied to the cells instead of the wrapper
// because we want the header background and the month dividing border to
// extend the entire width of the calendar.
&:first-child {
padding-left: $md-calendar-side-padding;
}

&:last-child {
padding-right: $md-calendar-side-padding;
}
}

// Styles for tables used in mdCalendar (the day-of-the-week header and the table of dates itself).
@mixin md-calendar-table() {
// Fixed table layout makes IE faster.
// https://msdn.microsoft.com/en-us/library/ms533020(VS.85).aspx
table-layout: fixed;
border-spacing: 0;
border-collapse: collapse;
}

md-calendar {
font-size: 12px;
font-size: 13px;
user-select: none;
}

.md-calendar-container {
position: relative;
max-height: $calendar-number-of-weeks * $date-cell-size;
// Wrap the scroll with overflow: hidden in order to hide the scrollbar.
// The inner .md-calendar-scroller will using a padding-right to push the
// scrollbar into the hidden area (done with javascript).
.md-calendar-scroll-mask {
display: inline-block;
overflow: hidden;
height: $md-calendar-weeks-to-show * $md-calendar-cell-size;

.md-virtual-repeat-scroller {
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}

.md-virtual-repeat-offsetter {
width: 100%;
}
}

// Scrolling element (the md-virtual-repeat-container).
.md-calendar-scroller {
display: inline-block;
height: $md-calendar-weeks-to-show * $md-calendar-cell-size;
width: $md-calendar-width;

&::-webkit-scrollbar {
display: none;
}
}

// A single date cell in the calendar table.
.md-calendar-date {
@include calendar-date-cell();
@include md-calendar-cell();
}

// Element inside of every date cell that can indicate that the date is selected.
.md-calendar-date-selection-indicator {
transition-property: background-color, color;
transition-duration: $swift-ease-out-duration;
Expand All @@ -34,25 +96,44 @@ md-calendar {

cursor: pointer;

width: $date-cell-emphasis-size;
height: $date-cell-emphasis-size;
line-height: $date-cell-emphasis-size;
width: $md-calendar-cell-emphasis-size;
height: $md-calendar-cell-emphasis-size;
line-height: $md-calendar-cell-emphasis-size;
}

// The label above each month (containing the month name and the year, e.g. "Jun 2014").
.md-calendar-month-label {
height: $date-cell-size;
height: $md-calendar-cell-size;
font-size: $md-calendar-month-label-font-size;
padding: 0 0 0 $md-calendar-side-padding + $md-calendar-month-label-padding;
}

.md-calendar-day-header th {
@include calendar-date-cell();
font-weight: normal;
// Table containing the day-of-the-week header.
.md-calendar-day-header {
@include md-calendar-table();

th {
@include md-calendar-cell();
font-weight: normal;
height: $md-calendar-header-height;
}
}

// Primary table containing all date cells. Each month is a tbody in this table.
.md-calendar {
// DEBUGGING: add border to container
border: 1px dotted lightgray;
}
@include md-calendar-table();
color: rgba(black, 0.7); // secondary

.md-calendar.ng-animate {
transition: transform $swift-ease-in-out-duration $swift-ease-in-out-timing-function;
// Divider between months.
tr:last-child td {
border-bottom: 1px solid #e9e9e9;
}

// The divider between months doesn't actualyl change the height of the tbody in which the
// border appear; it changes the height of the following tbody. The causes the first-child to be
// 1px shorter than the other months. We fix this by adding an invisible border-top.
&:first-child {
border-top: 1px solid transparent;
}
}

65 changes: 41 additions & 24 deletions src/components/calendar/calendar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ describe('md-calendar', function() {
var JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
NOV = 10, DEC = 11;

var ngElement, element, scope, pageScope, controller, $animate, $compile;
var $rootScope, dateLocale;
var ngElement, element, scope, pageScope, controller, $animate, $compile, $$rAF;
var $rootScope, dateLocale, $mdUtil;

/**
* To apply a change in the date, a scope $apply() AND a manual triggering of animation
Expand All @@ -15,11 +15,16 @@ describe('md-calendar', function() {
function applyDateChange() {
pageScope.$apply();
$animate.triggerCallbacks();
$$rAF.flush();

// Internally, the calendar sets scrollTop to scroll to the month for a change.
// The handler for that scroll won't be invoked unless we manually trigger it.
if (controller) {
angular.element(controller.calendarScroller).triggerHandler('scroll');
}
}

/**
* Extracts text as an array (one element per cell) from a tr element.
*/
/** Extracts text as an array (one element per cell) from a tr element. */
function extractRowText(tr) {
var cellContents = [];
angular.forEach(tr.children, function(tableElement) {
Expand All @@ -29,9 +34,7 @@ describe('md-calendar', function() {
return cellContents;
}

/**
* Finds a date td given a day of the month from an .md-calendar-month element.
*/
/** Finds a date td given a day of the month from an .md-calendar-month element. */
function findDateElement(monthElement, day) {
var tds = monthElement.querySelectorAll('td');
var td;
Expand All @@ -45,14 +48,14 @@ describe('md-calendar', function() {
}


/**
* Creates and compiles an md-calendar element.
*/
/** Creates and compiles an md-calendar element. */
function createElement(parentScope) {
var directiveScope = parentScope || $rootScope.$new();
var template = '<md-calendar ng-model="myDate"></md-calendar>';
var newElement = $compile(template)(directiveScope);
directiveScope.$apply();
var attachedElement = angular.element(template);
document.body.appendChild(attachedElement[0]);
var newElement = $compile(attachedElement)(directiveScope);
applyDateChange();
return newElement;
}

Expand All @@ -62,7 +65,9 @@ describe('md-calendar', function() {
$animate = $injector.get('$animate');
$compile = $injector.get('$compile');
$rootScope = $injector.get('$rootScope');
$$rAF = $injector.get('$$rAF');
dateLocale = $injector.get('$$mdDateLocale');
$mdUtil = $injector.get('$mdUtil');

pageScope = $rootScope.$new();
pageScope.myDate = null;
Expand All @@ -73,16 +78,22 @@ describe('md-calendar', function() {
controller = ngElement.controller('mdCalendar');
}));

afterEach(function() {
ngElement.remove();
});

describe('ngModel binding', function() {

it('should update the calendar based on ngModel change', function() {
pageScope.myDate = new Date(2014, MAY, 30);

applyDateChange();

var displayedMonth = element.querySelector('.md-calendar-month-label');
var selectedDate = element.querySelector('.md-calendar-selected-date');
var displayedMonth =
$mdUtil.getClosest(selectedDate, 'tbody').querySelector('.md-calendar-month-label');

expect(displayedMonth.textContent).toBe('May');
expect(displayedMonth.textContent).toBe('May 2014');
expect(selectedDate.textContent).toBe('30');
});

Expand All @@ -109,9 +120,16 @@ describe('md-calendar', function() {
});

describe('#buildCalendarForMonth', function() {
var monthCtrl;

beforeEach(function() {
monthCtrl = angular.element(element.querySelector('[md-calendar-month]'))
.controller('mdCalendarMonth');
});

it('should render a month correctly as a table', function() {
var date = new Date(2014, MAY, 30);
var monthElement = controller.buildCalendarForMonth(date);
var monthElement = monthCtrl.buildCalendarForMonth(date);

var calendarRows = monthElement.querySelectorAll('tr');
var calendarDates = [];
Expand All @@ -121,21 +139,22 @@ describe('md-calendar', function() {
});

var expectedDates = [
['May', '', '', '1', '2', '3'],
['May 2014', '', '', '1', '2', '3'],
['4', '5', '6', '7', '8', '9', '10'],
['11', '12', '13', '14', '15', '16', '17'],
['18', '19', '20', '21', '22', '23', '24'],
['25', '26', '27', '28', '29', '30', '31'],
['', '', '', '', '', '', ''],
];
expect(calendarDates).toEqual(expectedDates);
});

it('should show the month on its own row if the first day is before Tuesday', function() {
var date = new Date(2014, JUN, 30); // 1st on Sunday
var monthElement = controller.buildCalendarForMonth(date);
var monthElement = monthCtrl.buildCalendarForMonth(date);

var firstRow = monthElement.querySelector('tr');
expect(extractRowText(firstRow)).toEqual(['Jun']);
expect(extractRowText(firstRow)).toEqual(['Jun 2014']);
});
});

Expand All @@ -144,11 +163,9 @@ describe('md-calendar', function() {
pageScope.myDate = controller.today;
applyDateChange();

var monthElement = element.querySelector('.md-calendar-month');
var day = controller.today.getDate();

var dateElement = findDateElement(monthElement, day);
expect(dateElement.classList.contains('md-calendar-date-today')).toBe(true);
var todayElement = element.querySelector('.md-calendar-date-today');
expect(todayElement).not.toBeNull();
expect(todayElement.textContent).toBe(controller.today.getDate() + '');
});

it('should have ids for date elements unique to the directive instance', function() {
Expand Down
Loading

0 comments on commit 34eb48c

Please sign in to comment.