Skip to content

Instantly share code, notes, and snippets.

@bcoe
Created November 11, 2010 19:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bcoe/673013 to your computer and use it in GitHub Desktop.
Save bcoe/673013 to your computer and use it in GitHub Desktop.
/**
* The calendar widget.
*
* @param {jQuery object} $element an element to attach the calendar object to.
* @param {Date} dateObject the date of the first of the month.
* @param {Schedgy object} a reference to the parent schedgy object.
* @retrun calendar is initialized and attached to the element provided.
*/
var Calendar = Class.extend({
days: {}, // Lookup a day in the calendar based on a key representing year/month/day.
monthName: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
],
init: function($calendar, dateObject, schedgy) {
var self = this;
this.$calendar = $calendar;
this.dateObject = dateObject;
this.schedgy = schedgy;
// Set the actual date string on this calendar.
$('#schedgy-month').html(this.monthName[this.dateObject.getMonth()] + ' ' + this.dateObject.getFullYear());
// Create the day objects and add them into the table.
var days = $calendar.find('td');
var daysOffset = dateObject.getDay();
for (var day = 0;day < this._getDaysInMonth();day++) {
$day = $(days[day + daysOffset]);
var daysKey = (new Date(this.dateObject.getFullYear(), this.dateObject.getMonth(), (day + 1))).toString();
// Create a new Day object and add it into a lookup table in the calendar.
this.days[daysKey] = new Day({
day: $day,
dayOfMonth: (day + 1),
calendar: this,
schedgy: this.schedgy
});
}
// Load day's data from server.
this._loadMonthDays();
},
// Calls the Schedgy server and loads in data for each day.
_loadMonthDays: function() {
var self = this;
var action = '/list_days'; // The rails action.
$.ajax({
url: this.schedgy.controller + action,
dataType: 'json',
data: {
time: this.dateObject.getTime() / 1000, // Ruby takes seconds.
},
type: 'POST',
success: function (data) {
if (data.error) {
alert(data.error);
} else {
// Load in each us
for (var key in data) {
var dayData = data[key];
var day = self.days[(new Date(dayData.day)).toString()]; // Lookup day based on date key.
if (day) { // Make sure that this day exists in this month.
// Create a day restriction object and attach it to the day.
day.dayRequirements = new DayRequirements({
schedgy: self.schedgy,
day: day,
required_roles: dayData.required_roles,
restricted_roles: dayData.restricted_roles,
required_user_count: dayData.required_user_count
});
day.updateCounter(); // Update day widget with the new restrictions.
// Now add the users to the day.
for (var userKey in dayData.assigned_users) {
var userPayload = dayData.assigned_users[userKey];
var user = self.schedgy.getUser(userPayload.email);
if (day.currentUsers < day.requiredUsers) {
day.addUser(user.$user, dayData.assigned_users[userKey].tags);
} else {
alert('There are already enough users on this day.');
}
}
} // Otherwise this day must not be in this month.
}
}
}
});
},
// Helper function to return the count of the days in this month.
_getDaysInMonth: function() {
return 32 - new Date(this.dateObject.getFullYear(), this.dateObject.getMonth(), 32).getDate();
}
});
/**
* A day within the calendar.
*
* @param {jQuery object} params.day an object representing the td element to attach the day to.
* @param {int} params.dayOfMonth the day of the month that this day falls on.
* @param {Calendar object} params.calendar the parent calendar that contains this day.
* @param {Schedgy object} params.schedgy the parent schedgy object.
*/
var Day = Class.extend({
init: function(params) {
var self = this;
this.users = {};
this.$day = params.day;
this.dayOfMonth = params.dayOfMonth;
this.calendar = params.calendar;
this.schedgy = params.schedgy;
this.requiredUsers = 5;
this.currentUsers = 0;
// Highlight the day if it is today.
var dateObject = new Date();
if (
dateObject.getDate() == this.dayOfMonth &&
dateObject.getMonth() == this.calendar.dateObject.getMonth()
) {
$day.addClass('today');
}
this.dayRequirements = new DayRequirements({
schedgy: this.schedgy,
day: this,
required_roles: [],
restricted_roles: [],
required_user_count: 5
});
// Build the DOM element for the day.
var $div = $('<div class="number">' + this.dayOfMonth + '</div>');
this.$list = $('<ul></ul>');
this.$counter = $('<div class="bottom"></div>')
this.$list.droppable({
drop: function(event, ui) {
var $this = $(this);
var $element = ui.draggable;
var action = '/add_user_to_day'
var user = $element.data('class');
// We may have deleted this element in another asynchronous call.
if (!user) {
return true;
}
// Make sure we can add more users.
if (self.currentUsers < self.requiredUsers) {
$.ajax({
url: self.schedgy.controller + action,
dataType: 'json',
data: {
time: (new Date(self.calendar.dateObject.getFullYear(), self.calendar.dateObject.getMonth(), self.dayOfMonth)).getTime() / 1000, // Ruby takes seconds.
email: user.email
},
type: 'POST',
success: function (data) {
if (data.error) {
alert(data.error);
} else {
// If this is a user being dragged and dropped form another
// day we must remove them from the original day if everything
// else goes successfully.
if ($element.data('originalDay')) {
// Fetch the data from the element being dropped.
var $userElement = $element.data('$userElement');
var originalUser = $element.data('originalUser');
var originalDay = $element.data('originalDay');
// Actually remove the original user.
originalDay.removeUser(originalUser, $element);
$element = $userElement;
}
self.addUser($element);
}
}
});
} else {
alert('There are already enough users on this day.');
}
}
});
this.$day.append($div);
this.$day.append(this.$list);
this.$day.append(this.$counter);
this.updateCounter();
},
updateCounter: function() {
var self = this;
// Fetch the current restriction/requirement information from dayRequirements
// and display it.
var _clickCallback = function(){
// Remove the click event.
var $element = $(this).parent();
self.dayRequirements.show();
return false;
}
this.$counter.html('');
this.$counter.append(this.dayRequirements.getUserWidget().click(_clickCallback));
},
getUser: function(email) {
return this.users[email];
},
addTagToUser: function(user, tag) {
var self = this;
var action = '/add_tag_to_user';
$.ajax({
url: self.schedgy.controller + action,
dataType: 'json',
data: {
time: (new Date(self.calendar.dateObject.getFullYear(), self.calendar.dateObject.getMonth(), self.dayOfMonth)).getTime() / 1000, // Ruby takes seconds.
email: user.email,
tag: tag
},
type: 'POST',
success: function (data) {
if (data.error) {
alert(data.error);
} else {
// Currently we do nothing.
}
}
});
},
removeTagFromUser: function(user, tag) {
var self = this;
var action = '/remove_tag_from_user';
$.ajax({
url: self.schedgy.controller + action,
dataType: 'json',
data: {
time: (new Date(self.calendar.dateObject.getFullYear(), self.calendar.dateObject.getMonth(), self.dayOfMonth)).getTime() / 1000, // Ruby takes seconds.
email: user.email,
tag: tag
},
type: 'POST',
success: function (data) {
if (data.error) {
alert(data.error);
} else {
// Currently we do nothing.
}
}
});
},
addTagsToUserElement: function($user, tags) {
if (tags) {
for (var i = 0;i < tags.length;i++) {
var tag = tags[i];
$user.prepend('<img src="images/tags/' + tag + '.png" alt="' + tag +'" />');
}
}
},
addUser: function($element, tags) {
var self = this;
// Add to list of users on this day (if jquery call returns successfully)
var user = $element.data('class');
// Update the day requirements object and the
// counter widget.
this.dayRequirements.addUser(user);
this.updateCounter()
// We may have removed this element in another operation and user won't exist.
if (!user) {
return;
}
this.users[user.email] = user;
var template = new jsontemplate.Template($('#template-user-calendar').html());
var $user = $(template.expand({
first_name: user.first_name,
last_name: user.last_name,
last_name_initials: user.last_name.length <= 3 ? user.last_name : user.last_name.substring(0, 3) + '...'
}));
$user.data('class', user);
// Add tags to the user element.
this.addTagsToUserElement($user, tags);
// Allow a user to be removed.
$user.click(function (event) {
// Remove any menus currently open.
$('.user-menu').remove();
var $user = $(this);
var user = $user.data('class');
// Create user pop-up.
var $userMenu = $('<div class="user-menu" />');
$userMenu.css({position: 'absolute', top: '-28px', left: '113px'});
var $ul = $('<ul />');
// Menu for tagging a user on a given day is built from the
// taging options returned by the server.
$.each(self.schedgy.tags, function() {
$ul.append('<li><img src="images/tags/' + this + '.png" class="user-menu-' + this + '" alt="' + this + '" /><a href="#" class="user-menu-' + this + '">' + this + '</a></li>');
});
$ul.append('<li><img src="images/icons/cross.png" class="user-menu-remove" alt="Remove" /><a href="#" class="user-menu-remove">Remove</a></li>');
$userMenu.append($ul);
$user.append($userMenu);
// Add the remove event.
$userMenu.find('.user-menu-remove').click(function(event) {
self.removeUser(user, $user);
return false;
});
// Attach click events for adding and removing tags from a user.
$.each(self.schedgy.tags, function() {
var tag = this;
var $link = $userMenu.find('a.user-menu-' + tag);
$link.data('class', user); // Attach the user object to the link for easy lookup.
$link.data('tag', '' + tag);
$link.click(function(event) {
var user = $(this).data('class');
var tag = $(this).data('tag');
// Close the menu if it's open.
$('.user-menu').remove();
$imagesOnUser = $user.find('img[alt=' + tag + ']');
if (!$imagesOnUser.length) {
$user.prepend('<img src="images/tags/' + tag + '.png" alt="' + tag +'" />');
// Make an AJAX call out to add the tag.
self.addTagToUser(user, tag);
} else {
// Make an AJAX call to remove a tag.
self.removeTagFromUser(user, tag);
$user.find('img[alt=' + tag + ']').remove();
}
return false;
});
});
$userMenu.click(function(event) {
event.stopPropagation();
});
event.stopPropagation();
});
$(document).click(function(event) {
// Remove any menus currently open.
$('.user-menu').remove();
});
// Store this extra information in the cloned user element
// when dropping this element we remove the element from the day
// it originated on.
$user.data('$userElement', $element);
$user.data('originalUser', user);
$user.data('originalDay', this);
$user.draggable({
helper: 'clone'
});
this.$list.append($user);
},
removeUser: function(user, $user) {
var self = this;
var action = '/remove_user_from_day'
// Update the day requirements object and the
// counter widget.
this.dayRequirements.removeUser(user);
this.updateCounter()
$.ajax({
url: this.schedgy.controller + action,
dataType: 'json',
data: {
time: (new Date(this.calendar.dateObject.getFullYear(), this.calendar.dateObject.getMonth(), this.dayOfMonth)).getTime() / 1000, // Ruby takes seconds.
email: user.email
},
type: 'POST',
success: function (data) {
if (data.error) {
alert(data.error);
} else {
// Close the menu if it's open.
$('.user-menu').remove();
$user.slideUp('normal', function() {
$(this).remove();
});
}
}
});
}
});
/**
* A single user of the Schedgy system.
* @param {string} first_name
* @param {string} last_name
* @param {string} email
* @param {jQuery Object} the user list jQuery element for appending this user to.
*/
var User = Class.extend({
init: function(params) {
this.first_name = params.first_name;
this.last_name = params.last_name;
this.role = params.roles[0] || 'any';
this.email = params.email;
this.md5 = MD5(this.email); // A Gravatar image key is just an MD5 encoded email.
this.$userList = params.$userList;
// Actually create the element and append it to the users list.
var template = new jsontemplate.Template($('#template-user-side').html());
// Try to render the template.
try {
this.$user = $(template.expand(this));
} catch (exception) {
alert(exception.message)
}
this.$user.data('class', this); // Store a reference to the underlying user object.
this.$user.draggable({
helper: 'clone'
});
this.$user.click(function() {
var data1 = $(this).data('class');
$('#schedgy-calendar td ul li').each(function(k, v) {
var data2 = $(v).data('class');
if (data1.first_name == data2.first_name && data1.last_name == data2.last_name) {
$(v).toggleClass('highlight');
}
});
});
params.$userList.append(this.$user); // Append the user badge to the #users element.
}
});
/**
* Handles the prompting the user for and generating restrictions
* and requirements on a day.
*/
var DayRequirements = Class.extend({
init: function(params) {
var self = this;
this.schedgy = params.schedgy;
this.day = params.day;
this.required_roles = params.required_roles;
this.restricted_roles = params.restricted_roles;
this.required_user_count = params.required_user_count;
// Initialize the boxes for storing user information
// for a given day.
this.users = {any: 0};
for (var key in this.schedgy.userRoles) {
this.users[this.schedgy.userRoles[key]] = 0;
}
// Actually create the element and append it to the users list.
var template = new jsontemplate.Template($('#requirements-dialog').html());
// Try to render the template.
try {
this.$dialog = $(template.expand(this));
// Display the dialog.
this.$dialog.dialog({
autoOpen: false,
width: 400
});
this.$dialog.find('.add-requirement').click(function() {
self.addRequirement(self.$dialog.find('.requirement-selects'));
return false;
});
this.$dialog.find('.remove-requirement').click(function() {
self.removeRequirement(self.$dialog.find('.requirement-selects'));
return false;
});
this.$dialog.find('.add-restriction').click(function() {
self.addRestriction(self.$dialog.find('.restriction-selects'));
return false;
});
this.$dialog.find('.remove-restriction').click(function() {
self.removeRestriction(self.$dialog.find('.restriction-selects'));
return false;
});
this.$dialog.find('.save-requirements').click(function() {
self.saveRequirements();
return false;
});
// Pre-populate the dialog with a sane set of required users.
for (var key in this.required_roles) {
var name = this.required_roles[key].name;
var count = this.required_roles[key].count;
for (var i = 0; i < count; i++) {
self.addRequirement(self.$dialog.find('.requirement-selects'), name);
}
}
for (var key in this.restricted_roles) {
var name = this.restricted_roles[key];
self.addRestriction(self.$dialog.find('.restriction-selects'), name);
}
for (var i=0; i < this.required_user_count; i++) {
self.addRequirement(self.$dialog.find('.requirement-selects'));
}
} catch (exception) {
alert(exception.message)
}
},
addRequirement: function($element, option) {
var self = this;
// Default the option value to 'any' if none is specified.
option = option || 'any';
// Add the default option to the selector..
var $select = $('<select></select>');
var $option = $('<option name="type" value="any">Any Type</option>');
$select.append($option);
// Output options for each user role, select the appropriate
// option if it is specifed.
for (var key in this.schedgy.userRoles) {
$option = $('<option name="type"></option>');
$option.attr('value', this.schedgy.userRoles[key])
$option.html(this.schedgy.userRoles[key]);
if (this.schedgy.userRoles[key] == option) {
$option.attr('selected', 'selected')
}
$select.append($option);
}
// Save reference to this element's initial type,
// used during validation.
$select.data('type', option);
$select.change(function() {
$select = $(this);
var message = self.validate($select.val());
if (message) {
alert(message);
$select.val($select.data('type'));
} else {
$select.data('type', $select.val());
}
});
$element.append($select);
},
// Don't a allow requirements to be dropped below the
// current number of users in a box.
validate: function(val) {
var message = false;
var sumRequirements = this.sumRequirements();
for (var key in this.users) {
var sum = sumRequirements[key];
if (sumRequirements[key] == undefined) {
sum = 0;
}
if (this.users[key] > sum) {
message = "You must remove users of the role '" + key + "' to perform this action.";
}
}
this.$dialog.find('.restriction-selects select').each(function() {
var restriction = $(this).val();
if (restriction == val) {
message = "You must remove the restriction of the type '" + val + "' to perform this operation."
}
});
if (message) {
return message;
}
return false;
},
addRestriction: function($element, option) {
var self = this;
var $select = $('<select><option name="type" value="[Choose One]">[Choose One]</option></select>');
$select.data('type', $select.val());
for (var key in this.schedgy.userRoles) {
var $option = $('<option name="type"></option>');
$option.attr('value', this.schedgy.userRoles[key])
$option.html(this.schedgy.userRoles[key]);
if (option == this.schedgy.userRoles[key]) {
$option.attr('selected', 'selected')
}
$select.append($option);
}
// Don't let a user set a restriction for a user type already assigned
// to this day.
$select.change(function() {
var $select = $(this);
if (self.users[$select.val()] > 0) {
alert("Cannot add restriction of this type, remove users of type '" + $select.val() + "'");
$select.val($select.data('type'));
} else {
$select.data('type', $select.val());
}
});
$element.append($select);
},
removeRequirement: function($element) {
var $select = $element.find('select:last');
// Don't allow requirements to be dropped below the current
// number of users in a given box.
var sumRequirements = this.sumRequirements();
if (this.users[$select.data('type')] > (sumRequirements[$select.data('type')] - 1)) {
alert("You must remove users of the role '" + $select.data('type') + "' before performing this action.");
} else {
$select.remove();
}
},
removeRestriction: function($element) {
$selects = $element.find('select:last').remove();
},
sumRequirements: function() {
var sumRequirements = {}; // Used to sum the number of users needed for each requirement type.
this.$dialog.find('.requirement-selects select option:selected').each(function() {
$select = $(this);
if (sumRequirements[$select.val()] == undefined) {
sumRequirements[$select.val()] = 1;
} else {
sumRequirements[$select.val()] += 1;
}
});
return sumRequirements;
},
saveRequirements: function($element) {
// Extract the requirements from the form.
var self = this;
self.payload = {
time: (new Date(self.day.calendar.dateObject.getFullYear(), self.day.calendar.dateObject.getMonth(), self.day.dayOfMonth)).getTime() / 1000, // Ruby takes seconds.
};
var action = '/set_day_requirements_and_restrictions'
// Grab required user settings from the pop-up.
var sumRequirements = this.sumRequirements();
// Copy the values into the payload array.
for (var key in sumRequirements) {
self.payload['requirements[' + key + ']'] = sumRequirements[key];
}
// Grab user restrictions from the pop-up.
var restrictionCounter = 0;
this.$dialog.find('.restriction-selects select').each(function() {
$select = $(this);
self.payload['restrictions[' + restrictionCounter + ']'] = $select.val();
restrictionCounter++;
});
$.ajax({
url: this.schedgy.controller + action,
dataType: 'json',
data: self.payload,
type: 'POST',
success: function (data) {
if (data.error) {
alert(data.error);
}
self.hide();
}
});
},
show: function() {
this.$dialog.dialog('open');
},
hide: function() {
this.$dialog.dialog('close');
this.day.updateCounter();
},
addUser: function(user) {
var role = user.role || 'any';
var requirements = this.sumRequirements();
if (this.users[role] == requirements[role] || requirements[role] == undefined) {
role = 'any';
}
this.users[role]++;
},
removeUser: function(user) {
var role = user.role || 'any';
var requirements = this.sumRequirements();
if (this.users[role] == 0 || requirements[role] == undefined) {
role = 'any';
}
this.users[role]--;
},
getUserWidget: function() {
var $userWidget = $('<span></span>');
var requirements = this.sumRequirements();
var empty = true;
for (var key in this.users) {
if (requirements[key]) {
empty = false;
$a = $('<a href="#" style="text-decoration: none;font-size: 12px;"></a>');
var imageHTML = '<img src="images/user_types/' + key + '.png" alt="Anyone" style="width: 12px;height: 12px;" />';
$a.html(imageHTML + this.users[key] + '/' + requirements[key] + '&nbsp;');
$userWidget.append($a);
}
}
if (empty) {
$a = $('<a href="#" style="text-decoration: none;font-size: 9px;"></a>');
$a.html('Click Me.');
$userWidget.append($a);
}
return $userWidget;
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment