/* * controller.js: model-view-controller core * authors: Adam Cath (acath) and Dave Pacheco (dap) * (both email addresses @cs.brown.edu) * * Copyright 2006 by the Mocha team. All rights reserved. * * Email authors for permission to use. With enough (any?) demand, we might * release this under a BSD license. */ /* * The model view controller is an architecture for designing programs with user * interfaces. Look it up for more information. */ /* * Data model module. * The data model is primarily responsible for persistence of the cart data * (via a browser cookie). */ function Model() { this.courses = new Hashtable(); } Model.prototype.getCourses = function () { return this.courses.values(); } Model.prototype.addCourses = function (courses) { for (var ii in courses) { this.courses.put(courses[ii].getID(), courses[ii]); } } Model.prototype.removeCourse = function (id) { this.courses.remove(id); } Model.prototype.clear = function () { this.courses = new Hashtable(); } /* * Course layout (all Strings unless mentioned) * number: course number (including section suffix) * uid: Identifies a particular section in a particular semeseter * courseRegistrationNumber: Banner CRN (students use this to register) * semester: Mocha semester id * semesterText: text to desplay for the semester (e.g. "F 06") * title: course title * displayTime: time as it should be displayed (full time string) * shortDisplayTime: hour of the course (e.g., "H"), or "..." * realTime: time in the representation "X/Y/Z", where X is * the day (0-4 representing Mon-Fri), Y is the * starting time, and Z is the ending time (both * in minutes-past-midnight). * examInfo: exam info string * professor: name of the professor * searchNumber: what we should search for to get the link for this course * books: array of books for this course. See layout below * color: not initialized--use a coloriterator */ function Course(initializer) { for(var key in initializer) { this[key] = initializer[key]; } } Course.prototype.getID = function() { return this.uid; } /* * Controller module. * The controller is responsible for dispatching model changes to the views and * modifying the model on behalf of the views. */ var COLORS = [ /* lighter colors (stand out better against red conflicts) */ "#FF9999", "#FFFF66", "#CCFF99", "#33FF99", "#99FFCC", "#99FFFF", "#99CCFF", "#CCCCFF", "#CC99FF", "#FF99FF" ]; var COOKIE_NAME="MochaCart"; var COOKIE_VERSION="2.5"; var SEMESTER_COOKIE_NAME = "MochaSemester"; /* * The Controller is basically used as a singleton, but uses the object-oriented * model for consistency. */ var controller; /* used only for debugging. */ function Controller() { controller = this; this.model = new Model(); var smallCalendar = document.getElementById('SmallScheduleBody'); var bigCalendar = document.getElementById('BigScheduleBody'); var smallAgenda = document.getElementById('SmallAgenda'); var bigAgenda = document.getElementById('BigAgenda'); var examTable = document.getElementById('ExamTable'); var bookTable = document.getElementById('BookTable'); var smallSelector = document.getElementById('SmallSemesterSelector'); var bigSelector = document.getElementById('BigSemesterSelector'); var icsLink = document.getElementById('DownloadIcsLink'); var sem = this.currentSemester = getCookie(SEMESTER_COOKIE_NAME); this.views = [new ButtonView()]; if (smallSelector) this.views.push(new SelectorView(smallSelector, sem, document.getElementById('ExtraCartStuff'))); if (bigSelector) this.views.push(new SelectorView(bigSelector, sem)); if (smallCalendar) this.views.push(new CalView(smallCalendar, false, sem)); if (bigCalendar) this.views.push(new CalView(bigCalendar, true, sem)); if (smallAgenda) this.views.push(new CourseTableView(CTV_SMALL_AGENDA_COLS, smallAgenda, sem)); if (bigAgenda) this.views.push(new CourseTableView(CTV_BIG_AGENDA_COLS, bigAgenda, sem)); if (examTable) this.views.push(new CourseTableView(CTV_EXAM_COLS, examTable, sem)); if (bookTable) this.views.push(new BookTableView(bookTable, sem, true)); if (icsLink) this.views.push(new IcsLinkView(icsLink, sem)); /* * We have a colorIterator for each semester now. */ var getColorIterator = function() { return new CircularIterator(COLORS); } this.colorIterators = new DefaultHashtable(getColorIterator); /* Ugh. This is the only way I know to generate a proper closure in JS. */ var rerenderer_generator = function (obj) { return function (_) { if (!obj.ignoreResizeEvents) obj.rerender(); }; } this.ignoreResizeEvents = false; Util_addEvent(window, 'resize', rerenderer_generator(this)); } /* * Calls rerender on each of the views. */ Controller.prototype.rerender = function () { for (var ii = 0; ii < this.views.length; ++ii) { this.views[ii].rerender(); } } /* * Add courses to the model and views, but don't update persistant storage * (the cookie). This is used for initialization and refresh(). * * @param courses An array of Objects with all the properties of a course; * they don't actually have to be of type Course. */ Controller.prototype.addCourses = function(courses) { /* Assign colors to each course */ for (var ii in courses) { var course = courses[ii] = new Course(courses[ii]); course.color = this.colorIterators.get(course.semester).next() if (!this.currentSemester) { this.changeSemester(course.semesterText) } } this.model.addCourses(courses); for (var ii = 0; ii < this.views.length; ++ii) { this.views[ii].addCourses(courses); } } /* * Add courses to the cart and update persistant storage. * @param courses An array of Objects with all the properties of a course; * they don't actually have to be of type Course. */ Controller.prototype.addCoursesAndSave = function(courses) { if (courses.length > 0) { this.changeSemester(courses[0].semesterText); this.addCourses(courses); var cookieCourseIDs = this.getCoursesInCookie(); var newCourseIDs = Util_map(function (c) { return (new Course(c)).getID() }, courses); this.setCoursesInCookie(Util_setUnion(cookieCourseIDs, newCourseIDs)); } this.refresh(); } /* * Removes a course from the cart by ID, but do not remove it from persistant * storage (the cookie). This is used by refresh(). * * @param id of course to remove */ Controller.prototype.removeCourse = function (id) { this.model.removeCourse(id); for (var ii = 0; ii < this.views.length; ++ii) { this.views[ii].removeCourse(id); } } /* * Remove a course to the cart and update persistant storage. * * @param id of course to remove */ Controller.prototype.removeCourseAndSave = function(id) { this.removeCourse(id); this.setCoursesInCookie(Util_setDifference(this.getCoursesInCookie(), [id])); this.refresh(); } Controller.prototype.clearCart = function (doConfirm) { if (!doConfirm || confirm("Are you sure you want to clear your cart?" + " This removes all courses from all semesters.")) { this.setCoursesInCookie([]); this.model.clear(); for (var ii = 0; ii < this.views.length; ++ii) { this.views[ii].clear(); } } } Controller.prototype.addView = function (view) { this.views.push(view); var list = this.model.getCourses(); for (var ii = 0; ii < list.length; ++ii) { view.addCourse(list[ii]); } } Controller.prototype.changeSemester = function (semText) { this.currentSemester = semText; for (var ii = 0; ii < this.views.length; ++ii) { this.views[ii].changeSemester(this.currentSemester); } setCookie(SEMESTER_COOKIE_NAME, semText); } /* * Reads the cookie. If there are any courses in the Model that aren't in the * cookie, calls removeCourse() on them. If there are any courses in the cookie * that aren't in the Model, makes an AJAX request to get the course data and * calls addCourses(). */ Controller.prototype.refresh = function () { var cookieCourseIDs = this.getCoursesInCookie(); var currentCourseIDs = Util_map(function (c) { return c.getID() }, this.model.getCourses()); var addedCourseIDs = Util_setDifference(cookieCourseIDs, currentCourseIDs); var removedCourseIDs = Util_setDifference(currentCourseIDs, cookieCourseIDs); if (addedCourseIDs.length > 0) { var oldThis = this; var request = '/mocha/dataService/GetCourseData?courseIDs=["' + addedCourseIDs.join('","') + '"]'; ajaxRequest(request, function (courseDataJSON) { oldThis.addCourses(eval(courseDataJSON)); }); } for (var ii in removedCourseIDs) { this.removeCourse(removedCourseIDs[ii]); } } Controller.prototype.getCoursesInCookie = function() { var cookieText = getCookie(COOKIE_NAME); if (cookieText == null) return []; var parts = cookieText.split("/"); //var version = parts[0].split("-")[1]; var uidList = parts[1]; return (cookieText != null) ? Util_filter(function (x) { return x.length > 0 }, uidList.split(".")) : []; } Controller.prototype.setCoursesInCookie = function(courseIDs) { setCookie(COOKIE_NAME, "version-" + COOKIE_VERSION + "/" + courseIDs.join(".")); } /* * We need a way of turning off the resize events because IE sucks so much. * In particular, it appears to fire off window resize events while we're not * resizing the window (in fact, when we're READing the offsetLeft property * of an object) */ Controller.prototype.setIgnoreResizeEvents = function (doIgnore) { this.ignoreResizeEvents = doIgnore; }