Created
January 28, 2009 18:42
-
-
Save jamesu/54116 to your computer and use it in GitHub Desktop.
Revisions
-
jamesu created this gist
Jan 28, 2009 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,365 @@ ## # 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: 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)] @events = Event.find(:all, :include => :calendar, :conditions => ['((start_date >= ? AND start_date < ?) OR (end_date NOT 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 # (Note that i had to borrow the beginning_of_week function from calendar_helper to get the same dates) ## ## I've got events, but how do i display them? ## # Somewhere in your view, you should have: 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 # 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