Walks LLC, a provider of walking tours in Europe, are relaunching their web site. I built a calendar widget for clients to manage their itineraries visually.
Due to the complexity of the client side interactions required, I decided to use Backbone.js as an MVC Framework.
/* global namespace */
var walks = {
builder: {},
collections: {
bookings_details: [],
events: [],
events_domains_groups: [
// maps events to domain groups, aka locations
// also embedded by cake within events
],
locations: [
// DomainsGroup
],
stages: [
// EventsStagePaxRemaining
],
tags: []
},
constructors: {
backbone: {},
templates: {},
views: {}
},
data: {
// variable to pass CakePHP webroot across context
// set in View/Layouts/main
webroot: walksllc_webroot,
display_filters: {
week: {
first_date: function() {return new Date($(walks.selectors.calendar_range).attr('data-range-start'))},
last_date: function() {return new Date($(walks.selectors.calendar_range).attr('data-range-end'))}
},
events: {}
},
},
init_data: {},
selectors: {
locations: '.location-info tbody',
stages: '.stages',
events: '.events',
events_filters: '.events-filters',
calendar_range: '.calendar-range'
},
settings: {
builder_height: 480,
location_duration: 3,
test_client_id: 118,
test_mode: false,
url: {
ajax_url: function(this_url) {
return walks.settings.url.base + walks.settings.url.app + this_url;
},
app: 'elias/',
base: 'http://dev.walks.org/',
}
},
views: {
locations: [],
events: [],
stages: []
}
}
walks.builder = (function(){
/* Convenience functions */
Date.prototype.yyyymmdd = function() {
var yyyy = this.getFullYear().toString();
var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based
var dd = this.getDate().toString();
return Number(yyyy + (mm[1]?mm:"0"+mm[0]) + (dd[1]?dd:"0"+dd[0])); // padding
};
Date.prototype.prettyDate = function() {
var prettyDate =(this.getMonth()+1) + '/' + this.getDate() + '/' + this.getFullYear();
return prettyDate;
}
function toType (obj) {
return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
function capitaliseFirstLetter (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function logg (message) {
if (walks.settings.test_mode) {
console.log(message);
}
}
function initItineraryBuilder () {
initMVCTypes();
loadData();
initUI();
}
function initMVCTypes() {
initMVCLocations();
initMVCEvents();
initMVCStages();
initMVCBookingsDetails();
initMVCGeneric();
initTemplating();
}
function initTemplating () {
// TODO: pre-compile Handlebars walks.constructors.templates
walks.constructors.templates['events'] = Handlebars.compile($('#handlebars-event-template').html());
walks.constructors.templates['location-ui'] = Handlebars.compile($('#handlebars-location-ui-template').html());
walks.constructors.templates['stages'] = Handlebars.compile($('#handlebars-stages-template').html());
walks.constructors.templates['calendar-grid'] = Handlebars.compile($('#handlebars-calendar-grid-template').html());
walks.constructors.templates['calendar-hours-legend'] = Handlebars.compile($('#handlebars-calendar-hours-legend-template').html());
}
// custom MVC types
function initMVCLocations() {
walks.constructors.backbone.LocationModel = Backbone.Model.extend({
getCallbackStage: function () {
return walks.collections.stages.getByCid(this.get('callback_stage_id'));
}
});
walks.constructors.backbone.LocationsCollection = Backbone.Collection.extend({
model: walks.constructors.backbone.LocationModel,
initialize: function() {
this.on('add', function(location_model) {
mvcAddView(location_model, 'Location', 'locations');
});
this.on('change:start_date', function(m) {
if (m.has('start_date')) {
m.set({start_date_pretty: m.get('start_date').prettyDate()});
} else {
m.set({start_date_pretty: null});
}
});
this.on('change:end_date', function(m) {
if (m.has('end_date')) {
m.set({end_date_pretty: m.get('end_date').prettyDate()});
} else {
m.set({end_date_pretty: null});
}
});
},
listForSelect: function (target_location_id) {
var temp_collection = walks.collections.locations.map(function(c) {
var temp_id = c.get('id');
var temp_object;
temp_object = {
name: c.get('name'),
id: temp_id
}
if (target_location_id === temp_id) {
temp_object['selected'] = true;
}
return temp_object;
});
return temp_collection;
}
});
walks.collections.locations = new walks.constructors.backbone.LocationsCollection();
walks.constructors.backbone.LocationView = Backbone.View.extend({
tagName: 'tr',
className: 'Cant',
id: 'Stand',
attributes: 'It',
collection: walks.collections.locations,
events: {
'click .add-location': 'modifyLocationUI',
'click .location-cancel': 'cancelLocationUI',
'click .location-edit': 'editLocationUI'
},
initialize: function() {
this.model.on('change', this.render, this);
},
model: walks.constructors.backbone.LocationModel,
render: function(location_model, options) {
var temp_stage;
var temp_location = location_model;
var rendered_template;
temp_location['collection'] = this.collection.listForSelect(this.model.get('id'));
this.$el.addClass('location-ui');
if (this.model.get('edit_mode') === true) {
rendered_template = walks.constructors.templates['location-ui'](temp_location);
} else {
rendered_template = walks.constructors.templates['location-ui'](this.model);
}
this.$el.html(rendered_template);
$(walks.selectors.locations).append(this.$el);
if (temp_stage = this.getCallbackStage()) {
this.bindLocationUI(temp_stage);
}
toggleDrawer();
$('a[href="#Locations"]').click();
return this;
},
bindLocationUI: function(callback_stage) {
var to_date;
var from_date;
from_date = new Date(callback_stage.get('datetime'));
to_date = new Date(from_date);
to_date.setDate(to_date.getDate() + walks.settings.location_duration);
$( ".location-ui .from" ).datepicker({
defaultDate: from_date,
changeMonth: true,
numberOfMonths: 1,
onClose: function( selectedDate ) {
$( "#to" ).datepicker( "option", "minDate", selectedDate );
}
}).val(from_date.prettyDate());
$( ".location-ui .to" ).datepicker({
defaultDate: to_date,
changeMonth: true,
numberOfMonths: 1,
onClose: function( selectedDate ) {
$( "#from" ).datepicker( "option", "maxDate", selectedDate );
}
}).val(to_date.prettyDate());
},
cancelLocationUI: function() {
this.model.set({
callback_stage_id: null,
edit_mode: null,
end_date: null,
start_date: null
});
toggleDrawer(this.el, 'closed');
},
editLocationUI: function() {
this.model.set({edit_mode: true});
},
getCallbackStage: function() {
var temp_stage = this.model.getCallbackStage();
if (temp_stage) {
return temp_stage;
} else {
return false;
}
},
modifyLocationUI: function() {
var temp_stage;
var target_location_id = $('.location-select').val();
var target_location = walks.collections.locations.get(target_location_id);
target_location.set({
edit_mode: null,
end_date: new Date($('.location-ui .to').val()),
start_date: new Date($('.location-ui .from').val())
});
if (temp_stage = this.getCallbackStage()) {
temp_stage.addToItinerary();
}
this.paintBuilder();
},
paintBuilder: function() {
// on initial launch, model will only have default start_date
$('.column.location').removeClass('location');
walks.collections.locations.forEach(function(m) {
var temp_start_date = m.get('start_date');
var date_diff = (m.get('end_date') - temp_start_date)/86400000;
for (var i=0; i < date_diff; i++) {
$('[data-date="' + (temp_start_date.yyyymmdd() + i) + '"]').addClass('location');
}
});
}
});
}
function initMVCEvents () {
walks.constructors.backbone.EventModel = Backbone.Model.extend();
walks.constructors.backbone.EventsCollection = Backbone.Collection.extend({
model: walks.constructors.backbone.EventModel,
initialize: function() {
this.on('add', function(event_model) {
mvcAddView(event_model, 'Event', 'events');
});
}
});
walks.collections.events = new walks.constructors.backbone.EventsCollection;
walks.constructors.backbone.EventView = Backbone.View.extend({
events: {
'click a.show-availability': 'showAvailability'
},
initialize: function() {
this.model.on('change', this.render, this);
},
model: walks.constructors.backbone.EventModel,
render: function () {
var rendered_template = walks.constructors.templates['events'](this.model);
this.$el.addClass('span6');
this.$el.html(rendered_template);
$(walks.selectors.events).append(this.$el);
return this;
},
showAvailability: function(e) {
builderOpen();
$('.hide-availability').show();
walks.data.display_filters.events.id = this.model.get('id');
repaintBuilder();
}
});
}
function initMVCStages () {
walks.constructors.backbone.StageModel = Backbone.Model.extend({
initialize: function() {
this.on('change:itinerary', this.updateBookings, this),
this.set({js_date: new Date(this.get('datetime').replace(/-/g, '/'))});
},
addToItinerary: function() {
var temp_location = this.getLocation();
if (this.bookedLocation()) {
this.set({itinerary: true});
hideAvailability();
walks.collections.stages.suggestAnotherBooking(this);
return true;
} else {
temp_location.set({
callback_stage_id: this.cid,
edit_mode: true
});
return false;
}
},
// convenience function to set attributes dynamically
alterAttribute: function(this_attribute, attribute_value) {
this.attributes[this_attribute] = attribute_value;
},
bookedLocation: function() {
var this_location = this.getLocation();
if (!this_location.get('start_date') || !this_location.get('end_date')) {
return false;
} else {
return true;
}
},
initForDisplay: function() {
var this_date = this.get('js_date');
var this_event;
var this_duration;
var this_hours;
var this_minutes;
var this_end_date;
if (!this.has('js_end_date')) {
this_event = walks.collections.events.get(this.get('events_id'));
this_duration = this_event.get('duration');
this_hours = Math.floor(this_duration/60);
this_minutes = this_duration%60;
this_end_date = new Date(this_date);
this_end_date.setHours(this_date.getHours() + this_hours);
this_end_date.setMinutes(this_date.getMinutes() + this_minutes);
this.set({
event_name: this_event.get('name_long').replace(/-.*/, ''),
duration: this_duration,
js_end_date: this_end_date
});
}
},
getLocation: function() {
var this_location;
var temp_location_id;
var this_event_id = this.get('events_id');
temp_location_id = walks.collections.events_domains_groups.where({
event_id: this_event_id,
primary: true
})[0].get('group_id');
this_location = walks.collections.locations.get(temp_location_id);
return this_location;
},
updateBookings: function() {
if (walks.collections.bookings.length === 0) {
// create new booking
$.ajax({
url: walks.settings.url.ajax_url('Bookings/Add/new/ajax/'),
success: function (data) {
walks.data['current_booking'] = JSON.parse(data)['booking_id'];
},
error: function (data) {
// TODO: better error handling
this.model.set({itinerary: false});
}
});
} else {
// retrieve existing booking
}
if (this.get('itinerary') === true) {
walks.collections.bookings_details.add(this, {merge: true});
} else {
walks.collections.bookings_details.remove(this);
}
}
});
walks.constructors.backbone.StagesCollection = Backbone.Collection.extend({
comparator: function(stage_model) {
return stage_model.get('js_date');
},
initialize: function() {
this.on('add', function(stage_obj) {
mvcAddView(event_model, 'Stage', 'stages');
})
},
model: walks.constructors.backbone.StageModel,
isStageAvailable: function(test_stage) {
var overlapping_itinerary_p = true;
_.forEach(
walks.collections.stages.where({itinerary: true}),
function(itinerary_stage) {
// exclude stages that overlap with anything in itinerary
if (itinerary_stage.get('js_date') < test_stage.get('js_end_date') && itinerary_stage.get('js_end_date') > test_stage.get('js_date')) {
overlapping_itinerary_p = false;
}
}
);
return overlapping_itinerary_p;
},
nextAvailableStage: function(search_stage) {
return _.find(
walks.collections.stages.availableStages(search_stage),
function() {
// get first result
return true;
}
)
},
availableStages: function(start_stage) {
return _.filter(
walks.collections.stages.where({in_date_range: true}),
function(each_stage) {
if (each_stage.get('js_date') > start_stage.get('js_end_date')) {
return walks.collections.stages.isStageAvailable(each_stage);
}
}
);
},
suggestAnotherBooking: function(this_stage) {
// TODO: handle when nextAvailable returns null
this.nextAvailableStage(this_stage).set({'suggested': true});
}
});
walks.collections.stages = new walks.constructors.backbone.StagesCollection;
walks.constructors.backbone.StageView = Backbone.View.extend({
events: {
'click a.toggle-itinerary': 'toggleItinerary',
'click .stage-inner': 'stageClick'
},
initialize: function() {
this.model.on('change', function() {
this.render();
}, this);
},
model: walks.constructors.backbone.StageModel,
render: function () {
var this_date = this.model.get('js_date');
var this_events_id = this.model.get('events_id');
this.$el.addClass('stage');
var display_state_classes = ['itinerary', 'availability', 'suggested', 'in_date_range'];
for (var i=0; i<4; i++) {
if (this.model.get(display_state_classes[i])) {
this.$el.addClass(display_state_classes[i]);
} else {
this.$el.removeClass(display_state_classes[i]);
}
}
this.$el.attr('data-events-id', this_events_id);
this.$el.html(walks.constructors.templates['stages'](this.model));
var this_selector = [
walks.selectors.stages,
' [data-date="',
this_date.yyyymmdd(),
'"] ',
' [data-hour-id="',
this_date.getHours() +
'"]'
].join('');
$(this_selector).append(this.$el);
return this;
},
addToItinerary: function() {
this.model.addToItinerary();
},
removeFromItinerary: function () {
this.model.set('itinerary', false);
repaintBuilder();
},
stageClick: function () {
if (this.model.get('suggested') == true) {
filterEvents(
'id',
_.map(
walks.collections.stages.availableStages(this.model),
function(stage_model) {
return stage_model.get('events_id');
}
)
);
builderClose();
}
},
toggleItinerary: function (e) {
e.preventDefault();
e.stopPropagation();
if (this.model.get('itinerary') != true) {
this.addToItinerary();
} else {
this.removeFromItinerary();
}
}
});
}
function initMVCBookingsDetails () {
walks.constructors.backbone.BookingsDetailsModel = Backbone.Model.extend({
initialize: function() {
this.on('add', this.syncStage, this);
},
findStage: function() {
this.set({
stage_id: walks.collections.stages.where({
datetime: this.get('events_datetimes')
})[0].get('id')
});
},
markStage: function() {
walks.collections.stages.get(
this.get('stage_id')
).set({
itinerary: true
});
},
syncStage: function() {
this.findStage();
this.markStage();
}
});
walks.constructors.backbone.BookingsDetailsCollection = Backbone.Collection.extend({
initialize: function() {
this.on('reset', function() {
this.forEach(function(b) {
b.trigger('add');
});
}, this);
},
model: walks.constructors.backbone.BookingsDetailsModel
});
walks.collections.bookings_details = new walks.constructors.backbone.BookingsDetailsCollection;
}
// abstracted MVC functions
function mvcAddView (model_object, view_type, view_type_plural) {
if (!view_type_plural) {
view_type_plural = view_type.toLowerCase() + 's';
}
var temp_view = new walks.constructors.backbone[view_type + 'View']({model: model_object});
walks.views[view_type_plural].push(temp_view);
}
function mvcAddGenericDataType () {
var model_constructor_name = arguments[0][0];
var collection_constructor_name = arguments[0][1];
var collection_instance_name = arguments[0][2];
walks.constructors.backbone[model_constructor_name + 'Model'] = Backbone.Model.extend({});
walks.constructors.backbone[collection_constructor_name + 'Collection'] = Backbone.Collection.extend({
model: walks.constructors.backbone[model_constructor_name + 'Model']
});
walks.collections[collection_instance_name] = new walks.constructors.backbone[collection_constructor_name + 'Collection'];
}
function initMVCGeneric () {
var generic_mvcs = [
['Tag', 'Tags', 'tags'],
['DomainsGroup', 'DomainsGroups', 'domains_groups'],
['EventsDomainsGroup', 'EventsDomainsGroups', 'events_domains_groups'],
['Bookings', 'Booking', 'bookings']
]
for (var g=0; g < generic_mvcs.length; g++) {
mvcAddGenericDataType(generic_mvcs[g]);
}
}
// controls and presentation behavior
function builderToggle () {
if ($('.builder-content').hasClass('open')) {
builderClose();
} else {
builderOpen();
}
}
function builderOpen () {
if (!$('.builder-content').hasClass('open')) {
$('.builder-content').addClass('open').show();
builderAnimate('open');
}
}
function builderClose () {
$('.builder-content').removeClass('open').hide();
builderAnimate('close');
hideAvailability();
}
function builderAnimate (direction, height) {
var opacity;
var width;
if (direction === 'open') {
if (!height) {
height = walks.settings.builder_height;
}
width = 790;
} else if (direction === 'close') {
if (!height) {
height = 0;
width = 0;
}
}
$('.builder').animate({
width: width,
height: height,
opacity: opacity
});
}
function toggleDrawer (element, direction) {
if (!direction) {
direction = 'open';
}
if (direction === 'open') {
builderAnimate('open', 650);
} else {
builderAnimate('close', walks.settings.builder_height);
}
}
function bindControls () {
$('body').click(function () {
var $target_element = $(event.target);
if ($('.builder-content').hasClass('open')) {
if ($target_element.parents().index($('.builder')) == -1) {
if (!$target_element.hasClass('stop') && !$target_element.hasClass('prevent')) {
builderClose();
}
}
}
});
$('body').on('click', '.prevent', function(e) {
e.preventDefault();
}).on('click', '.stop', function(e) {
e.stopPropagation();
}).on('click', '.test', function(e) {
logg(e);
debugger;
});
$('body').on('click', 'a.repaint', function(e) {
repaintContent($(this).attr('href'));
});
$('.calendar-controls').on('click', '.prev', function() {
var date = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
date.setDate(date.getDate() - 7);
setCalendarRange(date);
repaintBuilder();
}).on('click', '.next', function() {
var date = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
date.setDate(date.getDate() + 7);
setCalendarRange(date);
repaintBuilder();
}).on('click', '.calendar-scale a.btn', function() {
$(this).button('toggle');
});
$('.events-filters').on('click', 'a', function() {
$('.events-filters a').removeClass('selected');
var temp_type = $(this).attr('data-filter-type');
var temp_value = $(this).attr('data-filter-value');
filterEvents(
temp_type,
temp_value
);
repaintBuilder();
$(this).addClass('selected');
});
$('.builder').on('click', '.builder-toggle', function() {
builderToggle();
}).on('click', '.hide-availability', function() {
hideAvailability();
}).on('click', '.book-all-test', function() {
walks.collections.stages.where({in_date_range: true}).forEach(function(a) {a.set({'itinerary': true})});
});
$('.itinerary-drawer .nav-tabs a').click(function (e) {
e.preventDefault();
$(this).tab('show');
toggleDrawer(this);
});
}
function loadData () {
var temp_object;
var temp_collection;
var internal_object_name;
var internal_collection_name;
for (var i in walks.init_data) {
logg(i);
switch (i) {
case 'BookingsDetail':
internal_object_name = 'BookingsDetail';
internal_collection_name = 'bookings_details';
break;
case 'EventsDomainsGroup':
internal_object_name = 'EventsDomainsGroup';
internal_collection_name = 'events_domains_groups';
break;
case 'EventsStagePaxRemaining':
internal_object_name = 'Stage';
internal_collection_name = 'stages';
break;
case 'DomainsGroup':
internal_object_name = 'Location';
internal_collection_name = 'locations';
break;
default:
internal_object_name = i;
internal_collection_name = i.toLowerCase() + 's';
}
// cake wraps json objects with the same name
// this section discards it for syntactical convenience
// the else statement below protects against it disappearing, but,
// TODO: if we ever alter the cake feed, this section could be removed entirely
temp_collection = _.map(walks.init_data[i], function (each_model) {
if (each_model[i]) {
temp_object = each_model[i];
for (var j in each_model) {
if (i != j) {
temp_object[j] = each_model[j];
}
}
} else {
temp_object = each_model[j];
}
return temp_object;
});
if (temp_collection[0] != undefined) {
walks.collections[internal_collection_name].reset(temp_collection).forEach(function (m) {
if ($.inArray(internal_object_name, ['Event', 'Stage', 'Location']) > -1) {
mvcAddView(m, internal_object_name, internal_collection_name);
}
});
}
}
filterEvents();
$('.events .spin-wheel').hide();
repaintBuilder();
}
function initUI () {
setCalendarRange();
initCalendarGrid();
bindControls();
addLocationFormToDrawer();
}
function addLocationFormToDrawer () {
var rendered_template = walks.constructors.templates['location-ui']({
edit_mode: true,
collection: function() { return walks.collections.locations.listForSelect(); }
});
$(walks.selectors.locations).append(rendered_template);
}
function initCalendarGrid () {
var rendered_template;
var grid_hours = new Array;
var grid_day;
var first_date_of_week = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
var grid_date = new Date;
for (var j=0; j<24; j++) {
grid_hours.push(j);
}
rendered_template = walks.constructors.templates['calendar-hours-legend']({hours: grid_hours});
$(walks.selectors.stages).append(rendered_template);
for (var i=0; i<7; i++) {
grid_date.setDate(first_date_of_week.getDate() + i);
grid_day = {
grid_date: grid_date.yyyymmdd(),
day_of_week: i,
hours: grid_hours
};
rendered_template = walks.constructors.templates['calendar-grid'](grid_day);
$(walks.selectors.stages).append(rendered_template);
}
setCalendarHeaders();
}
function setCalendarRange(date) {
var day_of_week;
var first_date_of_week = new Date();
var last_date_of_week;
var current_week_string;
if (!date) {
var date = new Date();
}
day_of_week = date.getDay();
if (day_of_week == 0) {
first_date_of_week = date;
} else {
first_date_of_week.setDate(date.getDate() - day_of_week);
}
$(walks.selectors.calendar_range).attr('data-range-start', first_date_of_week);
last_date_of_week = new Date(first_date_of_week);
last_date_of_week.setDate(first_date_of_week.getDate() + 7);
$(walks.selectors.calendar_range).attr('data-range-end', last_date_of_week);
current_week_string = (first_date_of_week.getMonth() + 1) + '/' + first_date_of_week.getDate() + ' - ' + (last_date_of_week.getMonth() + 1) + '/' + last_date_of_week.getDate();
$(walks.selectors.calendar_range).html(current_week_string);
setCalendarHeaders();
}
function setCalendarHeaders () {
for (var i=0; i<7; i++) {
var column_selector = '.day[data-day-id="' + i + '"]';
var first_date_of_week = new Date($(walks.selectors.calendar_range).attr('data-range-start'));
var each_date = new Date(first_date_of_week);
each_date.setDate(first_date_of_week.getDate() + i);
var date_string = (each_date.getMonth() + 1) + '/' + (each_date.getDate());
$(column_selector).attr('data-date', each_date.yyyymmdd());
$(column_selector + ' .date-header').html(date_string);
}
}
function filterEvents (filter_type, filter_value) {
$('.events .spin-wheel').show();
if (filter_value && toType(filter_value) != 'array') {
filter_value = [filter_value];
}
walks.collections.events.forEach(function(event_model, i) {
if (!filter_value) {
event_model.set({displayed: true});
} else {
event_model.set({displayed: false});
for (var i=0; i walks.data.display_filters.week.first_date() && stage_model.get('js_date') < walks.data.display_filters.week.last_date()) {
stage_model.initForDisplay();
stage_model.set({in_date_range: true});
// TODO: accept mulitple filters
if (stage_model.get('events_id') == walks.data.display_filters.events.id) {
stage_model.set({availability: walks.collections.stages.isStageAvailable(stage_model)});
}
}
});
}
function positionCalendarGrid () {
var filtered_stages = walks.collections.stages.where({availability: true});
var earliest_stage;
var offset_hours = 8;
var offset_px;
if (filtered_stages.length > 0) {
earliest_stage = _.min(
filtered_stages,
function(stage) {return stage.get('js_date').getHours()}
);
offset_hours = earliest_stage.get('js_date').getHours();
}
offset_px = offset_hours * -20;
if (offset_px < -140) {
offset_px = -140;
}
$('.hours').css({
top: offset_px + 'px',
});
}
function hideAvailability () {
walks.data.display_filters.events.id = null;
repaintBuilder();
$('.hide-availability').hide();
}
return {
initItineraryBuilder: initItineraryBuilder
}
})();
$(document).ready(function() {
var opts = {
lines: 12, // The number of lines to draw
length: 7, // The length of each line
width: 4, // The line thickness
radius: 10, // The radius of the inner circle
corners: 1, // Corner roundness (0..1)
rotate: 0, // The rotation offset
color: '#000', // #rgb or #rrggbb
speed: 1.8, // Rounds per second
trail: 60, // Afterglow percentage
shadow: false, // Whether to render a shadow
hwaccel: false, // Whether to use hardware acceleration
className: 'spinner', // The CSS class to assign to the spinner
zIndex: 2e9, // The z-index (defaults to 2000000000)
top: 'auto', // Top position relative to parent in px
left: 'auto' // Left position relative to parent in px
};
var target = $('.events .spin-wheel');
var spinner = new Spinner(opts).spin(target[0]);
});