Forked from jamesu/Making a Real Calendar in Rails.rb
Created
February 4, 2009 13:21
-
-
Save reservationlive/58097 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# Calendar helper with proper events | |
# http://www.cuppadev.co.uk/webdev/making-a-real-calendar-in-rails/ | |
# | |
# (C) 2009 James S Urquhart (jamesu at gmail dot com) | |
# Derived from calendar_helper | |
# (C) Jeremy Voorhis, Geoffrey Grosenbach, Jarkko Laine, Tom Armitage, Bryan Larsen | |
# Licensed under MIT. http://www.opensource.org/licenses/mit-license.php | |
## | |
# Ever wanted a calendar_helper with proper listed events, like all-day events in ical or google calendar? | |
# Well here is how you do it! | |
# Firstly, lets start off with the modified calendar_helper helpers. | |
def calendar(options = {}, &block) | |
block ||= Proc.new {|d| nil} | |
defaults = { | |
:year => Time.now.year, | |
:month => Time.now.month, | |
:table_class => 'calendar', | |
:month_name_class => 'monthName', | |
:other_month_class => 'otherMonth', | |
:day_name_class => 'dayName', | |
:day_class => 'day', | |
:abbrev => (0..2), | |
:first_day_of_week => 0, | |
:accessible => false, | |
:show_today => true, | |
:previous_month_text => nil, | |
:next_month_text => nil, | |
:start => nil, | |
:event_strips => nil, # [[nil]*days, ...] | |
:event_width => 81, # total width per day (including margins) | |
:event_height => 24, # height | |
:event_margin => 2 # height margin | |
} | |
options = defaults.merge options | |
options[:month_name_text] ||= Date::MONTHNAMES[options[:month]] | |
first = Date.civil(options[:year], options[:month], 1) | |
last = Date.civil(options[:year], options[:month], -1) | |
start = options[:start] | |
event_strips = options[:event_strips] | |
event_width = options[:event_width] | |
event_height = options[:event_height] | |
event_margin = options[:event_margin] | |
first_weekday = first_day_of_week(options[:first_day_of_week]) | |
last_weekday = last_day_of_week(options[:first_day_of_week]) | |
day_names = Date::DAYNAMES.dup | |
first_weekday.times do | |
day_names.push(day_names.shift) | |
end | |
# TODO Use some kind of builder instead of straight HTML | |
cal = %(<table class="#{options[:table_class]}" border="0" cellspacing="0" cellpadding="0">) | |
cal << %(<thead><tr>) | |
if options[:previous_month_text] or options[:next_month_text] | |
cal << %(<th colspan="2">#{options[:previous_month_text]}</th>) | |
colspan=3 | |
else | |
colspan=7 | |
end | |
cal << %(<th colspan="#{colspan}" class="#{options[:month_name_class]}">#{options[:month_name_text]}</th>) | |
cal << %(<th colspan="2">#{options[:next_month_text]}</th>) if options[:next_month_text] | |
cal << %(</tr><tr class="#{options[:day_name_class]}">) | |
day_names.each do |d| | |
unless d[options[:abbrev]].eql? d | |
cal << "<th scope='col'><abbr title='#{d}'>#{d[options[:abbrev]]}</abbr></th>" | |
else | |
cal << "<th scope='col'>#{d[options[:abbrev]]}</th>" | |
end | |
end | |
cal << "</tr></thead><tbody><tr>" | |
beginning_of_week(first, first_weekday).upto(first - 1) do |d| | |
cal << %(<td class="#{options[:other_month_class]}) | |
cal << " weekendDay" if weekend?(d) | |
if options[:accessible] | |
cal << %(">#{d.day}<span class="hidden"> #{Date::MONTHNAMES[d.month]}</span></td>) | |
else | |
cal << %(">#{d.day}</td>) | |
end | |
end unless first.wday == first_weekday | |
start_row = beginning_of_week(first, first_weekday) | |
last_row = start_row | |
first.upto(last) do |cur| | |
cell_text, cell_attrs = nil#block.call(cur) | |
cell_text ||= cur.mday | |
cell_attrs ||= {:class => options[:day_class]} | |
cell_attrs[:class] += " weekendDay" if [0, 6].include?(cur.wday) | |
cell_attrs[:class] += " today" if (cur == Date.today) and options[:show_today] | |
cell_attrs = cell_attrs.map {|k, v| %(#{k}="#{v}") }.join(" ") | |
cal << "<td #{cell_attrs}>#{cell_text}</td>" | |
if cur.wday == last_weekday | |
content = calendar_row(event_strips, | |
event_width, | |
event_height, | |
start_row, | |
last_row..cur, | |
&block) | |
cal << "</tr>#{event_row(content, event_height, event_margin)}<tr>" | |
last_row = cur + 1 | |
end | |
end | |
(last + 1).upto(beginning_of_week(last + 7, first_weekday) - 1) do |d| | |
cal << %(<td class="#{options[:other_month_class]}) | |
cal << " weekendDay" if weekend?(d) | |
if options[:accessible] | |
cal << %(">#{d.day}<span class='hidden'> #{Date::MONTHNAMES[d.mon]}</span></td>) | |
else | |
cal << %(">#{d.day}</td>) | |
end | |
end unless last.wday == last_weekday | |
content = calendar_row(event_strips, | |
event_width, | |
event_height, | |
start_row, | |
last_row..(beginning_of_week(last + 7, first_weekday) - 1), | |
&block) | |
cal << "</tr>#{event_row(content, event_height, event_margin)}</tbody></table>" | |
end | |
def calendar_row(event_strips, event_width, event_height, start, date_range, &block) | |
start_date = date_range.first | |
range = ((date_range.first - start).to_i)...((date_range.last - start + 1).to_i) | |
idx = -1 | |
last_offs = 0 | |
event_strips.collect do |strip| | |
idx += 1 | |
range.collect do |r| | |
event = strip[r] | |
if !event.nil? | |
# Clip event dates (if it extends before or beyond the row) | |
dates = event.clip_range(start_date, date_range.last) | |
if dates[0] - start_date == r-range.first | |
# Event somewhere on this row | |
cur_offs = (event_width*(r-range.first)) | |
start_d = event.start_date.to_date | |
end_d = event.end_date.nil? ? start_d+1 : event.end_date.to_date+1 | |
block.call(event, dates[1]-dates[0], cur_offs, idx) | |
else | |
nil | |
end | |
else | |
nil | |
end | |
end.compact | |
end | |
end | |
def event_row(content, height, margin) | |
"<tr><td colspan=\"7\"><div class=\"events\" style=\"height:#{(height+margin)*content.length}px\">#{content.join}</div><div class=\"clear\"></div></td></tr>" | |
end | |
## | |
## What is the difference? | |
## | |
# Instead of yielding for each day column, we yield for displaying each event displayed in the | |
# supplied event_strip. | |
# Instead of getting clumped in a single column, events are placed in rows after each set of day cells, | |
# so they can be spread over multiple days. | |
## | |
## Events? | |
## | |
# Events are merely ActiveRecord objects with the following schema: | |
create_table :events do |t| | |
t.integer "calendar_id" | |
t.string :title | |
t.datetime :start_date | |
t.datetime :end_date, :default => nil | |
t.text :description | |
end | |
# They also have two crucial helper functions: | |
# (These both go in your Event model file (/app/models/event.rb)) | |
def to_date | |
(end_date || start_date).to_date + 1 | |
end | |
def clip_range(start_d, end_d) | |
# Clip start date | |
if (start_date < start_d and to_date > start_d) | |
clipped_start = start_d | |
else | |
clipped_start = start_date.to_date | |
end | |
# Clip end date | |
if (to_date >= end_d) | |
clipped_end = end_d + 1 | |
else | |
clipped_end = to_date | |
end | |
[clipped_start, clipped_end] | |
end | |
## | |
## Event strip? | |
## | |
# An event strip is a list of arrays containing events corresponding to what goes on in a particular day, | |
# encompassing the whole period displayed in the calendar. | |
# An example is as follows: | |
# [ | |
# [ Event(0), nil ,Event(1), Event(1), Event(2), nil, nil, ... ] | |
# [ Event(3), Event(3),Event(3), Event(3), Event(3), Event(3), Event(3), ... ] | |
# ] | |
# So we can see, the event strip closely resembles what should be displayed on the calendar, | |
# with each array representing a separate "row" in which the events should be placed. | |
# Events 0 through 2 dont conflict with one another, so they can exist on the same row. | |
# Event 3 however exists for a whole 7 days and thus conflicts with events 0 through 2, | |
# so it gets placed on its own row. | |
## | |
## Ok, so how do we generate these event strips? | |
## | |
# The algorithm is simple: | |
# 1) Start off with the initial blank event strip encompassing all the dates represented | |
# in the calendar ends. | |
# 2) For each event: | |
# 3) Find out the range of dates it encompasses in the strip | |
# 4) For each existing strip | |
# 5) If the range is free, set it and go to the next event | |
# 6) Else, go to the next strip | |
# 7) If the event didn't fit in the existing strips, make a new strip | |
# 8) Fit the event in the new strip and go to the next event | |
# Thus in the controller you will need something like the following, | |
# which grabs all the events for the calendar and inserts them into the event strips | |
# according to the algorithm. | |
@month = params[:month].nil? ? Time.now.month : params[:month].to_i | |
@year = params[:year].nil? ? Time.now.year : params[:year].to_i | |
# Start of month, end of month | |
@now_date = Date.civil(@year, @month) | |
@prev_date = @now_date - 1.month | |
@prev_date = Date.civil(@prev_date.year, @prev_date.month, -1) | |
@next_date = @now_date + 1.month | |
@next_date = Date.civil(@next_date.year, @next_date.month) | |
@first_day_of_week = 1 | |
# offset by weekdays | |
@strip_start = beginning_of_week(@now_date, @first_day_of_week) | |
@next_date = beginning_of_week(@next_date + 7, @first_day_of_week)-1 | |
# initial event strip | |
@event_strips = [[nil] * (@next_date - @strip_start + 1)] | |
# gavinlaking changed 'end_date NOT NULL AND' to 'end_date != NULL AND' for compatibility with MySQL 5.0.67-log (MacOSX Leopard 10.5.6) | |
@events = Event.find(:all, | |
:include => :calendar, | |
:conditions => ['((start_date >= ? AND start_date < ?) OR | |
(end_date != NULL AND | |
(end_date > ? AND start_date < ?) | |
))', | |
@strip_start, @next_date, | |
@strip_start, @next_date+1], :order => 'start_date ASC').collect do |evt| | |
cur_date = evt.start_date.to_date | |
end_date = evt.to_date | |
cur_date, end_date = evt.clip_range(@strip_start, @next_date) | |
range = ((cur_date - @strip_start).to_i)...((end_date - @strip_start).to_i) | |
# Find strip | |
found_strip = nil | |
for strip in @event_strips | |
is_in = true | |
# Are all the spaces free? | |
range.each do |r| | |
if !strip[r].nil? | |
is_in = false | |
break | |
end | |
end | |
# Found it yet? | |
if is_in | |
found_strip = strip | |
break | |
end | |
end | |
# Make strip or add to found strip | |
if !found_strip.nil? | |
range.each {|r| found_strip[r] = evt} | |
else | |
found_strip = [nil] * (@next_date - @strip_start + 1) | |
range.each {|r| found_strip[r] = evt} | |
@event_strips << found_strip | |
end | |
evt | |
end | |
# This borrows the 'beginning_of_week' function from calendar_helper to get the same dates. | |
# 'beginning_of_week' needs 'days_between' also. | |
## | |
## I've got events, but how do i display them? | |
## | |
# Somewhere in your view, you should have: (don't forget to replace 'events_calendar_opts' with something like (:year => 2009, :month => 2) (see the README) | |
<%= calendar "events_calendar_opts" do |event, days, cur_offs, idx| | |
"<div class=\"event\" style=\"background: #{event.color}; width: #{(81*days)-1}px; top: #{idx*18}px; left:#{cur_offs}px; \"><div>#{h(event.title)}</div></div>" | |
end %> | |
i.e. | |
<%= calendar (:year => 2009, :month => 2, :first_day_of_week => 1) do |event, days, cur_offs, idx| | |
"<div class=\"event\" style=\"background: #{event.color}; width: #{(81*days)-1}px; top: #{idx*18}px; left:#{cur_offs}px; \"><div>#{h(event.title)}</div></div>" | |
end %> | |
# As for styling, ensure the following: | |
# - Your events need to be absolutely positioned within the events block | |
# - Width of the day column (in my case, 81) should match the event width specified for the helper. | |
# - For columns, don't use border. Instead make a repeating background image | |
# i.e. somethng like this... | |
" | |
content { width: 600px; } | |
table.calendar { | |
background-image: url('../images/cal.png'); | |
background-repeat: repeat-y; | |
} | |
.day { width: 100px; } | |
.events { position:relative; border-bottom: 1px solid #d5d5d5; } | |
.event { | |
overflow:hidden; | |
font-size: 12px; | |
text-align: left; | |
position:absolute; | |
height: 16px; | |
} | |
.event div { cursor: pointer; padding-left: 6px; color:#ffffff; text-decoration: none; } | |
" | |
## | |
## To conclude | |
## | |
# Any suggestions or improvements? Feel free to fork this gist. | |
# - JamesU | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment