/* * Schedule view module. * The schedule view manages the small and large schedules (formerly called * calendars, but renamed to avoid ambiguity with the iCalendar module). */ /* * All absolute times are "minutes past midnight" */ var CV_NUM_DAYS = 5; /* number of days per week */ var CV_SLOTS_PER_HOUR = 6; /* time slots per hour (for conflict resolution */ var CV_LATEST_START = 10; /* cal view won't start after 10am */ var CV_EARLIEST_END = 15; /* cal view won't end before 3pm */ var CV_HOURS_PER_DAY = 24; var CV_HORIZONTAL_BORDER_PIXELS = 1; /* space to skip when adding div */ var CV_VERTICAL_BORDER_PIXELS = 1; /* space to skip when adding div */ var CV_LINES_PER_SLOT = 1; var CV_BIG_LINE_HEIGHT = 12; /* px: one more than the actual lineheight for underline */ var CV_SMALL_LINE_HEIGHT = 2; /* px */ var CV_BIG_PIXELS_PER_SLOT = CV_LINES_PER_SLOT * CV_BIG_LINE_HEIGHT; var CV_BIG_PIXELS_PER_HOUR = CV_BIG_PIXELS_PER_SLOT * CV_SLOTS_PER_HOUR; var CV_SMALL_PIXELS_PER_HOUR = CV_SLOTS_PER_HOUR * CV_LINES_PER_SLOT * CV_SMALL_LINE_HEIGHT; var CV_CONFLICT_COLOR = "#FF0000"; /* * Construct a new calendar view. If the calendar is "big", then it shows labels * for each course. * @param elt the tbody element that the calendar should use for its body * @param isBig whether this is the big calendar. */ function CalView(elt, isBig, cursem) { this.ctable = new Array(CV_NUM_DAYS); for (var ii = 0; ii < this.ctable.length; ++ii) { this.ctable[ii] = new Array(CV_SLOTS_PER_HOUR * CV_HOURS_PER_DAY); for (var jj = 0; jj < this.ctable[ii].length; ++jj) { this.ctable[ii][jj] = new Hashtable(); } } this.tableBody = elt; this.colorIterator = 0; this.isBig = isBig; this.stylePrefix = (isBig ? "Big" : "Small") + "Schedule"; this.divs = new Array(); this.rerender(); this.currentSemester = cursem; if(cursem) this.initializeSemester(cursem); } CalView.prototype.addCourses = function (courses) { for (var ii in courses) { var course = courses[ii]; /* * Extract the time blocks, and add them to the conflict table. */ var times = course.realTime.split(";"); if (!this.ctable[0][0].get(course.semesterText)) this.initializeSemester(course.semesterText); for (var ii = 0; ii < times.length; ++ii) { var parts = times[ii].split("/"); var daynum = parseInt(parts[0]); var start = parseInt(parts[1]); /* minutes past midnight */ var end = parseInt(parts[2]); /* minutes */ var startslot = Math.floor(start * CV_SLOTS_PER_HOUR / 60); var endslot = Math.floor(end * CV_SLOTS_PER_HOUR / 60); for (var slot = startslot; slot < endslot; ++slot) { this.ctable[daynum][slot].get( course.semesterText).put(course.getID(), course); } } ++this.colorIterator; } /* * Redraw the calendar. */ this.rerender(); } CalView.prototype.removeCourse = function (id) { /* * Iterate over the entire conflict table and remove any references to * unused courses. */ for (var ii = 0; ii < this.ctable.length; ++ii) { for (var jj = 0; jj < this.ctable[ii].length; ++jj) { var semesters = this.ctable[ii][jj].keys(); for (var kk = 0; kk < semesters.length; ++kk) { this.ctable[ii][jj].get(semesters[kk]).remove(id); } } } /* * Redraw the calendar. */ this.rerender(); } CalView.prototype.clear = function () { /* * Iterate over the entire conflict table and remove any references to * unused courses. */ for (var ii = 0; ii < this.ctable.length; ++ii) { for (var jj = 0; jj < this.ctable[ii].length; ++jj) { this.ctable[ii][jj] = new Hashtable(); } } if (this.currentSemester) this.initializeSemester(this.currentSemester); /* * Redraw the calendar. */ this.rerender(); } /* * Rerender the calendar. This figures out the minimum and maximum hours * we need to display, and regenerates the table body. Then it generates * divs for all the blocks used by courses. */ CalView.prototype.rerender = function () { /* * If the semester is null, there can be no courses, so we can abort here. */ if (!this.currentSemester) { this.tableBody.parentNode.style.display = 'none'; return; } /* * It's annoying that we reach into the controller here, but not worth * avoiding right now. */ controller.setIgnoreResizeEvents(true); /* * Delete the current table body and all DIVs. * We'll regenerate them below. */ for (var ii in this.divs) { this.divs[ii].parentNode.removeChild(this.divs[ii]); } this.divs = new Array(); var tbody = this.tableBody; while (tbody.rows.length > 0) { tbody.deleteRow(0); } this.tableBody.parentNode.style.display = 'block'; /* * First, find the minimum and maximum times to use. */ var highSlot = CV_HOURS_PER_DAY * CV_SLOTS_PER_HOUR; var firstOccupiedSlot = highSlot; var lastOccupiedSlot = 0; for (var day = 0; day < this.ctable.length; ++day) { for (var slot = 0; slot < firstOccupiedSlot; ++slot) { if (!this.ctable[day][slot].get(this.currentSemester).isEmpty()) { firstOccupiedSlot = slot; break; } } for (var slot = highSlot-1; slot > lastOccupiedSlot; --slot) { if (!this.ctable[day][slot].get(this.currentSemester).isEmpty()) { lastOccupiedSlot = slot; break; } } } var firstHourDisplayed = Math.floor(firstOccupiedSlot / CV_SLOTS_PER_HOUR); var lastHourDisplayed = 1+Math.ceil((lastOccupiedSlot) / CV_SLOTS_PER_HOUR); firstHourDisplayed = Math.min(CV_LATEST_START, firstHourDisplayed); lastHourDisplayed = Math.max(CV_EARLIEST_END, lastHourDisplayed); /* Save away the number of slots we skipped. */ this.skippedSlots = firstHourDisplayed * CV_SLOTS_PER_HOUR; /* * Regenerate the table which contains the schedule. */ for (var hour = firstHourDisplayed; hour < lastHourDisplayed; ++hour) { var row = tbody.insertRow(tbody.rows.length); row.className = this.stylePrefix + "Row"; var displayTime = this.isBig ? encodedTimeToDisplayString(hour * 60) : encodedTimeToShortDisplayString(hour*60); var node = row.insertCell(0); node.appendChild(document.createTextNode(displayTime)); node.className = this.stylePrefix + "TimeCell"; for (var day = 0; day < this.ctable.length; ++day) { node = row.insertCell(day+1); node.innerHTML = " "; node.className = this.stylePrefix + "EmptyCell"; } } /* * Generate all the DIV's that we need. */ var ncourses = 0; for (var day = 0; day < this.ctable.length; ++day) { var firstSlotInCurrentBlock = 0; var setForCurrentBlock = new Hashtable(); for (var sloti = 0; sloti < this.ctable[day].length; ++sloti) { if (!this.ctable[day][sloti].get(this.currentSemester).hasSameKeys(setForCurrentBlock)) { if (!setForCurrentBlock.isEmpty()) { ++ncourses; this.addDiv(setForCurrentBlock, day, firstSlotInCurrentBlock, sloti); } firstSlotInCurrentBlock = sloti; } setForCurrentBlock = this.ctable[day][sloti].get(this.currentSemester); } if (!setForCurrentBlock.isEmpty()) { ++ncourses; addDiv(setForCurrentBlock, day, firstSlotInCurrentBlock, highSlot-1); } } /* If we didn't have any courses to add at all, hide the parent. */ if (ncourses == 0) { this.tableBody.parentNode.style.display = 'none'; } controller.setIgnoreResizeEvents(false); } CalView.prototype.initializeSemester = function (sem) { for (var ii = 0; ii < this.ctable.length; ++ii) { for (var jj = 0; jj < this.ctable[ii].length; ++jj) { this.ctable[ii][jj].put(sem, new Hashtable()); } } } /* * Adds a DIV element to the calendar representing a particular block (a meeting * time for a course) * @param courses a hashtable of course objects (as handed to us by the * controller) meeting at this block, indexed by course id * @param day an index representing the day on which this block falls * (0-CV_NUM_DAYS) * @param startSlot the starting "slot" (see algorithm and constants above) - * used to determine the location of the block on the schedule * @param endSlot the ending "slot" (ditto) - used to determine the height */ CalView.prototype.addDiv = function (courses, day, startSlot, endSlot) { var rowindex = Math.floor((startSlot - this.skippedSlots)/ CV_SLOTS_PER_HOUR); var td = this.tableBody.rows[rowindex].cells[day+1]; /* * Absolutely-positioned elements are actually placed relative to the * top-left of the containing block. We get the location of the parent * node and add the offset within that of the table cell. We can't * get the cell position directly because you can't call offsetParent * on something whose position has changed in IE. But this does * effectively the same thing. */ var coords = Util_findPos(this.tableBody.parentNode); var top = coords[1] + td.offsetTop; var left = coords[0] + td.offsetLeft; var topPanel = document.getElementById('HeaderPanel'); var div = document.createElement('div'); div.className = this.stylePrefix + 'Block'; div.style.position = 'absolute'; div.style.left = left + "px"; div.style.top = top + "px"; /* * Add the label and adjust the height. */ if (this.isBig) { var boxHeight = (endSlot - startSlot) * CV_BIG_PIXELS_PER_SLOT; div.style.height = boxHeight + "px"; var maxLines = boxHeight / CV_BIG_LINE_HEIGHT; /* The real top is somewhere in the cell (possibly the top of the cell) */ var voffset = (startSlot % CV_SLOTS_PER_HOUR)*CV_BIG_PIXELS_PER_SLOT; div.style.top = (top + voffset) + "px"; /* * I don't understand why td.clientWidth is sometimes 0 (which * makes the following expression negative). But on IE, some- * times it is. With this hack in place, things seem to work. */ div.style.width = Util_max(0, (td.clientWidth - 2*CV_HORIZONTAL_BORDER_PIXELS)) + "px"; var numToShow = courses.size(); if (maxLines < courses.size()) { numToShow = maxLines - 1; } for (var ii = 0; ii < numToShow; ++ii) { var course = (courses.values())[ii]; var detailsAnchor = document.createElement('a'); detailsAnchor.href = SearchUtil_getCourseUrl(course); detailsAnchor.title = course.number + ": " + course.title + " (" + course.professor + ")"; detailsAnchor.appendChild(document.createTextNode(course.number)); div.appendChild(detailsAnchor); div.appendChild(document.createElement('br')); } if (numToShow < courses.size()) { var diff = courses.size() - numToShow; div.appendChild(document.createTextNode('('+diff + ' more...)')); } } else { var boxHeight = (endSlot - startSlot) * CV_LINES_PER_SLOT + (CV_VERTICAL_BORDER_PIXELS * ((endSlot-startSlot)/CV_SLOTS_PER_HOUR-1)); div.style.top = (top + (((startSlot % CV_SLOTS_PER_HOUR)/CV_SLOTS_PER_HOUR) * td.offsetHeight)) + "px"; div.style.height = (CV_SMALL_LINE_HEIGHT * boxHeight) + "pt"; div.innerHTML = " "; } if (courses.size() == 1) { var course = (courses.values())[0]; div.style.backgroundColor = course.color; div.title = course.number + ": " + course.title + " (" + course.professor + ")"; } else { div.style.backgroundColor = CV_CONFLICT_COLOR; } this.divs.push(div); document.body.appendChild(div); } CalView.prototype.changeSemester = function (semText) { this.currentSemester = semText; /* make sure the hashtable exists for this semester in each row/col */ if (!this.ctable[0][0].get(semText)) { this.initializeSemester(semText); } this.rerender(); } /* * Convert a "number of minutes past midnight" to a useful time string (like * "9:05 AM") */ function encodedTimeToDisplayString(minutesPastMidnight) { var military_hour = Math.floor(minutesPastMidnight / 60); var useful_hour = military_hour % 12; var ampm = ((military_hour < 12) ? " AM" : " PM"); if (useful_hour == 0) useful_hour = 12; return useful_hour + ampm; } /* * Returns "10" for 10am, etc. */ function encodedTimeToShortDisplayString(minutesPastMidnight) { /* if it's zero, we want it be 12. */ return (Math.floor(minutesPastMidnight/60) % 12) || 12; } /** * Table view module. * This module is used to display the agenda view and exam table views. * The agenda is the table shown below the big schedule, which includes * most of a course's detailed information, as well as the the table shown * below the search box. * The exam table is shown on the cart details page underneath the agenda table. */ /* * We'll generate the column information once at the beginning, since it * can be reused for all Agendas. */ function createHeaderNode(title) { var th = document.createElement('th'); th.innerHTML = title; return th; } function createTextNode(text) { var td = document.createElement('td'); td.appendChild(document.createTextNode(text)); return td; } function ctvCreateSimpleColumn(headerHTML, sortProperty, displayProperty) { return { headerNode: createHeaderNode(headerHTML), comparator: function (c1, c2) { return SortableTable_lessThanComparator(c1[sortProperty], c2[sortProperty]); }, renderer: function(course) { return createTextNode(course[displayProperty]); } }; } function btvCreateSimpleColumn(headerHTML, sortObject, sortProperty, displayObject, displayProperty) { return { headerNode: createHeaderNode(headerHTML), comparator: function (data1, data2) { return SortableTable_lessThanComparator(data1[sortObject][sortProperty], data2[sortObject][sortProperty]); }, renderer: function(data) { return createTextNode(data[displayObject][displayProperty]); } }; } var CTV_ALL_COLS = { number: { headerNode: createHeaderNode('#'), comparator: function (c1, c2) { return SortableTable_lessThanComparator(c1.number, c2.number); }, renderer: function(course) { var td = document.createElement('td'); var link = document.createElement('a'); td.style.backgroundColor = course.color; link.href = SearchUtil_getCourseUrl(course); link.appendChild(document.createTextNode(course.number)); td.appendChild(link); return td; } }, courseRegistrationNumber: ctvCreateSimpleColumn('CRN', 'courseRegistrationNumber', 'courseRegistrationNumber'), title: ctvCreateSimpleColumn('Title', 'title', 'title'), when_where: ctvCreateSimpleColumn('When/Where', 'sortKeyTime', 'displayTime'), short_when: ctvCreateSimpleColumn('', 'sortKeyTime', 'shortDisplayTime'), professor: { headerNode: createHeaderNode('Professor'), comparator: function (c1, c2) { /* sort by last names */ var prof1 = c1.professor.split("\\s+")[-1]; var prof2 = c2.professor.split("\\s+")[-1]; return SortableTable_lessThanComparator(prof1, prof2); }, renderer: function(course) { return createTextNode(course.professor); } }, trash: { headerNode: createHeaderNode(''), comparator: null, renderer: function(course) { var td = document.createElement('td'); var link = document.createElement('a'); var img = document.createElement('img'); link.href = "javascript:controller.removeCourseAndSave('"+course.getID()+"')"; link.title = "Remove this course from your cart"; img.src = "images/trash2.gif"; img.style.border = "none"; //img.style.height="16px"; //img.style.width="16px"; link.appendChild(img); td.appendChild(link); return td; } }, exam: { headerNode: createHeaderNode('Exam group'), comparator: function(c1,c2) { return SortableTable_lessThanComparator(c1.sortKeyExam, c2.sortKeyExam); }, renderer: function (course) { var td = document.createElement('td'); td.innerHTML = course.examInfo; return td; } } }; var CTV_BIG_AGENDA_COLS = [ CTV_ALL_COLS.number, CTV_ALL_COLS.courseRegistrationNumber, CTV_ALL_COLS.title, CTV_ALL_COLS.when_where, CTV_ALL_COLS.professor, CTV_ALL_COLS.trash ]; var CTV_SMALL_AGENDA_COLS = [ CTV_ALL_COLS.number, CTV_ALL_COLS.title, CTV_ALL_COLS.short_when, CTV_ALL_COLS.trash ]; var CTV_EXAM_COLS = [ CTV_ALL_COLS.number, CTV_ALL_COLS.title, CTV_ALL_COLS.exam ]; var BTV_COLS = [ { headerNode: createHeaderNode('Course #'), comparator: function (data1, data2) { return SortableTable_lessThanComparator(data1.course.number, data2.course.number); }, renderer: function(data) { var course = data.course; var td = document.createElement('td'); var link = document.createElement('a'); td.style.backgroundColor = course.color; link.href = SearchUtil_getCourseUrl(course); link.appendChild(document.createTextNode(course.number)); td.appendChild(link); return td; } }, { headerNode: createHeaderNode('Book Title'), comparator: function (data1, data2) { return SortableTable_lessThanComparator(data1.book.title, data2.book.title); }, renderer: function(data) { var book = data.book; var td = document.createElement('td'); var link = document.createElement('a'); link.href = AmazonUtil_getProductPageURL(book.isbn); alert(data.course.number); link.appendChild(document.createTextNode(data.course.number)); link.appendChild(document.createTextNode(book.title)); td.appendChild(link); return td; } }, btvCreateSimpleColumn('Author', 'book', 'author', 'book', 'author'), btvCreateSimpleColumn('ISBN', 'book', 'isbn', 'book', 'isbn') /*, { headerNode: createHeaderNode('Add to Amazon Cart'), comparator: function (data1, data2) { return SortableTable_lessThanComparator(data1.book.getID(), data2.book.getID()); }, renderer: function(data) { var course = data.course; var td = document.createElement('td'); var link = document.createElement('a'); td.style.backgroundColor = course.color; link.href = AmazonUtil_getAddToCartUrl(course); link.appendChild(document.createTextNode('Add')); td.appendChild(link); return td; } }*/ ]; /* * Abstract class with a SortableTable for each semester and a bunch of courses. * Subclasses can define what the rows of the table are. * * Attributes of the TableView object: * tables semester -> SortableTable hashtable * columns array of columns this object was created with * course2semester course id -> semester hashtable * currentSemester semester * element parent element of this table */ function TableView() {} /* * Hack to get around the difficulties of calling the superconstructor in JS. * Call this from subclass's constructor. */ TableView.prototype.initTableView = function (cols, elt, cursem) { this.tables = new Hashtable(); this.course2semester = new Hashtable(); this.element = elt; this.columns = cols; if (cursem) this.changeSemester(cursem); } TableView.prototype.addCourses = function (courses) { for (var ii in courses) { var course = courses[ii]; if (!this.tables.containsKey(course.semesterText)) { this.tables.put(course.semesterText, new SortableTable(this.columns)); } this.course2semester.put(course.getID(), course.semesterText); this.addTableRowsForCourse(this.tables.get(course.semesterText), course); } this.rerender(); /* might cause the table to appear/disappear */ } TableView.prototype.removeCourse = function (id) { this.removeTableRowsForCourse(this.tables.get(this.course2semester.get(id)), id); this.rerender(); /* might cause the table to appear/disappear */ } TableView.prototype.clear = function () { if (this.currentSemester) this.tables.get(this.currentSemester).clear(); this.rerender(); /* might cause the table to appear/disappear */ } /* * Rerender chooses which SortableTable to display, shows it, and hides the * others. It doesn't show any table at all if there are no courses in the * current semester. */ TableView.prototype.rerender = function () { if (this.element.hasChildNodes()) this.element.removeChild(this.element.childNodes[0]); this.element.style.display = 'block'; if (this.currentSemester) { var tbl = this.tables.get(this.currentSemester); Util_assert(tbl != null); this.element.appendChild(tbl.getDomNode()); if (tbl.getNumRows() == 0) this.element.style.display = 'none'; } else { this.element.style.display = 'none'; } } TableView.prototype.changeSemester = function (sem) { this.currentSemester = sem; if (!this.tables.containsKey(sem)) this.tables.put(sem, new SortableTable(this.columns)); this.rerender(); } TableView.prototype.addTableRowsForCourse = function (table, course) {} TableView.prototype.removeTableRowsForCourse = function (table, courseID) {} /* * TableView whose rows are courses. */ function CourseTableView(cols, elt, cursem) { this.initTableView(cols, elt, cursem); } CourseTableView.prototype = new TableView(); CourseTableView.prototype.addTableRowsForCourse = function (table, course) { table.put(course.getID(), course); } CourseTableView.prototype.removeTableRowsForCourse = function (table, courseID) { table.remove(courseID); } /* * Table showing all the books for all the courses in your cart. * TODO what does it look like when empty? */ function BookTableView(elt, cursem, forMultipleCourses) { this.elt = elt; this.cursem = cursem; this.courses = {}; this.forMultipleCourses = forMultipleCourses; } BookTableView.prototype.addCourses = function (courses) { for (var ii = 0; ii < courses.length; ii++) { this.courses[courses[ii].getID()] = courses[ii]; } this.rerender(); } BookTableView.prototype.removeCourse = function (id) { delete this.courses[id]; this.rerender(); } BookTableView.prototype.clear = function () { this.courses = {}; this.rerender(); } BookTableView.prototype.changeSemester = function (sem) { this.cursem = sem; this.rerender(); } BookTableView.prototype.rerender = function() { if (this.container) { this.elt.removeChild(this.container); } if (this.hasBooks()) { this.renderNonEmpty(); } else { this.renderEmpty(); } } BookTableView.prototype.renderNonEmpty = function() { /* Create the table */ this.container = this.elt.appendChild(this.createElement("div", "BookTableContainer")); var table = this.container.appendChild(this.createElement("table", "BookTable")) var thead = table.appendChild(document.createElement("thead")); var tbody = table.appendChild(document.createElement("tbody")); var tfoot = table.appendChild(document.createElement("tfoot")); /* Create the header block */ var row1 = thead.insertRow(-1); var row2 = thead.insertRow(-1); row1.appendChild(this.createThWithText("Books", "BookTableTitle")).rowSpan = 2; row1.appendChild(this.createThWithText("", "BookRequired")).rowSpan = 2; row1.appendChild(this.createThWithText("Brown Bookstore price", "BookPrice")).colSpan = 2; row1.appendChild(this.createThWithText("Best Amazon price", "BookPrice")).colSpan = 2; row2.appendChild(this.createThWithText("New", "BookPrice")); row2.appendChild(this.createThWithText("Used", "BookPrice")); row2.appendChild(this.createThWithText("New", "BookPrice")); row2.appendChild(this.createThWithText("Used", "BookPrice")); /* Create a row for each book */ var totalPriceTracker = new TotalBookPriceTracker(); var lastcourse; for (var id in this.courses) { var course = this.courses[id]; if (course.semesterText == this.cursem) { for (var jj = 0; jj < course.books.length; jj++) { var book = course.books[jj]; /* Create a header row for this course if it hasn't been created yet */ if (this.forMultipleCourses && course != lastcourse) { var courseRow = tbody.insertRow(-1); courseRow.className = "CourseNameRow"; var className = courseRow.appendChild(this.createElement("td", "CourseNameLink")); className.appendChild(courseRow.appendChild(this.createLink(SearchUtil_getCourseUrl(course), course.number + ": " + course.title))); className.colSpan = 6; lastcourse = course; } /* Title row */ var titleRow = tbody.insertRow(-1); titleRow.className = "BookTitleRow"; var titleData = titleRow.appendChild(this.createElement("td", "BookTitle")); if (book.amazonUrl && book.amazonUrl.length > 0) { titleData.appendChild(this.createLink(book.amazonUrl, book.title)); } else { titleData.appendChild(document.createTextNode(book.title)); } titleData.colSpan = 6; /* Author, required, prices row */ var extraRow = tbody.insertRow(-1); extraRow.className = "BookRow"; extraRow.appendChild(this.createTdWithText(book.author, "BookAuthor")); extraRow.appendChild(this.createTdWithText((book.required ? "Required" : "Not Required"), "BookRequired")); extraRow.appendChild(this.createPriceCell(book.bookstoreUrl, book.priceBookstoreNew)); extraRow.appendChild(this.createPriceCell(book.bookstoreUrl, book.priceBookstoreUsed)); extraRow.appendChild(this.createPriceCell(book.amazonUrl, book.priceAmazonNew)); extraRow.appendChild(this.createPriceCell(book.amazonUrl, book.priceAmazonUsed)); totalPriceTracker.addBook(book); } } } /* Create the total rows */ var requiredTotalRow = tfoot.insertRow(-1); requiredTotalRow.appendChild(this.createTdWithText("Total price for required books", "BookTotal")).colSpan = 2; requiredTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "bookstore", "new", true)); requiredTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "bookstore", "used", true)); requiredTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "amazon", "new", true)); requiredTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "amazon", "used", true)); var allTotalRow = tfoot.insertRow(-1); allTotalRow.appendChild(this.createTdWithText("Total price for all books", "BookTotal")).colSpan = 2; allTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "bookstore", "new", false)); allTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "bookstore", "used", false)); allTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "amazon", "new", false)); allTotalRow.appendChild(this.createTotalPriceTd(totalPriceTracker, "amazon", "used", false)); /* Footnote iff there are incomplete totals */ if (totalPriceTracker.hasIncompleteTotals()) { var row = tfoot.insertRow(-1); var td = this.createTdWithText("* This total does not reflect all books for this course.", "BookPriceIncomplete"); td.colSpan = "6"; row.appendChild(td); } } BookTableView.prototype.createPriceCell = function (link, price) { var text = price ? Util_formatPrice(price) : "Lookup"; var td = this.createElement("td", "BookPrice"); if (link) { var anchor = td.appendChild(this.createElement("a", "")); anchor.href = link; anchor.appendChild(document.createTextNode(text)); } else { td.innerHTML = "—" // td.appendChild(document.createTextNode('')); } return (td); } BookTableView.prototype.renderEmpty = function() { var emptyText = this.forMultipleCourses ? "Either your courses have no textbooks, or we were unable to " + "get textbook information for the courses you've selected." : "Either this course has no textbooks, or we were unable to " + "get textbook information for this course."; this.container = this.elt.appendChild(this.createElement("div", "BookTableContainer")); this.container.appendChild(document.createTextNode(emptyText)); } BookTableView.prototype.hasBooks = function() { for (var id in this.courses) if (this.courses[id].semesterText == this.cursem && this.courses[id].books.length > 0) return true; return false; } BookTableView.prototype.createTdWithText = function (text, cssClass) { var td = this.createElement("td", cssClass); td.appendChild(document.createTextNode(text)); return td; } BookTableView.prototype.createThWithText = function (text, cssClass) { var th = this.createElement("th", cssClass); th.appendChild(document.createTextNode(text)); return th; } BookTableView.prototype.createElement = function (tag, cssClass) { var elt = document.createElement(tag); elt.className = cssClass; return elt; } BookTableView.prototype.createLink = function (href, text) { var link = document.createElement("a"); link.href = href; link.appendChild(document.createTextNode(text)); return link; } BookTableView.prototype.createTotalPriceTd = function (totalTracker, bookstoreOrAmazon, newOrUsed, requiredOnly) { var total = totalTracker.getTotalPrice(bookstoreOrAmazon, newOrUsed, requiredOnly); var isIncomplete = totalTracker.isTotalIncomplete(bookstoreOrAmazon, newOrUsed, requiredOnly); if (isIncomplete) { return this.createTdWithText(Util_formatPrice(total) + "*", "BookTotal BookPrice BookPriceIncomplete"); } else { return this.createTdWithText(Util_formatPrice(total), "BookTotal BookPrice"); } } /** * TotalBookPriceTracker * Used by BookTableView to store and access the total cost of books. */ function TotalBookPriceTracker() { /* There are 8 totals: { bookstore, amazon } X { used, new } X { required, not required } */ this.totals = [ 0, 0, 0, 0, 0, 0, 0, 0 ]; this.totalIncomplete = [ false, false, false, false, false, false, false, false ]; } TotalBookPriceTracker.prototype.addBook = function(book) { this.addPrice("bookstore", "new", book.required, book.priceBookstoreNew); this.addPrice("bookstore", "used", book.required, book.priceBookstoreUsed); this.addPrice("amazon", "new", book.required, book.priceAmazonNew); this.addPrice("amazon", "used", book.required, book.priceAmazonUsed); } TotalBookPriceTracker.prototype.getTotalPrice = function(bookstoreOrAmazon, newOrUsed, requiredOnly) { var result = this.totals[this.getTotalIndex(bookstoreOrAmazon, newOrUsed, true)]; if (!requiredOnly) result += this.totals[this.getTotalIndex(bookstoreOrAmazon, newOrUsed, "")]; return result; } TotalBookPriceTracker.prototype.isTotalIncomplete = function(bookstoreOrAmazon, newOrUsed, requiredOnly) { var result = this.totalIncomplete[this.getTotalIndex(bookstoreOrAmazon, newOrUsed, true)]; if (!requiredOnly) result = result || this.totalIncomplete[this.getTotalIndex(bookstoreOrAmazon, newOrUsed, "")]; return result; } TotalBookPriceTracker.prototype.hasIncompleteTotals = function() { for (var ii in this.totalIncomplete) if (this.totalIncomplete[ii]) return true; return false; } TotalBookPriceTracker.prototype.getTotalIndex = function(bookstoreOrAmazon, newOrUsed, required) { var idx = 0; if (bookstoreOrAmazon == "amazon") idx += 4; if (newOrUsed == "used") idx += 2; if (required) idx += 1; return idx; } TotalBookPriceTracker.prototype.addPrice = function(bookstoreOrAmazon, newOrUsed, required, price) { var idx = this.getTotalIndex(bookstoreOrAmazon, newOrUsed, required); if (!price) this.totalIncomplete[idx] = true; else this.totals[idx] += price; } /** * Button view * This view controls the "Add to cart" or "(in cart)" buttons on the course * pages. There's one instance of this view for all of the courses' buttons. */ function ButtonView() { this.courses = new Hashtable(); } ButtonView.prototype.addCourses = function (courses) { for (var ii in courses) { var course = courses[ii]; this.courses.put(course.getID(), course); this.enable(course.getID(), false); } } ButtonView.prototype.removeCourse = function (id) { this.courses.remove(id); this.enable(id, true); } ButtonView.prototype.clear = function () { var lst = this.courses.keys(); for (var ii = 0; ii < lst.length; ++ii) { this.removeCourse(lst[ii]); } } ButtonView.prototype.enable = function(id, doEnable) { var buttonElt = document.getElementById('addButton'+id); var addedElt = document.getElementById('addedIcon'+id); if (buttonElt) buttonElt.style.display = doEnable ? "block" : "none"; if (addedElt) addedElt.style.display = !doEnable ? "block" : "none"; } ButtonView.prototype.rerender = function () {} ButtonView.prototype.changeSemester = function (_) {} /* * Semester selector module. * The semester selector is the little drop-down box that allows you to change * which semester's courses you are currently viewing. It needs to appear only * when there is more than one semester in the set of semesters of courses * currently in the cart (union) the current semester. * * Because it also appears under the same conditions, the 'ExtraCartStuff' div * is controlled by this view as well. * * The important part of the implementation is that there is a hashtable mapping * (semester string name) to (count of courses in this semester). This allows * us to easily update the hash table when we add and remove courses. */ function SelectorView(elt, cursem, extra) { this.parentDiv = elt; this.currentSemester = cursem; this.semesters = new Hashtable(); this.courseIdToSemester = new Hashtable(); this.rerender(); this.extraStuffDiv = extra; } SelectorView.prototype.addCourses = function (courses) { for (var ii in courses) { var course = courses[ii]; var count = this.semesters.get(course.semesterText); if (!count) count = 0; this.semesters.put(course.semesterText, count+1); this.courseIdToSemester.put(course.getID(), course.semesterText); } this.rerender(); } SelectorView.prototype.removeCourse = function (id) { var semester = this.courseIdToSemester.get(id); var count = this.semesters.get(semester); if (count == 1) { this.semesters.remove(semester); if (this.semesters.keys().length > 0) controller.changeSemester(this.semesters.keys()[0]); } else this.semesters.put(semester, count-1); this.rerender(); } SelectorView.prototype.clear = function () { this.currentSemester = null; this.semesters = new Hashtable(); this.rerender(); } SelectorView.prototype.rerender = function () { /* Always remove the selector, if it exists. */ while (this.parentDiv.childNodes.length > 0) { this.parentDiv.removeChild(this.parentDiv.childNodes[0]) } /* Generate and add the selector if necessary. */ if (this.semesters.size() > 1 || (this.semesters.size() == 1 && !(this.semesters.keys()[0] == this.currentSemester))) { var selector = document.createElement('select'); var semesters = this.semesters.keys(); var sindex; /* we set the selectedIndex later because only that works in IE */ for (var ii = 0; ii < semesters.length; ++ii) { var name = semesters[ii]; var option = document.createElement('option'); option.text = name; option.value = name; if (name == this.currentSemester) sindex = ii; Util_addOptionToSelect(selector, option); } selector.selectedIndex = sindex; Util_addEvent(selector, "change", function (arg) { var select = arg.currentTarget || arg.srcElement; /* For IE */ var text = select.options[select.selectedIndex].value; controller.changeSemester(text); }); this.parentDiv.style.whiteSpace = "nowrap"; this.parentDiv.appendChild(document.createTextNode('Viewing semester ')); this.parentDiv.appendChild(selector); if (this.extraStuffDiv) this.extraStuffDiv.style.display = 'block'; } else if (this.semesters.size() == 1) { this.parentDiv.style.whiteSpace = "nowrap"; this.parentDiv.appendChild(document.createTextNode( 'Viewing semester ' + (this.semesters.keys()[0]))); if (this.extraStuffDiv) this.extraStuffDiv.style.display = 'block'; } else { this.parentDiv.style.whiteSpace = "normal"; this.parentDiv.appendChild(document.createTextNode( ' Cart is empty. ')); if (this.extraStuffDiv) this.extraStuffDiv.style.display = 'none'; } } SelectorView.prototype.changeSemester = function (text) { this.currentSemester = text; this.rerender(); } /** * IcsLink view * This view adjusts the semester passed in the ExportCalendar link. */ function IcsLinkView(domNode, sem) { this.elt = domNode; this.changeSemester(sem); } IcsLinkView.prototype.addCourses = function (courses) {} IcsLinkView.prototype.removeCourse = function (id) {} IcsLinkView.prototype.clear = function () {} IcsLinkView.prototype.rerender = function (_) {} IcsLinkView.prototype.changeSemester = function (sem) { this.elt.href="dataService/ExportCalendar?semester="+sem; }