diff --git a/tracks/app/controllers/application.rb b/tracks/app/controllers/application.rb index 0122cea4..6ddab9c3 100644 --- a/tracks/app/controllers/application.rb +++ b/tracks/app/controllers/application.rb @@ -2,13 +2,19 @@ # Likewise will all the methods added be available for all controllers. require_dependency "login_system" + require 'date' class ApplicationController < ActionController::Base helper :application include LoginSystem - + + # Contstants from settings.yml + DATE_FORMAT = app_configurations["formats"]["date"] + WEEK_STARTS_ON = app_configurations["formats"]["week_starts"] + NO_OF_ACTIONS = app_configurations["formats"]["hp_completed"] + def count_shown_items(hidden) count = 0 sub = 0 @@ -17,5 +23,10 @@ class ApplicationController < ActionController::Base end total = Todo.find_all("done=0").length - sub end - + + # Returns all the errors on the page for an object... + def errors_for( obj ) + error_messages_for( obj ) unless instance_eval("@#{obj}").nil? + end + end \ No newline at end of file diff --git a/tracks/app/controllers/context_controller.rb b/tracks/app/controllers/context_controller.rb index 07d09a6b..657c0006 100644 --- a/tracks/app/controllers/context_controller.rb +++ b/tracks/app/controllers/context_controller.rb @@ -1,126 +1,144 @@ class ContextController < ApplicationController - - + helper :context model :project + model :todo + before_filter :login_required - caches_action :list layout "standard" - - - # Main method for listing contexts - # Set page title, and collect existing contexts in @contexts - # def index list render_action "list" end + # Main method for listing contexts + # Set page title, and collect existing contexts in @contexts + # def list @page_title = "TRACKS::List Contexts" - @contexts = Context.find_all( conditions = nil, "position ASC", limit = nil ) + @contexts = Context.find(:all, :conditions => nil, :order => "position ASC", :limit => nil ) + end + + # Filter the projects to show just the one passed in the URL + # e.g. /project/show/ shows just . + # + def show + @context = Context.find_by_name(@params["name"].humanize) + @places = Context.find(:all) + @projects = Project.find(:all) + @page_title = "TRACKS::Context: #{@context.name}" + @not_done = Todo.find(:all, :conditions => "done=0 AND context_id=#{@context.id}", + :order => "due IS NULL, due ASC, created ASC") + @done = Todo.find(:all, :conditions => "done=1 AND context_id=#{@context.id}", + :order => "completed DESC") + @count = Todo.count( "context_id=#{@context.id} AND done=0" ) + end + + # Creates a new context via Ajax helpers + # + def new_context + @context = Context.new(@params['context']) + if @context.save + render_partial( 'context_listing', @context ) + else + flash["warning"] = "Couldn't add new context" + render_text "#{flash["warning"]}" + end end + # Edit the details of the context + # + def update + context = Context.find(params[:id]) + context.attributes = @params["context"] + if context.save + render_partial 'context_listing', context + else + flash["warning"] = "Couldn't update new context" + render_text "" + end + end + + # Edit the details of the action in this context + # + def update_action + @places = Context.find(:all) + @projects = Project.find(:all) + action = Todo.find(params[:id]) + action.attributes = @params["item"] + if action.due? + action.due = Date.strptime(@params["item"]["due"], DATE_FORMAT) + else + action.due = "" + end + + if action.save + render_partial 'show_items', action + else + flash["warning"] = "Couldn't update the action" + render_text "" + end + end # Called by a form button - # Parameters from form fields should be passed to create new context - # - def add_context - expire_action(:controller => "context", :action => "list") - context = Context.new - context.attributes = @params["new_context"] - - if context.save - flash["confirmation"] = "Succesfully created context \"#{context.name}\"" - redirect_to( :action => "list" ) - else - flash["warning"] = "Couldn't add new context \"#{context.name}\"" - redirect_to( :action => "list" ) - end - end - - def new - expire_action(:controller => "context", :action => "list") - context = Context.new - context.attributes = @params["new_context"] - - if context.save - flash["confirmation"] = "Succesfully created context \"#{context.name}\"" - redirect_to( :action => "list" ) - else - flash["warning"] = "Couldn't add new context \"#{context.name}\"" - redirect_to( :action => "list" ) - end - end - - def edit - expire_action(:controller => "context", :action => "list") - @context = Context.find(@params['id']) - @page_title = "TRACKS::Edit context: #{@context.name.capitalize}" - end - - - def update - @context = Context.find(@params['context']['id']) - @context.attributes = @params['context'] - if @context.save - flash["confirmation"] = "Context \"#{@context.name}\" was successfully updated" - redirect_to :action => 'list' - else - flash["warning"] = "Context \"#{@context.name}\" could not be updated" - redirect_to :action => 'list' - end - end - - - # Filter the contexts to show just the one passed in the URL - # e.g. /context/show/ shows just . - # - def show - @context = Context.find_by_name(@params["id"].humanize) - @projects = Project.find_all - @page_title = "TRACKS::Context: #{@context.name.capitalize}" - @not_done = Todo.find_all( "context_id=#{@context.id} AND done=0", "due DESC, created ASC" ) - @count = Todo.count( "context_id=#{@context.id} AND done=0" ) - end - - - # Called by a form button # Parameters from form fields are passed to create new action - # in the selected context. - def add_item - expire_action(:controller => "context", :action => "list") + # + def add_item + @projects = Project.find( :all ) + @places = Context.find( :all ) + item = Todo.new item.attributes = @params["new_item"] - where = Context.find_by_id(item.context_id) - - back_to = urlize(where.name) - - if item.save - flash["confirmation"] = "Succesfully added action \"#{item.description}\" to context" - redirect_to( :controller => "context", :action => "show", :name => "#{back_to}") - else - flash["warning"] = "Could not add action \"#{item.description}\" to context" - redirect_to( :controller => "context", :action => "show", :name => "#{back_to}" ) - end + + if item.due? + item.due = Date.strptime(@params["new_item"]["due"], DATE_FORMAT) + else + item.due = "" + end + + if item.save + render_partial 'show_items', item + else + flash["warning"] = "Couldn't add next action \"#{item.description}\"" + render_text "" + end end - # Fairly self-explanatory; deletes the context # If the context contains actions, you'll get a warning dialogue. # If you choose to go ahead, any actions in the context will also be deleted. - def destroy - expire_action(:controller => "context", :action => "list") - context = Context.find( @params['id'] ) - if context.destroy - flash["confirmation"] = "Succesfully deleted context \"#{context.name}\"" - redirect_to( :action => "list" ) + def destroy + this_context = Context.find(params[:id]) + if this_context.destroy + render_text "" + else + flash["warning"] = "Couldn't delete context \"#{context.name}\"" + redirect_to( :controller => "context", :action => "list" ) + end + end + + # Delete a next action in a context + # + def destroy_action + item = Todo.find(params[:id]) + if item.destroy + render_text "" else - flash["warning"] = "Couldn't delete context \"#{context.name}\"" - redirect_to( :action => "list" ) + flash["warning"] = "Couldn't delete next action \"#{item.description}\"" + redirect_to :action => "list" end end - - + + # Toggles the 'done' status of the action + # + def toggle_check + @places = Context.find(:all) + @projects = Project.find(:all) + + item = Todo.find(params[:id]) + + item.toggle!('done') + render_partial 'show_items', item + end end diff --git a/tracks/app/controllers/project_controller.rb b/tracks/app/controllers/project_controller.rb index 787595f8..a0bec9f6 100644 --- a/tracks/app/controllers/project_controller.rb +++ b/tracks/app/controllers/project_controller.rb @@ -5,7 +5,6 @@ class ProjectController < ApplicationController model :todo before_filter :login_required - caches_action :list layout "standard" def index @@ -18,105 +17,129 @@ class ProjectController < ApplicationController # def list @page_title = "TRACKS::List Projects" - @projects = Project.find_all + @projects = Project.find(:all, :conditions => nil, :order => "position ASC") end - # Filter the projects to show just the one passed in the URL - # e.g. /project/show/ shows just . + # e.g. /project/show/ shows just . # def show @project = Project.find_by_name(@params["name"].humanize) - @places = Context.find_all + @places = Context.find(:all) + @projects = Project.find(:all) @page_title = "TRACKS::Project: #{@project.name}" - @not_done = Todo.find_all( "project_id=#{@project.id} AND done=0", "due DESC, created ASC" ) - @count = Todo.count( "project_id=#{@project.id} AND done=0" ) + @not_done = Todo.find(:all, :conditions => "done=0 AND project_id=#{@project.id}", + :order => "due IS NULL, due ASC, created ASC") + @done = Todo.find(:all, :conditions => "done=1 AND project_id=#{@project.id}", + :order => "completed DESC") + @count = @not_done.length end - - def edit - expire_action(:controller => "project", :action => "list") - @project = Project.find(@params['id']) - @page_title = "TRACKS::Edit project: #{@project.name.capitalize}" + def new_project + @project = Project.new(@params['project']) + if @project.save + render_partial( 'project_listing', @project ) + else + flash["warning"] = "Couldn't update new project" + render_text "" + end end - + # Edit the details of the project + # def update - @project = Project.find(@params['project']['id']) - @project.attributes = @params['project'] - if @project.save - flash["confirmation"] = "Project \"#{@project.name}\" was successfully updated" - redirect_to :action => 'list' - else - flash["warning"] = "Project \"#{@project.name}\" could not be updated" - redirect_to :action => 'list' - end - end - - - # Called by a form button - # Parameters from form fields should be passed to create new project + project = Project.find(params[:id]) + project.attributes = @params["project"] + if project.save + render_partial 'project_listing', project + else + flash["warning"] = "Couldn't update new project" + render_text "" + end + end + + # Edit the details of the action in this project # - def add_project - expire_action(:controller => "project", :action => "list") - project = Project.new - project.name = @params["new_project"]["name"] - - if project.save - flash["confirmation"] = "Succesfully added project \"#{project.name}\"" - redirect_to( :action => "list" ) - else - flash["warning"] = "Couldn't add project \"#{project.name}\"" - redirect_to( :action => "list" ) - end + def update_action + @places = Context.find(:all) + @projects = Project.find(:all) + action = Todo.find(params[:id]) + action.attributes = @params["item"] + + if action.due? + action.due = Date.strptime(@params["item"]["due"], DATE_FORMAT) + else + action.due = "" + end + + if action.save + render_partial 'show_items', action + else + flash["warning"] = "Couldn't update the action" + render_text "" + end end - - def new - expire_action(:controller => "project", :action => "list") - project = Project.new - project.name = @params["new_project"]["name"] - - if project.save - flash["confirmation"] = "Succesfully added project \"#{project.name}\"" - redirect_to( :action => "list" ) - else - flash["warning"] = "Couldn't add project \"#{project.name}\"" - redirect_to( :action => "list" ) - end - end - # Called by a form button - # Parameters from form fields should be passed to create new item + # Parameters from form fields are passed to create new action # def add_item - expire_action(:controller => "project", :action => "list") + @projects = Project.find( :all ) + @places = Context.find( :all ) + item = Todo.new item.attributes = @params["new_item"] - - back_to = item.project_id - - if item.save - flash["confirmation"] = "Successfully added next action \"#{item.description}\" to project" - redirect_to( :controller => "project", :action => "show", :id => "#{back_to}" ) - else - flash["warning"] = "Couldn't add next action \"#{item.description}\" to project" - redirect_to( :controller => "project", :action => "show", :id => "#{back_to}" ) - end + + if item.due? + item.due = Date.strptime(@params["new_item"]["due"], DATE_FORMAT) + else + item.due = "" + end + + if item.save + render_partial 'show_items', item + else + flash["warning"] = "Couldn't add next action \"#{item.description}\"" + render_text "" + end end - + # Delete a project + # def destroy - expire_action(:controller => "project", :action => "list") - project = Project.find( @params['id'] ) - if project.destroy - flash["confirmation"] = "Succesfully deleted project \"#{project.name}\"" - redirect_to( :action => "list" ) + this_project = Project.find( @params['id'] ) + if this_project.destroy + render_text "" else flash["warning"] = "Couldn't delete project \"#{project.name}\"" - redirect_to( :action => "list" ) + redirect_to( :controller => "project", :action => "list" ) end end + + # Delete a next action in a project + # + def destroy_action + item = Todo.find(@params['id']) + if item.destroy + #flash["confirmation"] = "Next action \"#{item.description}\" was successfully deleted" + render_text "" + else + flash["warning"] = "Couldn't delete next action \"#{item.description}\"" + redirect_to :action => "list" + end + end + # Toggles the 'done' status of the action + # + def toggle_check + @places = Context.find(:all) + @projects = Project.find(:all) + item = Todo.find(@params['id']) + + item.toggle!('done') + render_partial 'show_items', item + end + + end diff --git a/tracks/app/controllers/todo_controller.rb b/tracks/app/controllers/todo_controller.rb index 4ca41a85..27a8cb95 100644 --- a/tracks/app/controllers/todo_controller.rb +++ b/tracks/app/controllers/todo_controller.rb @@ -4,30 +4,28 @@ class TodoController < ApplicationController model :context, :project before_filter :login_required - caches_action :list, :completed, :completed_archive layout "standard" - # Main method for listing tasks - # Set page title, and fill variables with contexts and done and not-done tasks - # Number of completed actions to show is determined by a setting in settings.yml - # - - def index - list - render_action "list" - end + def index + list + render_action "list" + end + # Main method for listing tasks + # Set page title, and fill variables with contexts and done and not-done tasks + # Number of completed actions to show is determined by a setting in settings.yml + # def list @page_title = "TRACKS::List tasks" - @no_of_actions = app_configurations["formats"]["hp_completed"] - @projects = Project.find_all - @places = Context.find_all - @shown_places = Context.find_all_by_hide( "0", "position ASC") - @hidden_places = Context.find_all_by_hide( "1", "position ASC" ) - @done = Todo.find_all_by_done( 1, "completed DESC", @no_of_actions ) + @projects = Project.find( :all ) + @places = Context.find( :all ) + @shown_places = Context.find( :all, :conditions => "hide=0", :order => "position ASC" ) + @hidden_places = Context.find( :all, :conditions => "hide=1", :order => "position ASC" ) + @done = Todo.find( :all, :conditions => "done=1", :order => "completed DESC", + :limit => NO_OF_ACTIONS ) # Set count badge to number of not-done, not hidden context items - @count = count_shown_items(@hidden_places) + @count = count_shown_items( @hidden_places ) end @@ -58,78 +56,73 @@ class TodoController < ApplicationController ORDER BY completed DESC;" ) end - # Called by a form button - # Parameters from form fields should be passed to create new item - # - def add_item - expire_action(:controller => "todo", :action => "list") - @item = Todo.new - @item.attributes = @params["item"] - - if @item.save - flash["confirmation"] = "Next action \"#{@item.description}\" was successfully added" - redirect_to( :action => "list" ) + # Called by a form button + # Parameters from form fields are passed to create new action + # in the selected context. + def add_item + @projects = Project.find( :all ) + @places = Context.find( :all ) + + item = Todo.new + item.attributes = @params["new_item"] + + if item.due? + item.due = Date.strptime(@params["new_item"]["due"], DATE_FORMAT) else - flash["warning"] = "Couldn't add the action \"#{@item.description}\"" - redirect_to( :action => "list" ) + item.due = "" end - end - - - def edit - expire_action(:controller => "todo", :action => "list") - @item = Todo.find(@params['id']) - @belongs = @item.project_id - @projects = Project.find_all - @places = Context.find_all - @page_title = "TRACKS::Edit task: #{@item.description}" + + if item.save + render_partial 'show_items', item + else + flash["warning"] = "Couldn't add next action \"#{item.description}\"" + render_text "" + end end - - - def update - expire_action(:controller => "todo", :action => "list") - @item = Todo.find(@params['item']['id']) - @item.attributes = @params['item'] - if @item.save - flash["confirmation"] = "Next action \"#{@item.description}\" was successfully updated" - redirect_to :action => 'list' + + # Edit the details of an action + # + def update_action + @places = Context.find(:all) + @projects = Project.find(:all) + action = Todo.find(params[:id]) + action.attributes = @params["item"] + if action.due? + action.due = Date.strptime(@params["item"]["due"], DATE_FORMAT) else - flash["warning"] = "Next action \"#{@item.description}\" could not be updated" - redirect_to :action => 'list' + action.due = "" + end + + if action.save + render_partial 'show_items', action + else + flash["warning"] = "Couldn't update the action" + render_text "" end end - - def destroy - expire_action(:controller => "todo", :action => "list") - item = Todo.find(@params['id']) - if item.destroy - flash["confirmation"] = "Next action \"#{item.description}\" was successfully deleted" - redirect_to :action => "list" - else - flash["warning"] = "Couldn't delete next action \"#{item.description}\"" - redirect_to :action => "list" - end - end + # Delete a next action in a context + # + def destroy_action + item = Todo.find(@params['id']) + if item.destroy + render_text "" + else + flash["warning"] = "Couldn't delete next action \"#{item.description}\"" + redirect_to :action => "list" + end + end # Toggles the 'done' status of the action # def toggle_check - expire_action(:controller => "todo", :action => "list") - expire_action(:controller => "todo", :action => "completed") - expire_action(:controller => "todo", :action => "completed_archive") + @projects = Project.find(:all) + @places = Context.find(:all) + item = Todo.find(@params['id']) - - item.toggle!('done') - - if item.save - flash["confirmation"] = "Next action \"#{item.description}\" marked as completed" - redirect_to( :action => "list" ) - else - flash["warning"] = "Couldn't mark action \"#{item.description}\" as completed" - redirect_to( :action => "list" ) - end - end - + + item.toggle!('done') + render_partial 'show_items', item + end end diff --git a/tracks/app/helpers/application_helper.rb b/tracks/app/helpers/application_helper.rb index 2b7428d0..7b55b6c3 100644 --- a/tracks/app/helpers/application_helper.rb +++ b/tracks/app/helpers/application_helper.rb @@ -5,8 +5,11 @@ module ApplicationHelper # in config/settings.yml # def format_date(date) - date_fmt = app_configurations["formats"]["date"] - formatted_date = date.strftime("#{date_fmt}") + if date + formatted_date = date.strftime("#{ApplicationController::DATE_FORMAT}") + else + formatted_date = '' + end end # Uses RedCloth to transform text using either Textile or Markdown @@ -24,9 +27,9 @@ module ApplicationHelper tagged = "<#{tag}>#{object}" end - def urlize(name) - name.to_s.gsub(/ /, "_").downcase - end + def urlize(name) + name.to_s.gsub(/ /, "_").downcase + end # Check due date in comparison to today's date @@ -52,4 +55,5 @@ module ApplicationHelper "" + format_date(due) + " " end end + end diff --git a/tracks/app/models/project.rb b/tracks/app/models/project.rb index 8af5f9f6..da486009 100644 --- a/tracks/app/models/project.rb +++ b/tracks/app/models/project.rb @@ -8,4 +8,5 @@ class Project < ActiveRecord::Base validates_presence_of :name, :message => "project must have a name" validates_length_of :name, :maximum => 255, :message => "project name must be less than %d" validates_uniqueness_of :name, :message => "already exists" + end diff --git a/tracks/app/views/context/_context_form.rhtml b/tracks/app/views/context/_context_form.rhtml new file mode 100644 index 00000000..5b21481f --- /dev/null +++ b/tracks/app/views/context/_context_form.rhtml @@ -0,0 +1,14 @@ +<% + @context = context_form +%> + +
+ <%= @context.position %> +
+
+ +<%= text_field 'context', 'name', :class => 'context-name' %> + +<%= check_box 'context', 'hide', :class => 'context-hide' %> +
+<% @context = nil %> \ No newline at end of file diff --git a/tracks/app/views/context/_context_listing.rhtml b/tracks/app/views/context/_context_listing.rhtml new file mode 100644 index 00000000..ee3dc80e --- /dev/null +++ b/tracks/app/views/context/_context_listing.rhtml @@ -0,0 +1,50 @@ +<% context = context_listing %> +
+ +
+<% if context.position % 2 == 0 %> +
+<% else %> +
+<% end %> +
+ <%= context.position.to_s %> +
+
+ <%= link_to( "#{context.name}", :action => "show", :name => urlize(context.name) ) %><%= " (" + Todo.count( "context_id=#{context.id} AND done=0" ).to_s + " actions)" %> +
+ +
+<% if context.hide == 1 %> + +<% else %> + +<% end %> + +<%= link_to_function(image_tag( "edit", :title => "Edit item", :width=>"10", :height=>"10", :border=>"0"), "Element.toggle('context-#{context.id}','context-#{context.id}-edit-form'); new Effect.Appear('context-#{context.id}-edit-form'); Form.focus_first('form-context-#{context.id}');" ) + " " + + link_to_remote( image_tag("delete", :title =>"Delete this context"), + :update => "context-#{context.id}-container", + :loading => "new Effect.Squish('context-#{context.id}-container')", + :url => { :controller => "context", :action => "destroy", :id => context.id }, :confirm => "Are you sure that you want to delete the context \'#{context.name}\'?" ) %> +
+
+ +
+ +
+<% if controller.action_name == 'new_context' %> + +<% end %> diff --git a/tracks/app/views/context/_not_done.rhtml b/tracks/app/views/context/_not_done.rhtml deleted file mode 100644 index 48624973..00000000 --- a/tracks/app/views/context/_not_done.rhtml +++ /dev/null @@ -1,26 +0,0 @@ -<% @item = not_done %> - - - <%= - link = url_for( :controller => 'todo', :action => 'toggle_check', :id => "#{@item.id}" ); check_box( "item", "done", :onclick => "document.location.href='#{link}'" ) - %> - - - <%= - link_to(image_tag( "edit", :title => "Edit item", :width=>"10", :height=>"10", :border=>"0"), { :controller =>"todo", :action => "edit", :id => @item.id } ) + " " + link_to(image_tag( "delete", :title => "Delete item", :width=>"10", :height=>"10", :border=>"0" ), { :controller => "todo", :action => "destroy", :id => @item.id}, :confirm => "Are you sure you want to delete this entry: #{@item.description}" ) + " " - %> - - <%= due_date( @item.due ) %> - <%= @item.description %> - <% if @item.project_id %> - <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(@item.project.name) }, :title => "View project: #{@item.project.name}" ) %> - <% end %> - <% if @item.notes? %> - <%= "" + - image_tag( "notes", :width=>"10", :height=>"10", :border=>"0") + "" - %> - <% m_notes = markdown( @item.notes ) %> - <%= "
" + m_notes + "
" %> - <% end %> - - \ No newline at end of file diff --git a/tracks/app/views/context/_show_items.rhtml b/tracks/app/views/context/_show_items.rhtml new file mode 100644 index 00000000..1fd26c18 --- /dev/null +++ b/tracks/app/views/context/_show_items.rhtml @@ -0,0 +1,84 @@ +<% item = show_items %> +<% if !item.done? %> +
+<%= form_remote_tag( :url => url_for( :controller => "context", :action => "toggle_check", :id => item.id ), + :html => { :id=> "checkbox-notdone-#{item.id}", :class => "inline-form" }, + :update => "completed", + :position => "top", + :loading => "Form.disable('checkbox-notdone-#{item.id}');", + :complete => "new Effect.Squish('item-#{item.id}-container', true);" + ) %> + +
+
+ + <%= + link_to_function(image_tag( "edit", :title => "Edit item", :width=>"10", :height=>"10", :border=>"0"), "Element.toggle('item-#{item.id}','action-#{item.id}-edit-form'); new Effect.Appear('action-#{item.id}-edit-form'); Form.focusFirstElement('form-action-#{item.id}');" ) + " " + + link_to_remote( image_tag("delete", :title =>"Delete this action"), + :update => "item-#{item.id}-container", + :loading => "new Effect.Squish('item-#{item.id}-container')", + :url => { :controller => "context", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + %> +
+
+ <%= due_date( item.due ) %> + <%= item.description %> + <% if item.project_id %> + <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> + <% end %> + <% if item.notes? %> + <%= "" + image_tag( "notes", :width=>"10", :height=>"10", :border=>"0") + "" %> + <% m_notes = markdown( item.notes ) %> + <%= "
" + m_notes + "
" %> + <% end %> +
+
+<%= end_form_tag %> + + + +
+<% else %> +
+<%= form_remote_tag( :url => url_for( :controller => "context", :action => "toggle_check", :id => item.id ), + :html => { :id=> "checkbox-done-#{item.id}", :class => "inline-form" }, + :update => "next_actions", + :position => "bottom", + :loading => "Form.disable('checkbox-done-#{item.id}');", + :complete => "new Effect.Squish('done-item-#{item.id}-container', true);" + ) %> + +
+
+ + <%= + link_to_remote( image_tag("delete", :title =>"Delete this action"), + :update => "done-item-#{item.id}-container", + :loading => "new Effect.Squish('done-item-#{item.id}-container')", + :url => { :controller => "context", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + %> +
+
+ <%= format_date( item.completed ) %> + <%= item.description %> + <% if item.project_id %> + <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> + <% end %> + <% if item.notes? %> + <%= "" + image_tag( "notes", :width=>"10", :height=>"10", :border=>"0") + "" %> + <% m_notes = markdown( item.notes ) %> + <%= "
" + m_notes + "
" %> + <% end %> +
+
+<%= end_form_tag %> +
+<% end %> +<% item = nil %> \ No newline at end of file diff --git a/tracks/app/views/context/edit.rhtml b/tracks/app/views/context/edit.rhtml deleted file mode 100644 index 1d22e1a2..00000000 --- a/tracks/app/views/context/edit.rhtml +++ /dev/null @@ -1,13 +0,0 @@ -
-

Edit context

- <%= error_messages_for 'context' %> - <%= start_form_tag :controller=>"context", :action=>"update", :id=>@context.id %> - <%= render_partial "edit_context", "@context" %> - - <%= end_form_tag %> - - <%= link_to 'Cancel', :action => 'list' %> -
- -<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> -<% if @flash["warning"] %>
<%= @flash["warning"] %>
<% end %> diff --git a/tracks/app/views/context/list.rhtml b/tracks/app/views/context/list.rhtml index eae39504..3ecce4cf 100644 --- a/tracks/app/views/context/list.rhtml +++ b/tracks/app/views/context/list.rhtml @@ -1,45 +1,36 @@ -
+
- -<% row = 1 %> -<% for @context in @contexts %> - <% if row % 2 == 0 %> - - <% else %> - - <% end %> - - - - - - -<% row += 1 %> +
+<% for context in @contexts %> + <%= render_partial( 'context_listing', context ) %> <% end %> -
<%= @context.position.to_s %><%= link_to( "#{@context.name.capitalize}", :action => "show", :name => urlize(@context.name) ) %> - <% if @context.hide == 1 %> - hidden - <% else %> - shown - <% end %> - - <%= link_to(image_tag( "edit", :title => "Edit item", :width=>"10", :height=>"10", :border=>"0"), { :action => "edit", :id => @context.id } ) + " " + link_to(image_tag( "delete", :title => "Delete item", :width=>10, :height=>10, :border=>0 ), { :action => "destroy", :id => @context.id}, :confirm => "Are you sure you want to delete the context \"#{@context.name}?\" Any todos in this context will be deleted." ) %> -
- -
- -
- <%= start_form_tag :controller=>'context', :action=>'new' %> -
- <%= text_field("new_context", "name") %> -
- - <%= check_box( "new_context", "hide" ) %> -
-
- - <%= end_form_tag %>
-<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> -<% if @flash["warning"] %>
<%= @flash["warning"] %>
<% end %> +
+Create new context » + + +<% if @flash["confirmation"] %> +
<%= @flash["confirmation"] %>
+<% end %> +<% if @flash["warning"] %> +
<%= @flash["warning"] %>
+<% end %> + +
\ No newline at end of file diff --git a/tracks/app/views/context/show.rhtml b/tracks/app/views/context/show.rhtml index 908e6034..ec524fac 100644 --- a/tracks/app/views/context/show.rhtml +++ b/tracks/app/views/context/show.rhtml @@ -1,49 +1,81 @@
-

<%= @context.name.capitalize %>

- - <%= render_collection_of_partials "not_done", @not_done %> -
-
-

Other Contexts: -<% for other_context in Context.find_all %> - <%= link_to( other_context.name.capitalize, { :controller => "context", :action => "show", :name => urlize(other_context.name) } ) + " | " %> -<% end %> -

-
+

<%= @context.name %>

+ +
+ <% if @not_done.empty? %> +

There are no next actions yet in this context

+ <% else %> + <% for item in @not_done %> + <%= render_partial "show_items", item %> + <% end %> + <% end %> +
+
+ +
+

Completed actions in this context

+ +
+ <% if @done.empty? %> +

There are no completed next actions yet in this context

+ <% else %> + <% for done_item in @done %> + <%= render_partial "show_items", done_item %> + <% end %> + <% end %> +
+
+ +
- <%= form_tag( :controller => "context", :action => "add_item") %> - <%= hidden_field( "new_item", "context_id", "value" => "#{@context.id}") %> -
- <%= text_field( "new_item", "description" ) %> -
-
- <%= text_area( "new_item", "notes", "cols" => 40, "rows" => 15 ) %> -
-
-
-
- <%= text_field( "item", "due", "tabindex" => 5, "cols" => 10 ) %> - - -
-
- - + +<%= link_to_function( "Add the next action in this context »", "Element.toggle('context_new_action'); Form.focusFirstElement('context-form-new-action');", {:title => "Add the next action [Alt+n]", :accesskey => "n"}) %> + + + +

Active Contexts:

+
    + <% for other_context in Context.find(:all, :conditions => "hide=0", :order => "position ASC") %> +
  • <%= link_to( other_context.name, { :controller => "context", :action => "show", :name => urlize(other_context.name) } ) + " (" + Todo.count( "project_id=#{other_context.id} AND done=0" ).to_s + " actions)" %>
  • + <% end %> +
+ +

Hidden Contexts:

+
    + <% for other_context in Context.find(:all, :conditions => "hide=1", :order => "position ASC") %> +
  • <%= link_to( other_context.name, { :controller => "context", :action => "show", :name => urlize(other_context.name) } ) + " (" + Todo.count( "project_id=#{other_context.id} AND done=0" ).to_s + " actions)" %>
  • + <% end %> +
<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> diff --git a/tracks/app/views/feed/na_feed.rxml b/tracks/app/views/feed/na_feed.rxml index 6b1bf732..26d754e4 100644 --- a/tracks/app/views/feed/na_feed.rxml +++ b/tracks/app/views/feed/na_feed.rxml @@ -8,7 +8,7 @@ xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do xml.title(i.description) @link = url_for(:controller => 'context', :action => 'show', :id => "#{i.context_id}") xml.link("http://#{@request.host}:#{@request.port}#{@link}") - xml.description(i.context['name'].capitalize) + xml.description(i.context['name']) end } end diff --git a/tracks/app/views/layouts/standard.rhtml b/tracks/app/views/layouts/standard.rhtml index 25f27e9c..a3843b57 100644 --- a/tracks/app/views/layouts/standard.rhtml +++ b/tracks/app/views/layouts/standard.rhtml @@ -3,29 +3,23 @@ <%= stylesheet_link_tag "standard" %> <%= javascript_include_tag "toggle_notes" %> - <%= javascript_include_tag "calendar" %> - + <%= javascript_include_tag "prototype" %> + <%= javascript_include_tag "prototype-ex" %> + <%= stylesheet_link_tag 'calendar-system.css' %> + <%= javascript_include_tag 'calendar', 'calendar-en', 'calendar-setup' %> + + <%= @page_title %> - - - +

<% if @count %> <%= @count %> <% end %> - <%= today = Time.now; datestamp = today.strftime("%A, %d %B %Y") %>

+ <%= Time.now.strftime("%A, %d %B %Y") %>
+
- <%= form_tag( :controller => "project", :action => "add_item") %> - <%= hidden_field( "new_item", "project_id", "value" => "#{@project.id}") %> -
- <%= text_field( "new_item", "description" ) %> -
-
- <%= text_area( "new_item", "notes", "cols" => 40, "rows" => 15 ) %> -
-
- -
-
- <%= text_field( "item", "due", "tabindex" => 5, "cols" => 10 ) %> - - -
-
- - + +<%= link_to_function( "Add the next action in this project »", "Element.toggle('project_new_action'); Form.focusFirstElement('project-form-new-action');", {:title => "Add the next action [Alt+n]", :accesskey => "n"}) %> + + + +

Active Projects:

+
    + <% for other_project in Project.find_all %> +
  • <%= link_to( other_project.name, { :controller => "project", :action => "show", :name => urlize(other_project.name) } ) + " (" + Todo.count( "project_id=#{other_project.id} AND done=0" ).to_s + " actions)" %>
  • + <% end %> +
<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> diff --git a/tracks/app/views/todo/_action_edit_form.rhtml b/tracks/app/views/todo/_action_edit_form.rhtml new file mode 100644 index 00000000..da0f648a --- /dev/null +++ b/tracks/app/views/todo/_action_edit_form.rhtml @@ -0,0 +1,56 @@ +<% + @item = action_edit_form +%> + +<%= hidden_field( "item", "id" ) %> + + + + + + + + + + + + + + + + + + + + + + + + +
<%= text_field( "item", "description", "tabindex" => 1 ) %>
<%= text_area( "item", "notes", "cols" => 20, "rows" => 5, "tabindex" => 2 ) %>
+ +
+ Cancel
+ + +<% @item = nil %> \ No newline at end of file diff --git a/tracks/app/views/todo/_done.rhtml b/tracks/app/views/todo/_done.rhtml deleted file mode 100644 index 278e5d8f..00000000 --- a/tracks/app/views/todo/_done.rhtml +++ /dev/null @@ -1,25 +0,0 @@ -<% @done_item = done %> - -<% if @done_item.completed %> - <%= image_tag( "done", :width=>"16", :height=>"16", :border=>"0") %> - <%= format_date( @done_item.completed ) %> - <%= " " + @done_item.description + " "%> - - <% if @done_item.project_id %> - <%= "(" + @done_item.context['name'].capitalize + ", " + @done_item.project['name'] + ")" %> - <% else %> - <%= "(" + @done_item.context['name'].capitalize + ")" %> - <% end %> - - <% if @done_item.due %> - <%= " - was due on " + format_date( @done_item.due ) %> - <% end %> - - <% if @done_item.notes? %> - <%= "" + image_tag( "notes", :width=>"10", :height=>"10", :border=>"0") + "" %> - <% m_notes = markdown( @done_item.notes ) %> - <%= "
" + m_notes + "
" %> - <% end %> - -<% end %> - \ No newline at end of file diff --git a/tracks/app/views/todo/_item.rhtml b/tracks/app/views/todo/_item.rhtml deleted file mode 100644 index bb05cc1e..00000000 --- a/tracks/app/views/todo/_item.rhtml +++ /dev/null @@ -1,55 +0,0 @@ -<%= hidden_field( "item", "id" ) %> -
-<%= text_field( "item", "description", "tabindex" => 1 ) %> -
-
-<%= text_area( "item", "notes", "tabindex" => 2, "cols" => 35, "rows" => 15 ) %> -
-
- -
-
- -
- -
-<%= text_field( "item", "due", "tabindex" => 5, "cols" => 10 ) %> - - -
-
diff --git a/tracks/app/views/todo/_not_done.rhtml b/tracks/app/views/todo/_not_done.rhtml deleted file mode 100644 index 7b920441..00000000 --- a/tracks/app/views/todo/_not_done.rhtml +++ /dev/null @@ -1,21 +0,0 @@ -<% @notdone_item = not_done %> - - <%= check_box( "item", "done", "onclick" => "markItemDone('action-#{@notdone_item.id}', '/todo/toggle_check/#{@notdone_item.id}', '#{@notdone_item.id}')" ) %> - - <%= link_to(image_tag( "edit", :title => "Edit item", :width=>"10", :height=>"10", :border=>"0"), { :action => "edit", :id => @notdone_item.id } ) + " " + - link_to(image_tag( "delete", :title => "Delete item", :width=>"10", :height=>"10", :border=>"0"), { :action => "destroy", :id => @notdone_item.id }, :confirm => "Are you sure you want to delete this entry: #{@notdone_item.description}" ) + " " %> - - <%= due_date( @notdone_item.due ) %> - <%= @notdone_item.description %> - <% if @notdone_item.project_id %> - <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(@notdone_item.project.name) }, :title => "View project: #{@notdone_item.project.name}" ) %> - <% end %> - <% if @notdone_item.notes? %> - <%= "" + - image_tag( "notes", :width=>"10", :height=>"10", :border=>"0") + "" - %> - <% m_notes = markdown( @notdone_item.notes ) %> - <%= "
" + m_notes + "
" %> - <% end %> - - diff --git a/tracks/app/views/todo/_show_items.rhtml b/tracks/app/views/todo/_show_items.rhtml new file mode 100644 index 00000000..c3bbd412 --- /dev/null +++ b/tracks/app/views/todo/_show_items.rhtml @@ -0,0 +1,84 @@ +<% item = show_items %> +<% if !item.done? %> +
+<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), + :html => { :id=> "checkbox-notdone-#{item.id}", :class => "inline-form" }, + :update => "completed", + :position => "top", + :loading => "Form.disable('checkbox-notdone-#{item.id}');", + :complete => "new Effect.Squish('item-#{item.id}-container', true);" + ) %> + +
+
+ + <%= + link_to_function(image_tag( "edit", :title => "Edit item", :width=>"10", :height=>"10", :border=>"0"), "Element.toggle('item-#{item.id}','action-#{item.id}-edit-form'); new Effect.Appear('action-#{item.id}-edit-form'); Form.focusFirstElement('form-action-#{item.id}')" ) + " " + + link_to_remote( image_tag("delete", :title =>"Delete this action"), + :update => "item-#{item.id}-container", + :loading => "new Effect.Squish('item-#{item.id}-container')", + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + %> +
+
+ <%= due_date( item.due ) %> + <%= item.description %> + <% if item.project_id %> + <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> + <% end %> + <% if item.notes? %> + <%= "" + image_tag( "notes", :width=>"10", :height=>"10", :border=>"0") + "" %> + <% m_notes = markdown( item.notes ) %> + <%= "
" + m_notes + "
" %> + <% end %> +
+
+<%= end_form_tag %> + + + +
+<% else %> +
+<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), + :html => { :id=> "checkbox-done-#{item.id}", :class => "inline-form" }, + :update => "new_actions", + :position => "bottom", + :loading => "Form.disable('checkbox-done-#{item.id}');", + :complete => "Element.toggle('new_actions');new Effect.Squish('done-item-#{item.id}-container', true);" + ) %> + +
+
+ + <%= + link_to_remote( image_tag("delete", :title =>"Delete this action"), + :update => "done-item-#{item.id}-container", + :loading => "new Effect.Squish('done-item-#{item.id}-container')", + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + %> +
+
+ <%= format_date( item.completed ) %> + <%= item.description %> + <% if item.project_id %> + <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> + <% end %> + <% if item.notes? %> + <%= "" + image_tag( "notes", :width=>"10", :height=>"10", :border=>"0") + "" %> + <% m_notes = markdown( item.notes ) %> + <%= "
" + m_notes + "
" %> + <% end %> +
+
+<%= end_form_tag %> +
+<% end %> +<% item = nil %> \ No newline at end of file diff --git a/tracks/app/views/todo/edit.rhtml b/tracks/app/views/todo/edit.rhtml deleted file mode 100644 index 92d782f1..00000000 --- a/tracks/app/views/todo/edit.rhtml +++ /dev/null @@ -1,12 +0,0 @@ -
-

Edit task

-
- <%= render_partial "todo/item", "@item" %> - -
- - <%= link_to 'Cancel', :action => 'list' %> -
- -<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> -<% if @flash["warning"] %>
<%= @flash["warning"] %>
<% end %> \ No newline at end of file diff --git a/tracks/app/views/todo/list.rhtml b/tracks/app/views/todo/list.rhtml index 3a6a485e..e21308a0 100644 --- a/tracks/app/views/todo/list.rhtml +++ b/tracks/app/views/todo/list.rhtml @@ -1,53 +1,94 @@
- <% for @shown_place in @shown_places %> - <% @not_done = Todo.find_all("done=0 AND context_id=#{@shown_place.id}", "due IS NULL, due ASC, created ASC") %> - <% if !@not_done.empty? %> -
-

- - <%= link_to( "#{@shown_place.name.capitalize}", :controller => "context", :action => "show", :name => urlize(@shown_place.name) ) %>

-
- - <%= render_collection_of_partials "not_done", @not_done %> -
+ + + + + + <% for @shown_place in @shown_places -%> + <% @not_done = Todo.find_all("done=0 AND context_id=#{@shown_place.id}", "due IS NULL, due ASC, created ASC") -%> + <% if !@not_done.empty? -%> +
+

+ <%= link_to( "#{@shown_place.name}", :controller => "context", :action => "show", :name => urlize(@shown_place.name) ) %>

+ +
+ <% if @not_done.empty? -%> +

There are no next actions yet in this context

+ <% else -%> + <% for item in @not_done -%> + <%= render_partial "show_items", item %> + <% end -%> + <% end -%> +
+
+ <% end -%> + <% end -%> + +
+

Completed actions in this context

+ +
+ <% if @done.empty? -%> +

There are no completed next actions yet in this context

+ <% else -%> + <% for done_item in @done -%> + <%= render_partial "show_items", done_item %> + <% end -%> + <% end -%>
-
- <% end %> -<% end %> - -
-

Just done

- - -
-
- -
-

Last <%= @no_of_actions %> completed actions

- - - <%= render_collection_of_partials "done", @done %> -
-
- -

Hidden contexts: -<% for @hidden_place in @hidden_places %> - <% num = count_items(@hidden_place) %> - <%= link_to @hidden_place.name.capitalize, :controller=>"context", :action=> "show", :name=> urlize(@hidden_place.name) %><%= ' ('+ num.to_s + ') ' + '|' %> -<% end %> -

- -
+
+
-
- <%= render_partial "todo/item", @item %> - -
+<%= link_to_function( "Add the next action in this context »", "Element.toggle('todo_new_action');Element.toggle('new_actions');Form.focusFirstElement('todo-form-new-action');", {:title => "Add the next action [Alt+n]", :accesskey => "n"}) %> -
+ + +

Active Projects:

+
    + <% for project in @projects -%> +
  • <%= link_to( project.name, { :controller => "project", :action => "show", :name => urlize(project.name) } ) + " (" + Todo.count( "project_id=#{project.id} AND done=0" ).to_s + " actions)" %>
  • + <% end -%> +
+ +

Hidden Contexts:

+
    + <% for other_context in Context.find(:all, :conditions => "hide=1", :order => "position ASC") %> +
  • <%= link_to( other_context.name, { :controller => "context", :action => "show", :name => urlize(other_context.name) } ) + " (" + Todo.count( "context_id=#{other_context.id} AND done=0" ).to_s + " actions)" -%>
  • + <% end -%> +
+ <% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> diff --git a/tracks/config/routes.rb b/tracks/config/routes.rb index 87b33623..cbee3442 100644 --- a/tracks/config/routes.rb +++ b/tracks/config/routes.rb @@ -23,18 +23,19 @@ ActionController::Routing::Routes.draw do |map| map.connect 'delete/todo/:id', :controller =>'todo', :action => 'destroy' # Context Routes + map.connect 'context/new_context', :controller => 'context', :action => 'new_context' + map.connect 'context/add_item', :controller => 'context', :action => 'add_item' map.connect 'contexts', :controller => 'context', :action => 'list' - map.connect 'add/context', :controller => 'context', :action => 'new' - map.connect 'context/:id', :controller=> 'context', :action => 'show' map.connect 'context/:name', :controller => 'context', :action => 'show' - map.connect 'delete/context/:id', :controller => 'context', :action => 'destroy' + map.connect 'context/:id', :controller=> 'context', :action => 'show' # Projects Routes + map.connect 'project/new_project', :controller => 'project', :action => 'new_project' + map.connect 'project/add_item/:id', :controller => 'project', :action => 'add_item' + map.connect 'project/toggle_check/:id', :controller => 'project', :action => 'toggle_check' map.connect 'projects', :controller => 'project', :action => 'list' - map.connect 'add/project', :controller => 'project', :action => 'new' map.connect 'project/:name', :controller => 'project', :action => 'show' map.connect 'project/:id', :controller => 'project', :action => 'show' - map.connect 'delete/project/:id', :controller => 'project', :action => 'destroy' # Feed Routes map.connect 'feed/:action/:name/:user', :controller => 'feed' diff --git a/tracks/config/settings.yml.tmpl b/tracks/config/settings.yml.tmpl index 9cc89fda..5f088a2d 100644 --- a/tracks/config/settings.yml.tmpl +++ b/tracks/config/settings.yml.tmpl @@ -1,5 +1,11 @@ +# It's very important not to use TAB characters in this file +# Use two spaces instead of a TAB +# week_starts is the day you want the calendar to display first (0=Sunday, 1=Monday etc.) +# hp_completed is the number of completed items you want displayed on the home page +# formats: date: %d/%m/%Y + week_starts: 1 hp_completed: 5 admin: email: butshesagirl@rousette.org.uk diff --git a/tracks/doc/CHANGENOTES.txt b/tracks/doc/CHANGENOTES.txt index 227ca0d4..f909984d 100644 --- a/tracks/doc/CHANGENOTES.txt +++ b/tracks/doc/CHANGENOTES.txt @@ -16,7 +16,7 @@ Project wiki: 1. Added back border="0" to images which I had mistakenly taken out (thanks, Adam Hughes) 2. Removed the section in config/environment.rb which requires Redcloth. This was causing errors because Rails now requires Redcloth itself. This means that you now need to have Redcloth installed as a gem (gem install redcloth) (thanks, Jim). 3. Fixed SQLite dump format in db/tracks_1.0.2_sqlite.sql (thanks, Jim) -4. Added a mini-calendar to the todo/list page. Needs some tidying up, but it provides a quick way to look up a date a few months ahead. Note that it doesn't insert the date: it's just for viewing. I modified the calendar a little bit from here: +4. The new item forms on all pages now use a mini calendar which pops up when you click the due date field. Calendar is the GPL one from dynarch.com 5. Added some XMLHTTPRequest calls to speed up checking off an item as done. It grabs the checked item and appends it immediately to a 'holding' section (where you can uncheck it again if it was a mistake, or add a closing note). When you next refresh the page, it will be added to the 'Last 5 completed items' section. 6. [Contributed by Lolindrath] Toggling of contexts in /todo/list to collapse or expand their display via a small '+' or '-' graphic. This is independent of the shown/hidden setting for contexts, and is ideal for just hiding things on the fly to focus your view. 7. [Contributed by Jim Ray] Jim added a host of fixes and bits of cleaning up, including a position column for contexts and projects to allow custom sorting, and changes to the links for pages to make them more human-readable. @@ -25,6 +25,9 @@ Project wiki: 10. [Contributed by Arnaud Limbourg, ticket:18] A new entry in settings.yml allows you to choose the number of completed actions you want to see on the /todo/list home page. Also sorts by due date (ascending) first, then creation date (descending) on /todo/list, /context/show/[name], and /project/show/[name] 11. Added a count of next actions to the /projects page, showing how many uncompleted next actions remain for each project. 12. [Patch by lolindrath] Sorting by date is now much smarter on /todo/list: Actions are sorted by ascending due date then ascending creation date, but non-due dated items sort to the bottom. This means that the most urgent items float to the top of each context list. +13. Can now uncheck actions from the completed actions list, so that they dynamically appear back in the uncompleted actions area. +14. A tiny improvement: the toggling of the individual notes now uses Element.toggle from prototype.js, so it doesn't have to be an onLoad property of the body tag. This means that you don't get the notes flashing before they are hidden when you load or reload a page. +15. All the adding, updating, deleting and marking actions done is performed using Ajax, so happens without needing to refresh the page. ## Version 1.02 diff --git a/tracks/lib/iCal.rb b/tracks/lib/iCal.rb deleted file mode 100644 index 00645ac2..00000000 --- a/tracks/lib/iCal.rb +++ /dev/null @@ -1,289 +0,0 @@ - -require 'date' -require 'ParseDate' - -module ICal - -SECONDS_PER_DAY = 24 * 60 * 60 - -class ICalReader - @@iCalFolder = "/Users/jchappell/Library/Calendars" - - def initialize(calendarName = nil) - @events = [] - @iCalFiles = Dir[@@iCalFolder + "/*.ics"] - @iCalFiles.sort! {|x, y| x.downcase <=> y.downcase} - if calendarName then - fullName = @@iCalFolder + "/" + calendarName + ".ics" - @iCalFiles = @iCalFiles.select {|name| name == fullName} - end - end - - def calendars - @iCalFiles.collect {|name| File.basename(name, ".ics")} - end - - def readEvents - @events = [] - @iCalFiles.each do |fileName| - lines = File.readlines(fileName); - inEvent = false - eventLines = [] - lines.each do |line| - if line =~ /^BEGIN:VEVENT/ then - inEvent = true - eventLines = [] - end - - if inEvent - eventLines << line - if line =~ /^END:VEVENT/ then - inEvent = false - @events << parseEvent(eventLines) - end - end - end - end - @events - end - - def parseEvent(lines) - event = ICalEvent.new() - startDate = nil - rule = nil - - lines.each do |line| - if line =~ /^SUMMARY:(.*)/ then - event.summary = $1 - elsif line =~ /^DTSTART;.*:(.*).*/ then - startDate = parseDate($1) - elsif line =~ /^EXDATE.*:(.*)/ then - event.addExceptionDate(parseDate($1)) - elsif line =~ /^RRULE:(.*)/ then - rule = $1 - end - end - - event.startDate = startDate - event.addRecurrenceRule(rule) - event - end - - def parseDate(dateStr) - # We constrain the year to 1970 because Time won't handle lesser years - # If it is less than 1970 then its probably a birthday or something - # in which case, we don't really care about the year - year = dateStr[0,4].to_i - year = 1970 if year < 1970 - - month = dateStr[4,2].to_i - day = dateStr[6,2].to_i - hour = dateStr[9,2].to_i - minute = dateStr[11,2].to_i - Time.local(year, month, day, hour, minute) - end - - def events - readEvents if @events == [] - @events - end - - def selectEvents(&predicate) - # - # The start date of each event could be different due to recurring events - # Since we can assume the date is the same for all events, we just compare - # the times when sorting - # - now = Time.now - events.select(&predicate).sort do |event1, event2| - time1 = Time.local(now.year, now.month, now.day, event1.startDate.hour, - event1.startDate.min) - time2 = Time.local(now.year, now.month, now.day, event2.startDate.hour, - event2.startDate.min) - time1 <=> time2 - end - end - - def todaysEvents - #events.select {|event| event.startsToday? } - selectEvents {|event| event.startsToday? } - end - - def tomorrowsEvents - #events.select {|event| event.startsTomorrow? } - selectEvents {|event| event.startsTomorrow?} - end - - def eventsFor(date) - #events.select {|event| event.startsOn?(date)} - selectEvents {|event| event.startsOn?(date)} - end -end - -class DateParser - # Given a date as a string, returns a Time object - def DateParser.parse(dateStr) - dateValues = ParseDate::parsedate(dateStr) - Time.local(*dateValues[0, 3]) - end - - def DateParser.format(date) - date.strftime("%m/%d/%Y") - end -end - -class ICalEvent - - def initialize - @exceptionDates = [] - end - - def <=>(otherEvent) - return @startDate <=> otherEvent.startDate - end - - def addExceptionDate(date) - @exceptionDates << date - end - - def addRecurrenceRule(rule) - @dateSet = DateSet.new(@startDate, rule) - end - - def startsToday? - startsOn?(Time.now) - end - - def startsTomorrow? - tomorrow = Time.now + SECONDS_PER_DAY; - startsOn?(tomorrow) - end - - def startsOn?(date) - (startDate.year == date.year and startDate.month == date.month and - startDate.day == date.day) or @dateSet.includes?(date) - end - - def to_s - "#{@startDate.strftime("%m/%d/%Y (%I:%M %p)")} - #{@summary}" - end - - def startTime - @startDate - end - - attr_accessor :startDate, :summary -end - -class DateSet - - def initialize(startDate, rule) - @startDate = startDate - @frequency = nil - @count = nil - @untilDate = nil - @byMonth = nil - @byDay = nil - parseRecurrenceRule(rule) - end - - def parseRecurrenceRule(rule) - - if rule =~ /FREQ=(.*?);/ then - @frequency = $1 - end - - if rule =~ /COUNT=(\d*)/ then - @count = $1.to_i - end - - if rule =~ /UNTIL=(.*?);/ then - @untilDate = DateParser.parse($1) - #puts @untilDate - end - - if rule =~ /INTERVAL=(\d*)/ then - @interval = $1.to_i - end - - if rule =~ /BYMONTH=(.*?);/ then - @byMonth = $1 - end - - if rule =~ /BYDAY=(.*?);/ then - @byDay = $1 - #puts "byDay = #{@byDay}" - end - end - - def to_s - puts "#" - end - - def includes?(date) - return true if date == @startDate - return false if @untilDate and date > @untilDate - - case @frequency - when 'DAILY' - #if @untilDate then - # return (@startDate..@untilDate).include?(date) - #end - increment = @interval ? @interval : 1 - d = @startDate - counter = 0 - until d > date - - if @count then - counter += 1 - if counter >= @count - return false - end - end - - d += (increment * SECONDS_PER_DAY) - if d.day == date.day and - d.year == date.year and - d.month == date.month then - return true - end - - end - - when 'WEEKLY' - return true if @startDate.wday == date.wday - - when 'MONTHLY' - - when 'YEARLY' - end - - false - end - - attr_reader :frequency - attr_accessor :startDate -end - -if $0 == __FILE__ then - #reader = ICalReader.new("Test") - reader = ICalReader.new - - puts - puts "Today" - puts "=====" - puts reader.todaysEvents - - puts - puts "Tomorrow" - puts "========" - puts reader.tomorrowsEvents - - puts - puts "08/14/2003 Events" - puts "=================" - puts reader.eventsFor(Time.local(2003, 8, 14)) - puts -end - -end diff --git a/tracks/public/javascripts/calendar-en.js b/tracks/public/javascripts/calendar-en.js new file mode 100644 index 00000000..0dbde793 --- /dev/null +++ b/tracks/public/javascripts/calendar-en.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "About the calendar"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Go Today"; +Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Select date"; +Calendar._TT["DRAG_TO_MOVE"] = "Drag to move"; +Calendar._TT["PART_TODAY"] = " (today)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "Display %s first"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Close"; +Calendar._TT["TODAY"] = "Today"; +Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Time:"; diff --git a/tracks/public/javascripts/calendar-setup.js b/tracks/public/javascripts/calendar-setup.js new file mode 100644 index 00000000..f2b48543 --- /dev/null +++ b/tracks/public/javascripts/calendar-setup.js @@ -0,0 +1,200 @@ +/* Copyright Mihai Bazon, 2002, 2003 | http://dynarch.com/mishoo/ + * --------------------------------------------------------------------------- + * + * The DHTML Calendar + * + * Details and latest version at: + * http://dynarch.com/mishoo/calendar.epl + * + * This script is distributed under the GNU Lesser General Public License. + * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html + * + * This file defines helper functions for setting up the calendar. They are + * intended to help non-programmers get a working calendar on their site + * quickly. This script should not be seen as part of the calendar. It just + * shows you what one can do with the calendar, while in the same time + * providing a quick and simple method for setting it up. If you need + * exhaustive customization of the calendar creation process feel free to + * modify this code to suit your needs (this is recommended and much better + * than modifying calendar.js itself). + */ + +// $Id: calendar-setup.js,v 1.25 2005/03/07 09:51:33 mishoo Exp $ + +/** + * This function "patches" an input field (or other element) to use a calendar + * widget for date selection. + * + * The "params" is a single object that can have the following properties: + * + * prop. name | description + * ------------------------------------------------------------------------------------------------- + * inputField | the ID of an input field to store the date + * displayArea | the ID of a DIV or other element to show the date + * button | ID of a button or other element that will trigger the calendar + * eventName | event that will trigger the calendar, without the "on" prefix (default: "click") + * ifFormat | date format that will be stored in the input field + * daFormat | the date format that will be used to display the date in displayArea + * singleClick | (true/false) wether the calendar is in single click mode or not (default: true) + * firstDay | numeric: 0 to 6. "0" means display Sunday first, "1" means display Monday first, etc. + * align | alignment (default: "Br"); if you don't know what's this see the calendar documentation + * range | array with 2 elements. Default: [1900, 2999] -- the range of years available + * weekNumbers | (true/false) if it's true (default) the calendar will display week numbers + * flat | null or element ID; if not null the calendar will be a flat calendar having the parent with the given ID + * flatCallback | function that receives a JS Date object and returns an URL to point the browser to (for flat calendar) + * disableFunc | function that receives a JS Date object and should return true if that date has to be disabled in the calendar + * onSelect | function that gets called when a date is selected. You don't _have_ to supply this (the default is generally okay) + * onClose | function that gets called when the calendar is closed. [default] + * onUpdate | function that gets called after the date is updated in the input field. Receives a reference to the calendar. + * date | the date that the calendar will be initially displayed to + * showsTime | default: false; if true the calendar will include a time selector + * timeFormat | the time format; can be "12" or "24", default is "12" + * electric | if true (default) then given fields/date areas are updated for each move; otherwise they're updated only on close + * step | configures the step of the years in drop-down boxes; default: 2 + * position | configures the calendar absolute position; default: null + * cache | if "true" (but default: "false") it will reuse the same calendar object, where possible + * showOthers | if "true" (but default: "false") it will show days from other months too + * + * None of them is required, they all have default values. However, if you + * pass none of "inputField", "displayArea" or "button" you'll get a warning + * saying "nothing to setup". + */ +Calendar.setup = function (params) { + function param_default(pname, def) { if (typeof params[pname] == "undefined") { params[pname] = def; } }; + + param_default("inputField", null); + param_default("displayArea", null); + param_default("button", null); + param_default("eventName", "click"); + param_default("ifFormat", "%Y/%m/%d"); + param_default("daFormat", "%Y/%m/%d"); + param_default("singleClick", true); + param_default("disableFunc", null); + param_default("dateStatusFunc", params["disableFunc"]); // takes precedence if both are defined + param_default("dateText", null); + param_default("firstDay", null); + param_default("align", "Br"); + param_default("range", [1900, 2999]); + param_default("weekNumbers", true); + param_default("flat", null); + param_default("flatCallback", null); + param_default("onSelect", null); + param_default("onClose", null); + param_default("onUpdate", null); + param_default("date", null); + param_default("showsTime", false); + param_default("timeFormat", "24"); + param_default("electric", true); + param_default("step", 2); + param_default("position", null); + param_default("cache", false); + param_default("showOthers", false); + param_default("multiple", null); + + var tmp = ["inputField", "displayArea", "button"]; + for (var i in tmp) { + if (typeof params[tmp[i]] == "string") { + params[tmp[i]] = document.getElementById(params[tmp[i]]); + } + } + if (!(params.flat || params.multiple || params.inputField || params.displayArea || params.button)) { + alert("Calendar.setup:\n Nothing to setup (no fields found). Please check your code"); + return false; + } + + function onSelect(cal) { + var p = cal.params; + var update = (cal.dateClicked || p.electric); + if (update && p.inputField) { + p.inputField.value = cal.date.print(p.ifFormat); + if (typeof p.inputField.onchange == "function") + p.inputField.onchange(); + } + if (update && p.displayArea) + p.displayArea.innerHTML = cal.date.print(p.daFormat); + if (update && typeof p.onUpdate == "function") + p.onUpdate(cal); + if (update && p.flat) { + if (typeof p.flatCallback == "function") + p.flatCallback(cal); + } + if (update && p.singleClick && cal.dateClicked) + cal.callCloseHandler(); + }; + + if (params.flat != null) { + if (typeof params.flat == "string") + params.flat = document.getElementById(params.flat); + if (!params.flat) { + alert("Calendar.setup:\n Flat specified but can't find parent."); + return false; + } + var cal = new Calendar(params.firstDay, params.date, params.onSelect || onSelect); + cal.showsOtherMonths = params.showOthers; + cal.showsTime = params.showsTime; + cal.time24 = (params.timeFormat == "24"); + cal.params = params; + cal.weekNumbers = params.weekNumbers; + cal.setRange(params.range[0], params.range[1]); + cal.setDateStatusHandler(params.dateStatusFunc); + cal.getDateText = params.dateText; + if (params.ifFormat) { + cal.setDateFormat(params.ifFormat); + } + if (params.inputField && typeof params.inputField.value == "string") { + cal.parseDate(params.inputField.value); + } + cal.create(params.flat); + cal.show(); + return false; + } + + var triggerEl = params.button || params.displayArea || params.inputField; + triggerEl["on" + params.eventName] = function() { + var dateEl = params.inputField || params.displayArea; + var dateFmt = params.inputField ? params.ifFormat : params.daFormat; + var mustCreate = false; + var cal = window.calendar; + if (dateEl) + params.date = Date.parseDate(dateEl.value || dateEl.innerHTML, dateFmt); + if (!(cal && params.cache)) { + window.calendar = cal = new Calendar(params.firstDay, + params.date, + params.onSelect || onSelect, + params.onClose || function(cal) { cal.hide(); }); + cal.showsTime = params.showsTime; + cal.time24 = (params.timeFormat == "24"); + cal.weekNumbers = params.weekNumbers; + mustCreate = true; + } else { + if (params.date) + cal.setDate(params.date); + cal.hide(); + } + if (params.multiple) { + cal.multiple = {}; + for (var i = params.multiple.length; --i >= 0;) { + var d = params.multiple[i]; + var ds = d.print("%Y%m%d"); + cal.multiple[ds] = d; + } + } + cal.showsOtherMonths = params.showOthers; + cal.yearStep = params.step; + cal.setRange(params.range[0], params.range[1]); + cal.params = params; + cal.setDateStatusHandler(params.dateStatusFunc); + cal.getDateText = params.dateText; + cal.setDateFormat(dateFmt); + if (mustCreate) + cal.create(); + cal.refresh(); + if (!params.position) + cal.showAtElement(params.button || params.displayArea || params.inputField, params.align); + else + cal.showAt(params.position[0], params.position[1]); + return false; + }; + + return cal; +}; diff --git a/tracks/public/javascripts/calendar.js b/tracks/public/javascripts/calendar.js index e47c6899..9088e0e8 100644 --- a/tracks/public/javascripts/calendar.js +++ b/tracks/public/javascripts/calendar.js @@ -1,137 +1,1806 @@ -// Popup calendar date picker -// Written by Michele Tranquilli of pxl8.com -// +/* Copyright Mihai Bazon, 2002-2005 | www.bazon.net/mishoo + * ----------------------------------------------------------- + * + * The DHTML Calendar, version 1.0 "It is happening again" + * + * Details and latest version at: + * www.dynarch.com/projects/calendar + * + * This script is developed by Dynarch.com. Visit us at www.dynarch.com. + * + * This script is distributed under the GNU Lesser General Public License. + * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html + */ -function Calendar_get_daysofmonth(monthNo, p_year) { - if ((p_year % 4) == 0) { - if ((p_year % 100) == 0 && (p_year % 400) != 0) - return DOMonth[monthNo]; - return lDOMonth[monthNo]; - } else - return DOMonth[monthNo]; -} -// -- globals used with calendar and date functions -var calField; var calSpan; var calFormat; var calWknd = false; -/* ^^^^^^ end BASIC DATE STARTER-SET ^^^^^^ */ -function getNextMonth(m,y,incr){ - var ret_arr = new Array(); - ret_arr[0] = m + incr; ret_arr[1] = y; - if (ret_arr[0] == 12){ ret_arr[0]=0; ret_arr[1]=ret_arr[1]+1; } - if (ret_arr[0] == -1){ ret_arr[0]=11; ret_arr[1]=ret_arr[1]-1; } - return ret_arr; -} -function figureDOTW(m,d,y){ - var tDate = new Date(); tDate.setDate(d); tDate.setMonth(m); tDate.setFullYear(y); return tDate.getDay(); -} -function scramKids(n){ // this is a basic removeChild loop for removing all childNodes from node n - var numKids = n.childNodes.length; - for (i=0;i monthNo) // cells before the first of the month or after the last day - cellContent = document.createElement('br'); - else { - var dyA = document.createElement('a'); - dyA.setAttribute('href','javascript:placeDate('+m+','+dayCount+','+y+')'); - - calTDtext = document.createTextNode(dayCount.toString()); - cellContent = calTDtext; - if (dayCount == curr_dy && m == curr_mn && y == curr_yr) - calTD.style.backgroundColor = '#FFFF99'; - if ((j!=0 && j!=6) || calWknd == true){ // if the day is a weekday or weekends allowed - if (dayCount == curr_dy && m == curr_mn && y == curr_yr && calSpan != 3 && calSpan != 0 && calSpan != 4){ - dyA.appendChild(calTDtext); cellContent = dyA; - } - if (calSpan == 1 || calSpan == 4){ - if (y < curr_yr || (m < curr_mn && y == curr_yr) || (m == curr_mn && y == curr_yr && dayCount < curr_dy)) - { - dyA.appendChild(calTDtext); cellContent = dyA; - } - } - if (calSpan == 2 || calSpan == 3){ - if (y > curr_yr || (m > curr_mn && y == curr_yr) || (m == curr_mn && y == curr_yr && dayCount > curr_dy)) - {dyA.appendChild(calTDtext); cellContent = dyA;} - } - if (calSpan == 5){ - dyA.appendChild(calTDtext); cellContent = dyA; +// $Id: calendar.js,v 1.51 2005/03/07 16:44:31 mishoo Exp $ + +/** The Calendar object constructor. */ +Calendar = function (firstDayOfWeek, dateStr, onSelected, onClose) { + // member variables + this.activeDiv = null; + this.currentDateEl = null; + this.getDateStatus = null; + this.getDateToolTip = null; + this.getDateText = null; + this.timeout = null; + this.onSelected = onSelected || null; + this.onClose = onClose || null; + this.dragging = false; + this.hidden = false; + this.minYear = 1970; + this.maxYear = 2050; + this.dateFormat = Calendar._TT["DEF_DATE_FORMAT"]; + this.ttDateFormat = Calendar._TT["TT_DATE_FORMAT"]; + this.isPopup = true; + this.weekNumbers = true; + this.firstDayOfWeek = typeof firstDayOfWeek == "number" ? firstDayOfWeek : Calendar._FD; // 0 for Sunday, 1 for Monday, etc. + this.showsOtherMonths = false; + this.dateStr = dateStr; + this.ar_days = null; + this.showsTime = false; + this.time24 = true; + this.yearStep = 2; + this.hiliteToday = true; + this.multiple = null; + // HTML elements + this.table = null; + this.element = null; + this.tbody = null; + this.firstdayname = null; + // Combo boxes + this.monthsCombo = null; + this.yearsCombo = null; + this.hilitedMonth = null; + this.activeMonth = null; + this.hilitedYear = null; + this.activeYear = null; + // Information + this.dateClicked = false; + + // one-time initializations + if (typeof Calendar._SDN == "undefined") { + // table of short day names + if (typeof Calendar._SDN_len == "undefined") + Calendar._SDN_len = 3; + var ar = new Array(); + for (var i = 8; i > 0;) { + ar[--i] = Calendar._DN[i].substr(0, Calendar._SDN_len); + } + Calendar._SDN = ar; + // table of short month names + if (typeof Calendar._SMN_len == "undefined") + Calendar._SMN_len = 3; + ar = new Array(); + for (var i = 12; i > 0;) { + ar[--i] = Calendar._MN[i].substr(0, Calendar._SMN_len); + } + Calendar._SMN = ar; + } +}; + +// ** constants + +/// "static", needed for event handlers. +Calendar._C = null; + +/// detect a special case of "web browser" +Calendar.is_ie = ( /msie/i.test(navigator.userAgent) && + !/opera/i.test(navigator.userAgent) ); + +Calendar.is_ie5 = ( Calendar.is_ie && /msie 5\.0/i.test(navigator.userAgent) ); + +/// detect Opera browser +Calendar.is_opera = /opera/i.test(navigator.userAgent); + +/// detect KHTML-based browsers +Calendar.is_khtml = /Konqueror|Safari|KHTML/i.test(navigator.userAgent); + +// BEGIN: UTILITY FUNCTIONS; beware that these might be moved into a separate +// library, at some point. + +Calendar.getAbsolutePos = function(el) { + var SL = 0, ST = 0; + var is_div = /^div$/i.test(el.tagName); + if (is_div && el.scrollLeft) + SL = el.scrollLeft; + if (is_div && el.scrollTop) + ST = el.scrollTop; + var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST }; + if (el.offsetParent) { + var tmp = this.getAbsolutePos(el.offsetParent); + r.x += tmp.x; + r.y += tmp.y; + } + return r; +}; + +Calendar.isRelated = function (el, evt) { + var related = evt.relatedTarget; + if (!related) { + var type = evt.type; + if (type == "mouseover") { + related = evt.fromElement; + } else if (type == "mouseout") { + related = evt.toElement; + } + } + while (related) { + if (related == el) { + return true; + } + related = related.parentNode; + } + return false; +}; + +Calendar.removeClass = function(el, className) { + if (!(el && el.className)) { + return; + } + var cls = el.className.split(" "); + var ar = new Array(); + for (var i = cls.length; i > 0;) { + if (cls[--i] != className) { + ar[ar.length] = cls[i]; + } + } + el.className = ar.join(" "); +}; + +Calendar.addClass = function(el, className) { + Calendar.removeClass(el, className); + el.className += " " + className; +}; + +// FIXME: the following 2 functions totally suck, are useless and should be replaced immediately. +Calendar.getElement = function(ev) { + var f = Calendar.is_ie ? window.event.srcElement : ev.currentTarget; + while (f.nodeType != 1 || /^div$/i.test(f.tagName)) + f = f.parentNode; + return f; +}; + +Calendar.getTargetElement = function(ev) { + var f = Calendar.is_ie ? window.event.srcElement : ev.target; + while (f.nodeType != 1) + f = f.parentNode; + return f; +}; + +Calendar.stopEvent = function(ev) { + ev || (ev = window.event); + if (Calendar.is_ie) { + ev.cancelBubble = true; + ev.returnValue = false; + } else { + ev.preventDefault(); + ev.stopPropagation(); + } + return false; +}; + +Calendar.addEvent = function(el, evname, func) { + if (el.attachEvent) { // IE + el.attachEvent("on" + evname, func); + } else if (el.addEventListener) { // Gecko / W3C + el.addEventListener(evname, func, true); + } else { + el["on" + evname] = func; + } +}; + +Calendar.removeEvent = function(el, evname, func) { + if (el.detachEvent) { // IE + el.detachEvent("on" + evname, func); + } else if (el.removeEventListener) { // Gecko / W3C + el.removeEventListener(evname, func, true); + } else { + el["on" + evname] = null; + } +}; + +Calendar.createElement = function(type, parent) { + var el = null; + if (document.createElementNS) { + // use the XHTML namespace; IE won't normally get here unless + // _they_ "fix" the DOM2 implementation. + el = document.createElementNS("http://www.w3.org/1999/xhtml", type); + } else { + el = document.createElement(type); + } + if (typeof parent != "undefined") { + parent.appendChild(el); + } + return el; +}; + +// END: UTILITY FUNCTIONS + +// BEGIN: CALENDAR STATIC FUNCTIONS + +/** Internal -- adds a set of events to make some element behave like a button. */ +Calendar._add_evs = function(el) { + with (Calendar) { + addEvent(el, "mouseover", dayMouseOver); + addEvent(el, "mousedown", dayMouseDown); + addEvent(el, "mouseout", dayMouseOut); + if (is_ie) { + addEvent(el, "dblclick", dayMouseDblClick); + el.setAttribute("unselectable", true); + } + } +}; + +Calendar.findMonth = function(el) { + if (typeof el.month != "undefined") { + return el; + } else if (typeof el.parentNode.month != "undefined") { + return el.parentNode; + } + return null; +}; + +Calendar.findYear = function(el) { + if (typeof el.year != "undefined") { + return el; + } else if (typeof el.parentNode.year != "undefined") { + return el.parentNode; + } + return null; +}; + +Calendar.showMonthsCombo = function () { + var cal = Calendar._C; + if (!cal) { + return false; + } + var cal = cal; + var cd = cal.activeDiv; + var mc = cal.monthsCombo; + if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + if (cal.activeMonth) { + Calendar.removeClass(cal.activeMonth, "active"); + } + var mon = cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()]; + Calendar.addClass(mon, "active"); + cal.activeMonth = mon; + var s = mc.style; + s.display = "block"; + if (cd.navtype < 0) + s.left = cd.offsetLeft + "px"; + else { + var mcw = mc.offsetWidth; + if (typeof mcw == "undefined") + // Konqueror brain-dead techniques + mcw = 50; + s.left = (cd.offsetLeft + cd.offsetWidth - mcw) + "px"; + } + s.top = (cd.offsetTop + cd.offsetHeight) + "px"; +}; + +Calendar.showYearsCombo = function (fwd) { + var cal = Calendar._C; + if (!cal) { + return false; + } + var cal = cal; + var cd = cal.activeDiv; + var yc = cal.yearsCombo; + if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + if (cal.activeYear) { + Calendar.removeClass(cal.activeYear, "active"); + } + cal.activeYear = null; + var Y = cal.date.getFullYear() + (fwd ? 1 : -1); + var yr = yc.firstChild; + var show = false; + for (var i = 12; i > 0; --i) { + if (Y >= cal.minYear && Y <= cal.maxYear) { + yr.innerHTML = Y; + yr.year = Y; + yr.style.display = "block"; + show = true; + } else { + yr.style.display = "none"; + } + yr = yr.nextSibling; + Y += fwd ? cal.yearStep : -cal.yearStep; + } + if (show) { + var s = yc.style; + s.display = "block"; + if (cd.navtype < 0) + s.left = cd.offsetLeft + "px"; + else { + var ycw = yc.offsetWidth; + if (typeof ycw == "undefined") + // Konqueror brain-dead techniques + ycw = 50; + s.left = (cd.offsetLeft + cd.offsetWidth - ycw) + "px"; + } + s.top = (cd.offsetTop + cd.offsetHeight) + "px"; + } +}; + +// event handlers + +Calendar.tableMouseUp = function(ev) { + var cal = Calendar._C; + if (!cal) { + return false; + } + if (cal.timeout) { + clearTimeout(cal.timeout); + } + var el = cal.activeDiv; + if (!el) { + return false; + } + var target = Calendar.getTargetElement(ev); + ev || (ev = window.event); + Calendar.removeClass(el, "active"); + if (target == el || target.parentNode == el) { + Calendar.cellClick(el, ev); + } + var mon = Calendar.findMonth(target); + var date = null; + if (mon) { + date = new Date(cal.date); + if (mon.month != date.getMonth()) { + date.setMonth(mon.month); + cal.setDate(date); + cal.dateClicked = false; + cal.callHandler(); + } + } else { + var year = Calendar.findYear(target); + if (year) { + date = new Date(cal.date); + if (year.year != date.getFullYear()) { + date.setFullYear(year.year); + cal.setDate(date); + cal.dateClicked = false; + cal.callHandler(); + } + } + } + with (Calendar) { + removeEvent(document, "mouseup", tableMouseUp); + removeEvent(document, "mouseover", tableMouseOver); + removeEvent(document, "mousemove", tableMouseOver); + cal._hideCombos(); + _C = null; + return stopEvent(ev); + } +}; + +Calendar.tableMouseOver = function (ev) { + var cal = Calendar._C; + if (!cal) { + return; + } + var el = cal.activeDiv; + var target = Calendar.getTargetElement(ev); + if (target == el || target.parentNode == el) { + Calendar.addClass(el, "hilite active"); + Calendar.addClass(el.parentNode, "rowhilite"); + } else { + if (typeof el.navtype == "undefined" || (el.navtype != 50 && (el.navtype == 0 || Math.abs(el.navtype) > 2))) + Calendar.removeClass(el, "active"); + Calendar.removeClass(el, "hilite"); + Calendar.removeClass(el.parentNode, "rowhilite"); + } + ev || (ev = window.event); + if (el.navtype == 50 && target != el) { + var pos = Calendar.getAbsolutePos(el); + var w = el.offsetWidth; + var x = ev.clientX; + var dx; + var decrease = true; + if (x > pos.x + w) { + dx = x - pos.x - w; + decrease = false; + } else + dx = pos.x - x; + + if (dx < 0) dx = 0; + var range = el._range; + var current = el._current; + var count = Math.floor(dx / 10) % range.length; + for (var i = range.length; --i >= 0;) + if (range[i] == current) + break; + while (count-- > 0) + if (decrease) { + if (--i < 0) + i = range.length - 1; + } else if ( ++i >= range.length ) + i = 0; + var newval = range[i]; + el.innerHTML = newval; + + cal.onUpdateTime(); + } + var mon = Calendar.findMonth(target); + if (mon) { + if (mon.month != cal.date.getMonth()) { + if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + Calendar.addClass(mon, "hilite"); + cal.hilitedMonth = mon; + } else if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + } else { + if (cal.hilitedMonth) { + Calendar.removeClass(cal.hilitedMonth, "hilite"); + } + var year = Calendar.findYear(target); + if (year) { + if (year.year != cal.date.getFullYear()) { + if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + Calendar.addClass(year, "hilite"); + cal.hilitedYear = year; + } else if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + } else if (cal.hilitedYear) { + Calendar.removeClass(cal.hilitedYear, "hilite"); + } + } + return Calendar.stopEvent(ev); +}; + +Calendar.tableMouseDown = function (ev) { + if (Calendar.getTargetElement(ev) == Calendar.getElement(ev)) { + return Calendar.stopEvent(ev); + } +}; + +Calendar.calDragIt = function (ev) { + var cal = Calendar._C; + if (!(cal && cal.dragging)) { + return false; + } + var posX; + var posY; + if (Calendar.is_ie) { + posY = window.event.clientY + document.body.scrollTop; + posX = window.event.clientX + document.body.scrollLeft; + } else { + posX = ev.pageX; + posY = ev.pageY; + } + cal.hideShowCovered(); + var st = cal.element.style; + st.left = (posX - cal.xOffs) + "px"; + st.top = (posY - cal.yOffs) + "px"; + return Calendar.stopEvent(ev); +}; + +Calendar.calDragEnd = function (ev) { + var cal = Calendar._C; + if (!cal) { + return false; + } + cal.dragging = false; + with (Calendar) { + removeEvent(document, "mousemove", calDragIt); + removeEvent(document, "mouseup", calDragEnd); + tableMouseUp(ev); + } + cal.hideShowCovered(); +}; + +Calendar.dayMouseDown = function(ev) { + var el = Calendar.getElement(ev); + if (el.disabled) { + return false; + } + var cal = el.calendar; + cal.activeDiv = el; + Calendar._C = cal; + if (el.navtype != 300) with (Calendar) { + if (el.navtype == 50) { + el._current = el.innerHTML; + addEvent(document, "mousemove", tableMouseOver); + } else + addEvent(document, Calendar.is_ie5 ? "mousemove" : "mouseover", tableMouseOver); + addClass(el, "hilite active"); + addEvent(document, "mouseup", tableMouseUp); + } else if (cal.isPopup) { + cal._dragStart(ev); + } + if (el.navtype == -1 || el.navtype == 1) { + if (cal.timeout) clearTimeout(cal.timeout); + cal.timeout = setTimeout("Calendar.showMonthsCombo()", 250); + } else if (el.navtype == -2 || el.navtype == 2) { + if (cal.timeout) clearTimeout(cal.timeout); + cal.timeout = setTimeout((el.navtype > 0) ? "Calendar.showYearsCombo(true)" : "Calendar.showYearsCombo(false)", 250); + } else { + cal.timeout = null; + } + return Calendar.stopEvent(ev); +}; + +Calendar.dayMouseDblClick = function(ev) { + Calendar.cellClick(Calendar.getElement(ev), ev || window.event); + if (Calendar.is_ie) { + document.selection.empty(); + } +}; + +Calendar.dayMouseOver = function(ev) { + var el = Calendar.getElement(ev); + if (Calendar.isRelated(el, ev) || Calendar._C || el.disabled) { + return false; + } + if (el.ttip) { + if (el.ttip.substr(0, 1) == "_") { + el.ttip = el.caldate.print(el.calendar.ttDateFormat) + el.ttip.substr(1); + } + el.calendar.tooltips.innerHTML = el.ttip; + } + if (el.navtype != 300) { + Calendar.addClass(el, "hilite"); + if (el.caldate) { + Calendar.addClass(el.parentNode, "rowhilite"); + } + } + return Calendar.stopEvent(ev); +}; + +Calendar.dayMouseOut = function(ev) { + with (Calendar) { + var el = getElement(ev); + if (isRelated(el, ev) || _C || el.disabled) + return false; + removeClass(el, "hilite"); + if (el.caldate) + removeClass(el.parentNode, "rowhilite"); + if (el.calendar) + el.calendar.tooltips.innerHTML = _TT["SEL_DATE"]; + return stopEvent(ev); + } +}; + +/** + * A generic "click" handler :) handles all types of buttons defined in this + * calendar. + */ +Calendar.cellClick = function(el, ev) { + var cal = el.calendar; + var closing = false; + var newdate = false; + var date = null; + if (typeof el.navtype == "undefined") { + if (cal.currentDateEl) { + Calendar.removeClass(cal.currentDateEl, "selected"); + Calendar.addClass(el, "selected"); + closing = (cal.currentDateEl == el); + if (!closing) { + cal.currentDateEl = el; + } + } + cal.date.setDateOnly(el.caldate); + date = cal.date; + var other_month = !(cal.dateClicked = !el.otherMonth); + if (!other_month && !cal.currentDateEl) + cal._toggleMultipleDate(new Date(date)); + else + newdate = !el.disabled; + // a date was clicked + if (other_month) + cal._init(cal.firstDayOfWeek, date); + } else { + if (el.navtype == 200) { + Calendar.removeClass(el, "hilite"); + cal.callCloseHandler(); + return; + } + date = new Date(cal.date); + if (el.navtype == 0) + date.setDateOnly(new Date()); // TODAY + // unless "today" was clicked, we assume no date was clicked so + // the selected handler will know not to close the calenar when + // in single-click mode. + // cal.dateClicked = (el.navtype == 0); + cal.dateClicked = false; + var year = date.getFullYear(); + var mon = date.getMonth(); + function setMonth(m) { + var day = date.getDate(); + var max = date.getMonthDays(m); + if (day > max) { + date.setDate(max); + } + date.setMonth(m); + }; + switch (el.navtype) { + case 400: + Calendar.removeClass(el, "hilite"); + var text = Calendar._TT["ABOUT"]; + if (typeof text != "undefined") { + text += cal.showsTime ? Calendar._TT["ABOUT_TIME"] : ""; + } else { + // FIXME: this should be removed as soon as lang files get updated! + text = "Help and about box text is not translated into this language.\n" + + "If you know this language and you feel generous please update\n" + + "the corresponding file in \"lang\" subdir to match calendar-en.js\n" + + "and send it back to to get it into the distribution ;-)\n\n" + + "Thank you!\n" + + "http://dynarch.com/mishoo/calendar.epl\n"; + } + alert(text); + return; + case -2: + if (year > cal.minYear) { + date.setFullYear(year - 1); + } + break; + case -1: + if (mon > 0) { + setMonth(mon - 1); + } else if (year-- > cal.minYear) { + date.setFullYear(year); + setMonth(11); + } + break; + case 1: + if (mon < 11) { + setMonth(mon + 1); + } else if (year < cal.maxYear) { + date.setFullYear(year + 1); + setMonth(0); + } + break; + case 2: + if (year < cal.maxYear) { + date.setFullYear(year + 1); + } + break; + case 100: + cal.setFirstDayOfWeek(el.fdow); + return; + case 50: + var range = el._range; + var current = el.innerHTML; + for (var i = range.length; --i >= 0;) + if (range[i] == current) + break; + if (ev && ev.shiftKey) { + if (--i < 0) + i = range.length - 1; + } else if ( ++i >= range.length ) + i = 0; + var newval = range[i]; + el.innerHTML = newval; + cal.onUpdateTime(); + return; + case 0: + // TODAY will bring us here + if ((typeof cal.getDateStatus == "function") && + cal.getDateStatus(date, date.getFullYear(), date.getMonth(), date.getDate())) { + return false; + } + break; + } + if (!date.equalsTo(cal.date)) { + cal.setDate(date); + newdate = true; + } else if (el.navtype == 0) + newdate = closing = true; + } + if (newdate) { + ev && cal.callHandler(); + } + if (closing) { + Calendar.removeClass(el, "hilite"); + ev && cal.callCloseHandler(); + } +}; + +// END: CALENDAR STATIC FUNCTIONS + +// BEGIN: CALENDAR OBJECT FUNCTIONS + +/** + * This function creates the calendar inside the given parent. If _par is + * null than it creates a popup calendar inside the BODY element. If _par is + * an element, be it BODY, then it creates a non-popup calendar (still + * hidden). Some properties need to be set before calling this function. + */ +Calendar.prototype.create = function (_par) { + var parent = null; + if (! _par) { + // default parent is the document body, in which case we create + // a popup calendar. + parent = document.getElementsByTagName("body")[0]; + this.isPopup = true; + } else { + parent = _par; + this.isPopup = false; + } + this.date = this.dateStr ? new Date(this.dateStr) : new Date(); + + var table = Calendar.createElement("table"); + this.table = table; + table.cellSpacing = 0; + table.cellPadding = 0; + table.calendar = this; + Calendar.addEvent(table, "mousedown", Calendar.tableMouseDown); + + var div = Calendar.createElement("div"); + this.element = div; + div.className = "calendar"; + if (this.isPopup) { + div.style.position = "absolute"; + div.style.display = "none"; + } + div.appendChild(table); + + var thead = Calendar.createElement("thead", table); + var cell = null; + var row = null; + + var cal = this; + var hh = function (text, cs, navtype) { + cell = Calendar.createElement("td", row); + cell.colSpan = cs; + cell.className = "button"; + if (navtype != 0 && Math.abs(navtype) <= 2) + cell.className += " nav"; + Calendar._add_evs(cell); + cell.calendar = cal; + cell.navtype = navtype; + cell.innerHTML = "
" + text + "
"; + return cell; + }; + + row = Calendar.createElement("tr", thead); + var title_length = 6; + (this.isPopup) && --title_length; + (this.weekNumbers) && ++title_length; + + hh("?", 1, 400).ttip = Calendar._TT["INFO"]; + this.title = hh("", title_length, 300); + this.title.className = "title"; + if (this.isPopup) { + this.title.ttip = Calendar._TT["DRAG_TO_MOVE"]; + this.title.style.cursor = "move"; + hh("×", 1, 200).ttip = Calendar._TT["CLOSE"]; + } + + row = Calendar.createElement("tr", thead); + row.className = "headrow"; + + this._nav_py = hh("«", 1, -2); + this._nav_py.ttip = Calendar._TT["PREV_YEAR"]; + + this._nav_pm = hh("‹", 1, -1); + this._nav_pm.ttip = Calendar._TT["PREV_MONTH"]; + + this._nav_now = hh(Calendar._TT["TODAY"], this.weekNumbers ? 4 : 3, 0); + this._nav_now.ttip = Calendar._TT["GO_TODAY"]; + + this._nav_nm = hh("›", 1, 1); + this._nav_nm.ttip = Calendar._TT["NEXT_MONTH"]; + + this._nav_ny = hh("»", 1, 2); + this._nav_ny.ttip = Calendar._TT["NEXT_YEAR"]; + + // day names + row = Calendar.createElement("tr", thead); + row.className = "daynames"; + if (this.weekNumbers) { + cell = Calendar.createElement("td", row); + cell.className = "name wn"; + cell.innerHTML = Calendar._TT["WK"]; + } + for (var i = 7; i > 0; --i) { + cell = Calendar.createElement("td", row); + if (!i) { + cell.navtype = 100; + cell.calendar = this; + Calendar._add_evs(cell); + } + } + this.firstdayname = (this.weekNumbers) ? row.firstChild.nextSibling : row.firstChild; + this._displayWeekdays(); + + var tbody = Calendar.createElement("tbody", table); + this.tbody = tbody; + + for (i = 6; i > 0; --i) { + row = Calendar.createElement("tr", tbody); + if (this.weekNumbers) { + cell = Calendar.createElement("td", row); + } + for (var j = 7; j > 0; --j) { + cell = Calendar.createElement("td", row); + cell.calendar = this; + Calendar._add_evs(cell); + } + } + + if (this.showsTime) { + row = Calendar.createElement("tr", tbody); + row.className = "time"; + + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = 2; + cell.innerHTML = Calendar._TT["TIME"] || " "; + + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = this.weekNumbers ? 4 : 3; + + (function(){ + function makeTimePart(className, init, range_start, range_end) { + var part = Calendar.createElement("span", cell); + part.className = className; + part.innerHTML = init; + part.calendar = cal; + part.ttip = Calendar._TT["TIME_PART"]; + part.navtype = 50; + part._range = []; + if (typeof range_start != "number") + part._range = range_start; + else { + for (var i = range_start; i <= range_end; ++i) { + var txt; + if (i < 10 && range_end >= 10) txt = '0' + i; + else txt = '' + i; + part._range[part._range.length] = txt; } } - else { /* else if it's a weekend */ } - dayCount++; - } - calTD.appendChild(cellContent); - calTD.setAttribute('width','14%'); - calTR.appendChild(calTD); + Calendar._add_evs(part); + return part; + }; + var hrs = cal.date.getHours(); + var mins = cal.date.getMinutes(); + var t12 = !cal.time24; + var pm = (hrs > 12); + if (t12 && pm) hrs -= 12; + var H = makeTimePart("hour", hrs, t12 ? 1 : 0, t12 ? 12 : 23); + var span = Calendar.createElement("span", cell); + span.innerHTML = ":"; + span.className = "colon"; + var M = makeTimePart("minute", mins, 0, 59); + var AP = null; + cell = Calendar.createElement("td", row); + cell.className = "time"; + cell.colSpan = 2; + if (t12) + AP = makeTimePart("ampm", pm ? "pm" : "am", ["am", "pm"]); + else + cell.innerHTML = " "; + + cal.onSetTime = function() { + var pm, hrs = this.date.getHours(), + mins = this.date.getMinutes(); + if (t12) { + pm = (hrs >= 12); + if (pm) hrs -= 12; + if (hrs == 0) hrs = 12; + AP.innerHTML = pm ? "pm" : "am"; + } + H.innerHTML = (hrs < 10) ? ("0" + hrs) : hrs; + M.innerHTML = (mins < 10) ? ("0" + mins) : mins; + }; + + cal.onUpdateTime = function() { + var date = this.date; + var h = parseInt(H.innerHTML, 10); + if (t12) { + if (/pm/i.test(AP.innerHTML) && h < 12) + h += 12; + else if (/am/i.test(AP.innerHTML) && h == 12) + h = 0; + } + var d = date.getDate(); + var m = date.getMonth(); + var y = date.getFullYear(); + date.setHours(h); + date.setMinutes(parseInt(M.innerHTML, 10)); + date.setFullYear(y); + date.setMonth(m); + date.setDate(d); + this.dateClicked = false; + this.callHandler(); + }; + })(); + } else { + this.onSetTime = this.onUpdateTime = function() {}; + } + + var tfoot = Calendar.createElement("tfoot", table); + + row = Calendar.createElement("tr", tfoot); + row.className = "footrow"; + + cell = hh(Calendar._TT["SEL_DATE"], this.weekNumbers ? 8 : 7, 300); + cell.className = "ttip"; + if (this.isPopup) { + cell.ttip = Calendar._TT["DRAG_TO_MOVE"]; + cell.style.cursor = "move"; + } + this.tooltips = cell; + + div = Calendar.createElement("div", this.element); + this.monthsCombo = div; + div.className = "combo"; + for (i = 0; i < Calendar._MN.length; ++i) { + var mn = Calendar.createElement("div"); + mn.className = Calendar.is_ie ? "label-IEfix" : "label"; + mn.month = i; + mn.innerHTML = Calendar._SMN[i]; + div.appendChild(mn); + } + + div = Calendar.createElement("div", this.element); + this.yearsCombo = div; + div.className = "combo"; + for (i = 12; i > 0; --i) { + var yr = Calendar.createElement("div"); + yr.className = Calendar.is_ie ? "label-IEfix" : "label"; + div.appendChild(yr); + } + + this._init(this.firstDayOfWeek, this.date); + parent.appendChild(this.element); +}; + +/** keyboard navigation, only for popup calendars */ +Calendar._keyEvent = function(ev) { + var cal = window._dynarch_popupCalendar; + if (!cal || cal.multiple) + return false; + (Calendar.is_ie) && (ev = window.event); + var act = (Calendar.is_ie || ev.type == "keypress"), + K = ev.keyCode; + if (ev.ctrlKey) { + switch (K) { + case 37: // KEY left + act && Calendar.cellClick(cal._nav_pm); + break; + case 38: // KEY up + act && Calendar.cellClick(cal._nav_py); + break; + case 39: // KEY right + act && Calendar.cellClick(cal._nav_nm); + break; + case 40: // KEY down + act && Calendar.cellClick(cal._nav_ny); + break; + default: + return false; } - calTB.appendChild(calTR); + } else switch (K) { + case 32: // KEY space (now) + Calendar.cellClick(cal._nav_now); + break; + case 27: // KEY esc + act && cal.callCloseHandler(); + break; + case 37: // KEY left + case 38: // KEY up + case 39: // KEY right + case 40: // KEY down + if (act) { + var prev, x, y, ne, el, step; + prev = K == 37 || K == 38; + step = (K == 37 || K == 39) ? 1 : 7; + function setVars() { + el = cal.currentDateEl; + var p = el.pos; + x = p & 15; + y = p >> 4; + ne = cal.ar_days[y][x]; + };setVars(); + function prevMonth() { + var date = new Date(cal.date); + date.setDate(date.getDate() - step); + cal.setDate(date); + }; + function nextMonth() { + var date = new Date(cal.date); + date.setDate(date.getDate() + step); + cal.setDate(date); + }; + while (1) { + switch (K) { + case 37: // KEY left + if (--x >= 0) + ne = cal.ar_days[y][x]; + else { + x = 6; + K = 38; + continue; + } + break; + case 38: // KEY up + if (--y >= 0) + ne = cal.ar_days[y][x]; + else { + prevMonth(); + setVars(); + } + break; + case 39: // KEY right + if (++x < 7) + ne = cal.ar_days[y][x]; + else { + x = 0; + K = 40; + continue; + } + break; + case 40: // KEY down + if (++y < cal.ar_days.length) + ne = cal.ar_days[y][x]; + else { + nextMonth(); + setVars(); + } + break; + } + break; + } + if (ne) { + if (!ne.disabled) + Calendar.cellClick(ne); + else if (prev) + prevMonth(); + else + nextMonth(); + } + } + break; + case 13: // KEY enter + if (act) + Calendar.cellClick(cal.currentDateEl, ev); + break; + default: + return false; } - var nMonth = getNextMonth(m,y,+1); - var pMonth = getNextMonth(m,y,-1); - document.getElementById('calNavPY').innerHTML = '<<'; - document.getElementById('calNavPM').innerHTML = '<'; - document.getElementById('calNavMY').innerHTML = moty[m] +' '+y; - document.getElementById('calNavNY').innerHTML = '>>'; - document.getElementById('calNavNM').innerHTML = '>'; -} -function showCal(m,y,f,dateSpan,wknd,format){ - /* - dateSpan - date that should have links; does not include weekends - 0 = no dates - 1 = all past dates up to and including today - 2 = all future dates starting with today - 3 = all future dates NOT including today ( for GTC Dates ) - 4 = all past dates NOT including today ( for start / from dates ) - 5 = all dates - */ - calField = f; calSpan = dateSpan; calFormat = format; calWknd = wknd; - if (m == '' && y == ''){m = curr_mn; y = curr_yr;} - buildCalendar(m,y); - document.getElementById('calDiv').style.display = ''; -} -function placeDate(m,d,y){ - eval(calField).value = dateFormats(m,d,y,calFormat); - document.getElementById('calDiv').style.display = 'none'; -} -function dateFormats(m,d,y,calFormat){ - d = d.toString(); - m = m+1; m = m.toString(); - y = y.toString(); - var sy = y; -// -- convert to 2 digit numbers - if (m.length == 1){m = '0'+ m;} - if (d.length == 1){d = '0'+ d;} - if (y.length == 4) - sy = y.substring(2,4); - var format; - switch (calFormat){ - case 0 : format = m + d + sy; break; // mmddyy - case 1 : format = m + d + y; break; // mmddyyyy - case 2 : format = m +'/'+ d +'/'+ y; break; // mm/dd/yyyy - case 3 : format = m +'/'+ d +'/'+ sy; break; // mm/dd/yy - case 4 : format = y + m; break; // yyyymm - case 5 : format = d + m + sy; break; // ddmmyy - case 6 : format = d +'/'+ m +'/'+ sy; break; // dd/mm/yy - case 7 : format = d + m + y; break; // ddmmyyyy - case 8 : format = d +'/'+ m +'/'+ y; break; // dd/mm/yyyy - case 9 : format = y +'-'+ m +'-'+ d; break; // yyyy-mm-dd - default: format = m + d + y; break; // mmddyyyy + return Calendar.stopEvent(ev); +}; + +/** + * (RE)Initializes the calendar to the given date and firstDayOfWeek + */ +Calendar.prototype._init = function (firstDayOfWeek, date) { + var today = new Date(), + TY = today.getFullYear(), + TM = today.getMonth(), + TD = today.getDate(); + this.table.style.visibility = "hidden"; + var year = date.getFullYear(); + if (year < this.minYear) { + year = this.minYear; + date.setFullYear(year); + } else if (year > this.maxYear) { + year = this.maxYear; + date.setFullYear(year); } - return format; -} \ No newline at end of file + this.firstDayOfWeek = firstDayOfWeek; + this.date = new Date(date); + var month = date.getMonth(); + var mday = date.getDate(); + var no_days = date.getMonthDays(); + + // calendar voodoo for computing the first day that would actually be + // displayed in the calendar, even if it's from the previous month. + // WARNING: this is magic. ;-) + date.setDate(1); + var day1 = (date.getDay() - this.firstDayOfWeek) % 7; + if (day1 < 0) + day1 += 7; + date.setDate(-day1); + date.setDate(date.getDate() + 1); + + var row = this.tbody.firstChild; + var MN = Calendar._SMN[month]; + var ar_days = this.ar_days = new Array(); + var weekend = Calendar._TT["WEEKEND"]; + var dates = this.multiple ? (this.datesCells = {}) : null; + for (var i = 0; i < 6; ++i, row = row.nextSibling) { + var cell = row.firstChild; + if (this.weekNumbers) { + cell.className = "day wn"; + cell.innerHTML = date.getWeekNumber(); + cell = cell.nextSibling; + } + row.className = "daysrow"; + var hasdays = false, iday, dpos = ar_days[i] = []; + for (var j = 0; j < 7; ++j, cell = cell.nextSibling, date.setDate(iday + 1)) { + iday = date.getDate(); + var wday = date.getDay(); + cell.className = "day"; + cell.pos = i << 4 | j; + dpos[j] = cell; + var current_month = (date.getMonth() == month); + if (!current_month) { + if (this.showsOtherMonths) { + cell.className += " othermonth"; + cell.otherMonth = true; + } else { + cell.className = "emptycell"; + cell.innerHTML = " "; + cell.disabled = true; + continue; + } + } else { + cell.otherMonth = false; + hasdays = true; + } + cell.disabled = false; + cell.innerHTML = this.getDateText ? this.getDateText(date, iday) : iday; + if (dates) + dates[date.print("%Y%m%d")] = cell; + if (this.getDateStatus) { + var status = this.getDateStatus(date, year, month, iday); + if (this.getDateToolTip) { + var toolTip = this.getDateToolTip(date, year, month, iday); + if (toolTip) + cell.title = toolTip; + } + if (status === true) { + cell.className += " disabled"; + cell.disabled = true; + } else { + if (/disabled/i.test(status)) + cell.disabled = true; + cell.className += " " + status; + } + } + if (!cell.disabled) { + cell.caldate = new Date(date); + cell.ttip = "_"; + if (!this.multiple && current_month + && iday == mday && this.hiliteToday) { + cell.className += " selected"; + this.currentDateEl = cell; + } + if (date.getFullYear() == TY && + date.getMonth() == TM && + iday == TD) { + cell.className += " today"; + cell.ttip += Calendar._TT["PART_TODAY"]; + } + if (weekend.indexOf(wday.toString()) != -1) + cell.className += cell.otherMonth ? " oweekend" : " weekend"; + } + } + if (!(hasdays || this.showsOtherMonths)) + row.className = "emptyrow"; + } + this.title.innerHTML = Calendar._MN[month] + ", " + year; + this.onSetTime(); + this.table.style.visibility = "visible"; + this._initMultipleDates(); + // PROFILE + // this.tooltips.innerHTML = "Generated in " + ((new Date()) - today) + " ms"; +}; + +Calendar.prototype._initMultipleDates = function() { + if (this.multiple) { + for (var i in this.multiple) { + var cell = this.datesCells[i]; + var d = this.multiple[i]; + if (!d) + continue; + if (cell) + cell.className += " selected"; + } + } +}; + +Calendar.prototype._toggleMultipleDate = function(date) { + if (this.multiple) { + var ds = date.print("%Y%m%d"); + var cell = this.datesCells[ds]; + if (cell) { + var d = this.multiple[ds]; + if (!d) { + Calendar.addClass(cell, "selected"); + this.multiple[ds] = date; + } else { + Calendar.removeClass(cell, "selected"); + delete this.multiple[ds]; + } + } + } +}; + +Calendar.prototype.setDateToolTipHandler = function (unaryFunction) { + this.getDateToolTip = unaryFunction; +}; + +/** + * Calls _init function above for going to a certain date (but only if the + * date is different than the currently selected one). + */ +Calendar.prototype.setDate = function (date) { + if (!date.equalsTo(this.date)) { + this._init(this.firstDayOfWeek, date); + } +}; + +/** + * Refreshes the calendar. Useful if the "disabledHandler" function is + * dynamic, meaning that the list of disabled date can change at runtime. + * Just * call this function if you think that the list of disabled dates + * should * change. + */ +Calendar.prototype.refresh = function () { + this._init(this.firstDayOfWeek, this.date); +}; + +/** Modifies the "firstDayOfWeek" parameter (pass 0 for Synday, 1 for Monday, etc.). */ +Calendar.prototype.setFirstDayOfWeek = function (firstDayOfWeek) { + this._init(firstDayOfWeek, this.date); + this._displayWeekdays(); +}; + +/** + * Allows customization of what dates are enabled. The "unaryFunction" + * parameter must be a function object that receives the date (as a JS Date + * object) and returns a boolean value. If the returned value is true then + * the passed date will be marked as disabled. + */ +Calendar.prototype.setDateStatusHandler = Calendar.prototype.setDisabledHandler = function (unaryFunction) { + this.getDateStatus = unaryFunction; +}; + +/** Customization of allowed year range for the calendar. */ +Calendar.prototype.setRange = function (a, z) { + this.minYear = a; + this.maxYear = z; +}; + +/** Calls the first user handler (selectedHandler). */ +Calendar.prototype.callHandler = function () { + if (this.onSelected) { + this.onSelected(this, this.date.print(this.dateFormat)); + } +}; + +/** Calls the second user handler (closeHandler). */ +Calendar.prototype.callCloseHandler = function () { + if (this.onClose) { + this.onClose(this); + } + this.hideShowCovered(); +}; + +/** Removes the calendar object from the DOM tree and destroys it. */ +Calendar.prototype.destroy = function () { + var el = this.element.parentNode; + el.removeChild(this.element); + Calendar._C = null; + window._dynarch_popupCalendar = null; +}; + +/** + * Moves the calendar element to a different section in the DOM tree (changes + * its parent). + */ +Calendar.prototype.reparent = function (new_parent) { + var el = this.element; + el.parentNode.removeChild(el); + new_parent.appendChild(el); +}; + +// This gets called when the user presses a mouse button anywhere in the +// document, if the calendar is shown. If the click was outside the open +// calendar this function closes it. +Calendar._checkCalendar = function(ev) { + var calendar = window._dynarch_popupCalendar; + if (!calendar) { + return false; + } + var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev); + for (; el != null && el != calendar.element; el = el.parentNode); + if (el == null) { + // calls closeHandler which should hide the calendar. + window._dynarch_popupCalendar.callCloseHandler(); + return Calendar.stopEvent(ev); + } +}; + +/** Shows the calendar. */ +Calendar.prototype.show = function () { + var rows = this.table.getElementsByTagName("tr"); + for (var i = rows.length; i > 0;) { + var row = rows[--i]; + Calendar.removeClass(row, "rowhilite"); + var cells = row.getElementsByTagName("td"); + for (var j = cells.length; j > 0;) { + var cell = cells[--j]; + Calendar.removeClass(cell, "hilite"); + Calendar.removeClass(cell, "active"); + } + } + this.element.style.display = "block"; + this.hidden = false; + if (this.isPopup) { + window._dynarch_popupCalendar = this; + Calendar.addEvent(document, "keydown", Calendar._keyEvent); + Calendar.addEvent(document, "keypress", Calendar._keyEvent); + Calendar.addEvent(document, "mousedown", Calendar._checkCalendar); + } + this.hideShowCovered(); +}; + +/** + * Hides the calendar. Also removes any "hilite" from the class of any TD + * element. + */ +Calendar.prototype.hide = function () { + if (this.isPopup) { + Calendar.removeEvent(document, "keydown", Calendar._keyEvent); + Calendar.removeEvent(document, "keypress", Calendar._keyEvent); + Calendar.removeEvent(document, "mousedown", Calendar._checkCalendar); + } + this.element.style.display = "none"; + this.hidden = true; + this.hideShowCovered(); +}; + +/** + * Shows the calendar at a given absolute position (beware that, depending on + * the calendar element style -- position property -- this might be relative + * to the parent's containing rectangle). + */ +Calendar.prototype.showAt = function (x, y) { + var s = this.element.style; + s.left = x + "px"; + s.top = y + "px"; + this.show(); +}; + +/** Shows the calendar near a given element. */ +Calendar.prototype.showAtElement = function (el, opts) { + var self = this; + var p = Calendar.getAbsolutePos(el); + if (!opts || typeof opts != "string") { + this.showAt(p.x, p.y + el.offsetHeight); + return true; + } + function fixPosition(box) { + if (box.x < 0) + box.x = 0; + if (box.y < 0) + box.y = 0; + var cp = document.createElement("div"); + var s = cp.style; + s.position = "absolute"; + s.right = s.bottom = s.width = s.height = "0px"; + document.body.appendChild(cp); + var br = Calendar.getAbsolutePos(cp); + document.body.removeChild(cp); + if (Calendar.is_ie) { + br.y += document.body.scrollTop; + br.x += document.body.scrollLeft; + } else { + br.y += window.scrollY; + br.x += window.scrollX; + } + var tmp = box.x + box.width - br.x; + if (tmp > 0) box.x -= tmp; + tmp = box.y + box.height - br.y; + if (tmp > 0) box.y -= tmp; + }; + this.element.style.display = "block"; + Calendar.continuation_for_the_fucking_khtml_browser = function() { + var w = self.element.offsetWidth; + var h = self.element.offsetHeight; + self.element.style.display = "none"; + var valign = opts.substr(0, 1); + var halign = "l"; + if (opts.length > 1) { + halign = opts.substr(1, 1); + } + // vertical alignment + switch (valign) { + case "T": p.y -= h; break; + case "B": p.y += el.offsetHeight; break; + case "C": p.y += (el.offsetHeight - h) / 2; break; + case "t": p.y += el.offsetHeight - h; break; + case "b": break; // already there + } + // horizontal alignment + switch (halign) { + case "L": p.x -= w; break; + case "R": p.x += el.offsetWidth; break; + case "C": p.x += (el.offsetWidth - w) / 2; break; + case "l": p.x += el.offsetWidth - w; break; + case "r": break; // already there + } + p.width = w; + p.height = h + 40; + self.monthsCombo.style.display = "none"; + fixPosition(p); + self.showAt(p.x, p.y); + }; + if (Calendar.is_khtml) + setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10); + else + Calendar.continuation_for_the_fucking_khtml_browser(); +}; + +/** Customizes the date format. */ +Calendar.prototype.setDateFormat = function (str) { + this.dateFormat = str; +}; + +/** Customizes the tooltip date format. */ +Calendar.prototype.setTtDateFormat = function (str) { + this.ttDateFormat = str; +}; + +/** + * Tries to identify the date represented in a string. If successful it also + * calls this.setDate which moves the calendar to the given date. + */ +Calendar.prototype.parseDate = function(str, fmt) { + if (!fmt) + fmt = this.dateFormat; + this.setDate(Date.parseDate(str, fmt)); +}; + +Calendar.prototype.hideShowCovered = function () { + if (!Calendar.is_ie && !Calendar.is_opera) + return; + function getVisib(obj){ + var value = obj.style.visibility; + if (!value) { + if (document.defaultView && typeof (document.defaultView.getComputedStyle) == "function") { // Gecko, W3C + if (!Calendar.is_khtml) + value = document.defaultView. + getComputedStyle(obj, "").getPropertyValue("visibility"); + else + value = ''; + } else if (obj.currentStyle) { // IE + value = obj.currentStyle.visibility; + } else + value = ''; + } + return value; + }; + + var tags = new Array("applet", "iframe", "select"); + var el = this.element; + + var p = Calendar.getAbsolutePos(el); + var EX1 = p.x; + var EX2 = el.offsetWidth + EX1; + var EY1 = p.y; + var EY2 = el.offsetHeight + EY1; + + for (var k = tags.length; k > 0; ) { + var ar = document.getElementsByTagName(tags[--k]); + var cc = null; + + for (var i = ar.length; i > 0;) { + cc = ar[--i]; + + p = Calendar.getAbsolutePos(cc); + var CX1 = p.x; + var CX2 = cc.offsetWidth + CX1; + var CY1 = p.y; + var CY2 = cc.offsetHeight + CY1; + + if (this.hidden || (CX1 > EX2) || (CX2 < EX1) || (CY1 > EY2) || (CY2 < EY1)) { + if (!cc.__msh_save_visibility) { + cc.__msh_save_visibility = getVisib(cc); + } + cc.style.visibility = cc.__msh_save_visibility; + } else { + if (!cc.__msh_save_visibility) { + cc.__msh_save_visibility = getVisib(cc); + } + cc.style.visibility = "hidden"; + } + } + } +}; + +/** Internal function; it displays the bar with the names of the weekday. */ +Calendar.prototype._displayWeekdays = function () { + var fdow = this.firstDayOfWeek; + var cell = this.firstdayname; + var weekend = Calendar._TT["WEEKEND"]; + for (var i = 0; i < 7; ++i) { + cell.className = "day name"; + var realday = (i + fdow) % 7; + if (i) { + cell.ttip = Calendar._TT["DAY_FIRST"].replace("%s", Calendar._DN[realday]); + cell.navtype = 100; + cell.calendar = this; + cell.fdow = realday; + Calendar._add_evs(cell); + } + if (weekend.indexOf(realday.toString()) != -1) { + Calendar.addClass(cell, "weekend"); + } + cell.innerHTML = Calendar._SDN[(i + fdow) % 7]; + cell = cell.nextSibling; + } +}; + +/** Internal function. Hides all combo boxes that might be displayed. */ +Calendar.prototype._hideCombos = function () { + this.monthsCombo.style.display = "none"; + this.yearsCombo.style.display = "none"; +}; + +/** Internal function. Starts dragging the element. */ +Calendar.prototype._dragStart = function (ev) { + if (this.dragging) { + return; + } + this.dragging = true; + var posX; + var posY; + if (Calendar.is_ie) { + posY = window.event.clientY + document.body.scrollTop; + posX = window.event.clientX + document.body.scrollLeft; + } else { + posY = ev.clientY + window.scrollY; + posX = ev.clientX + window.scrollX; + } + var st = this.element.style; + this.xOffs = posX - parseInt(st.left); + this.yOffs = posY - parseInt(st.top); + with (Calendar) { + addEvent(document, "mousemove", calDragIt); + addEvent(document, "mouseup", calDragEnd); + } +}; + +// BEGIN: DATE OBJECT PATCHES + +/** Adds the number of days array to the Date object. */ +Date._MD = new Array(31,28,31,30,31,30,31,31,30,31,30,31); + +/** Constants used for time computations */ +Date.SECOND = 1000 /* milliseconds */; +Date.MINUTE = 60 * Date.SECOND; +Date.HOUR = 60 * Date.MINUTE; +Date.DAY = 24 * Date.HOUR; +Date.WEEK = 7 * Date.DAY; + +Date.parseDate = function(str, fmt) { + var today = new Date(); + var y = 0; + var m = -1; + var d = 0; + var a = str.split(/\W+/); + var b = fmt.match(/%./g); + var i = 0, j = 0; + var hr = 0; + var min = 0; + for (i = 0; i < a.length; ++i) { + if (!a[i]) + continue; + switch (b[i]) { + case "%d": + case "%e": + d = parseInt(a[i], 10); + break; + + case "%m": + m = parseInt(a[i], 10) - 1; + break; + + case "%Y": + case "%y": + y = parseInt(a[i], 10); + (y < 100) && (y += (y > 29) ? 1900 : 2000); + break; + + case "%b": + case "%B": + for (j = 0; j < 12; ++j) { + if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { m = j; break; } + } + break; + + case "%H": + case "%I": + case "%k": + case "%l": + hr = parseInt(a[i], 10); + break; + + case "%P": + case "%p": + if (/pm/i.test(a[i]) && hr < 12) + hr += 12; + else if (/am/i.test(a[i]) && hr >= 12) + hr -= 12; + break; + + case "%M": + min = parseInt(a[i], 10); + break; + } + } + if (isNaN(y)) y = today.getFullYear(); + if (isNaN(m)) m = today.getMonth(); + if (isNaN(d)) d = today.getDate(); + if (isNaN(hr)) hr = today.getHours(); + if (isNaN(min)) min = today.getMinutes(); + if (y != 0 && m != -1 && d != 0) + return new Date(y, m, d, hr, min, 0); + y = 0; m = -1; d = 0; + for (i = 0; i < a.length; ++i) { + if (a[i].search(/[a-zA-Z]+/) != -1) { + var t = -1; + for (j = 0; j < 12; ++j) { + if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; } + } + if (t != -1) { + if (m != -1) { + d = m+1; + } + m = t; + } + } else if (parseInt(a[i], 10) <= 12 && m == -1) { + m = a[i]-1; + } else if (parseInt(a[i], 10) > 31 && y == 0) { + y = parseInt(a[i], 10); + (y < 100) && (y += (y > 29) ? 1900 : 2000); + } else if (d == 0) { + d = a[i]; + } + } + if (y == 0) + y = today.getFullYear(); + if (m != -1 && d != 0) + return new Date(y, m, d, hr, min, 0); + return today; +}; + +/** Returns the number of days in the current month */ +Date.prototype.getMonthDays = function(month) { + var year = this.getFullYear(); + if (typeof month == "undefined") { + month = this.getMonth(); + } + if (((0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400)))) && month == 1) { + return 29; + } else { + return Date._MD[month]; + } +}; + +/** Returns the number of day in the year. */ +Date.prototype.getDayOfYear = function() { + var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0); + var then = new Date(this.getFullYear(), 0, 0, 0, 0, 0); + var time = now - then; + return Math.floor(time / Date.DAY); +}; + +/** Returns the number of the week in year, as defined in ISO 8601. */ +Date.prototype.getWeekNumber = function() { + var d = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0); + var DoW = d.getDay(); + d.setDate(d.getDate() - (DoW + 6) % 7 + 3); // Nearest Thu + var ms = d.valueOf(); // GMT + d.setMonth(0); + d.setDate(4); // Thu in Week 1 + return Math.round((ms - d.valueOf()) / (7 * 864e5)) + 1; +}; + +/** Checks date and time equality */ +Date.prototype.equalsTo = function(date) { + return ((this.getFullYear() == date.getFullYear()) && + (this.getMonth() == date.getMonth()) && + (this.getDate() == date.getDate()) && + (this.getHours() == date.getHours()) && + (this.getMinutes() == date.getMinutes())); +}; + +/** Set only the year, month, date parts (keep existing time) */ +Date.prototype.setDateOnly = function(date) { + var tmp = new Date(date); + this.setDate(1); + this.setFullYear(tmp.getFullYear()); + this.setMonth(tmp.getMonth()); + this.setDate(tmp.getDate()); +}; + +/** Prints the date in a string according to the given format. */ +Date.prototype.print = function (str) { + var m = this.getMonth(); + var d = this.getDate(); + var y = this.getFullYear(); + var wn = this.getWeekNumber(); + var w = this.getDay(); + var s = {}; + var hr = this.getHours(); + var pm = (hr >= 12); + var ir = (pm) ? (hr - 12) : hr; + var dy = this.getDayOfYear(); + if (ir == 0) + ir = 12; + var min = this.getMinutes(); + var sec = this.getSeconds(); + s["%a"] = Calendar._SDN[w]; // abbreviated weekday name [FIXME: I18N] + s["%A"] = Calendar._DN[w]; // full weekday name + s["%b"] = Calendar._SMN[m]; // abbreviated month name [FIXME: I18N] + s["%B"] = Calendar._MN[m]; // full month name + // FIXME: %c : preferred date and time representation for the current locale + s["%C"] = 1 + Math.floor(y / 100); // the century number + s["%d"] = (d < 10) ? ("0" + d) : d; // the day of the month (range 01 to 31) + s["%e"] = d; // the day of the month (range 1 to 31) + // FIXME: %D : american date style: %m/%d/%y + // FIXME: %E, %F, %G, %g, %h (man strftime) + s["%H"] = (hr < 10) ? ("0" + hr) : hr; // hour, range 00 to 23 (24h format) + s["%I"] = (ir < 10) ? ("0" + ir) : ir; // hour, range 01 to 12 (12h format) + s["%j"] = (dy < 100) ? ((dy < 10) ? ("00" + dy) : ("0" + dy)) : dy; // day of the year (range 001 to 366) + s["%k"] = hr; // hour, range 0 to 23 (24h format) + s["%l"] = ir; // hour, range 1 to 12 (12h format) + s["%m"] = (m < 9) ? ("0" + (1+m)) : (1+m); // month, range 01 to 12 + s["%M"] = (min < 10) ? ("0" + min) : min; // minute, range 00 to 59 + s["%n"] = "\n"; // a newline character + s["%p"] = pm ? "PM" : "AM"; + s["%P"] = pm ? "pm" : "am"; + // FIXME: %r : the time in am/pm notation %I:%M:%S %p + // FIXME: %R : the time in 24-hour notation %H:%M + s["%s"] = Math.floor(this.getTime() / 1000); + s["%S"] = (sec < 10) ? ("0" + sec) : sec; // seconds, range 00 to 59 + s["%t"] = "\t"; // a tab character + // FIXME: %T : the time in 24-hour notation (%H:%M:%S) + s["%U"] = s["%W"] = s["%V"] = (wn < 10) ? ("0" + wn) : wn; + s["%u"] = w + 1; // the day of the week (range 1 to 7, 1 = MON) + s["%w"] = w; // the day of the week (range 0 to 6, 0 = SUN) + // FIXME: %x : preferred date representation for the current locale without the time + // FIXME: %X : preferred time representation for the current locale without the date + s["%y"] = ('' + y).substr(2, 2); // year without the century (range 00 to 99) + s["%Y"] = y; // year with the century + s["%%"] = "%"; // a literal '%' character + + var re = /%./g; + if (!Calendar.is_ie5 && !Calendar.is_khtml) + return str.replace(re, function (par) { return s[par] || par; }); + + var a = str.match(re); + for (var i = 0; i < a.length; i++) { + var tmp = s[a[i]]; + if (tmp) { + re = new RegExp(a[i], 'g'); + str = str.replace(re, tmp); + } + } + + return str; +}; + +Date.prototype.__msh_oldSetFullYear = Date.prototype.setFullYear; +Date.prototype.setFullYear = function(y) { + var d = new Date(this); + d.__msh_oldSetFullYear(y); + if (d.getMonth() != this.getMonth()) + this.setDate(28); + this.__msh_oldSetFullYear(y); +}; + +// END: DATE OBJECT PATCHES + + +// global object that remembers the calendar +window._dynarch_popupCalendar = null; diff --git a/tracks/public/javascripts/prototype-ex.js b/tracks/public/javascripts/prototype-ex.js new file mode 100644 index 00000000..27c7eb6d --- /dev/null +++ b/tracks/public/javascripts/prototype-ex.js @@ -0,0 +1,55 @@ +/* Form extensions - prototype-ex by Matt McCray + */ + +Form.extend( { + + disable: function( form ) { + var elements = Form.getElements( form ); + for(var i=0; i * - * Prototype is freely distributable under the terms of an MIT-style license. - * For details, see http://prototype.conio.net/ - */ + * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff + * against the source tree, available from the Prototype darcs repository. + * + * Prototype is freely distributable under the terms of an MIT-style license. + * + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ -Prototype = { - Version: '1.0.1' +var Prototype = { + Version: '1.2.1' } -Class = { +var Class = { create: function() { return function() { this.initialize.apply(this, arguments); @@ -17,7 +22,7 @@ Class = { } } -Abstract = new Object(); +var Abstract = new Object(); Object.prototype.extend = function(object) { for (property in object) { @@ -40,7 +45,13 @@ Function.prototype.bindAsEventListener = function(object) { } } -Try = { +Number.prototype.toColorPart = function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; +} + +var Try = { these: function() { var returnValue; @@ -56,13 +67,33 @@ Try = { } } -Toggle = { - display: function() { - for (var i = 0; i < elements.length; i++) { - var element = $(elements[i]); - element.style.display = - (element.style.display == 'none' ? '' : 'none'); +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setTimeout(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } } + + this.registerCallback(); } } @@ -85,27 +116,39 @@ function $() { return elements; } -function getElementsByClassName(className, element) { - var children = (element || document).getElementsByTagName('*'); - var elements = new Array(); - - for (var i = 0; i < children.length; i++) { - var child = children[i]; - var classNames = child.className.split(' '); - for (var j = 0; j < classNames.length; j++) { - if (classNames[j] == className) { - elements.push(child); - break; - } - } +/*--------------------------------------------------------------------------*/ + +if (!Array.prototype.push) { + Array.prototype.push = function() { + var startLength = this.length; + for (var i = 0; i < arguments.length; i++) + this[startLength + i] = arguments[i]; + return this.length; + } +} + +if (!Function.prototype.apply) { + // Based on code from http://www.youngpup.net/ + Function.prototype.apply = function(object, parameters) { + var parameterStrings = new Array(); + if (!object) object = window; + if (!parameters) parameters = new Array(); + + for (var i = 0; i < parameters.length; i++) + parameterStrings[i] = 'x[' + i + ']'; + + object.__apply__ = this; + var result = eval('obj.__apply__(' + + parameterStrings[i].join(', ') + ')'); + object.__apply__ = null; + + return result; } - - return elements; } /*--------------------------------------------------------------------------*/ -Ajax = { +var Ajax = { getTransport: function() { return Try.these( function() {return new ActiveXObject('Msxml2.XMLHTTP')}, @@ -141,13 +184,17 @@ Ajax.Request.prototype = (new Ajax.Base()).extend({ if (this.options.method == 'get') url += '?' + this.options.parameters + '&_='; - this.transport.open(this.options.method, url, true); + this.transport.open(this.options.method, url, + this.options.asynchronous); if (this.options.asynchronous) { this.transport.onreadystatechange = this.onStateChange.bind(this); setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); } + this.transport.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + this.transport.setRequestHeader('X-Prototype-Version', Prototype.Version); + if (this.options.method == 'post') { this.transport.setRequestHeader('Connection', 'close'); this.transport.setRequestHeader('Content-type', @@ -191,14 +238,22 @@ Ajax.Updater.prototype = (new Ajax.Base()).extend({ }, updateContent: function() { - this.container.innerHTML = this.request.transport.responseText; - if (this.onComplete) this.onComplete(this.request); + if (this.options.insertion) { + new this.options.insertion(this.container, + this.request.transport.responseText); + } else { + this.container.innerHTML = this.request.transport.responseText; + } + + if (this.onComplete) { + setTimeout((function() {this.onComplete(this.request)}).bind(this), 10); + } } }); /*--------------------------------------------------------------------------*/ -Field = { +var Field = { clear: function() { for (var i = 0; i < arguments.length; i++) $(arguments[i]).value = ''; @@ -212,12 +267,21 @@ Field = { for (var i = 0; i < arguments.length; i++) if ($(arguments[i]).value == '') return false; return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + $(element).focus(); + $(element).select(); } } /*--------------------------------------------------------------------------*/ -Form = { +var Form = { serialize: function(form) { var elements = Form.getElements($(form)); var queryComponents = new Array(); @@ -241,6 +305,31 @@ Form = { elements.push(tagElements[j]); } return elements; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disable = 'true'; + } + }, + + focusFirstElement: function(form) { + form = $(form); + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + if (element.type != 'hidden' && !element.disabled) { + Field.activate(element); + break; + } + } + }, + + reset: function(form) { + $(form).reset(); } } @@ -269,12 +358,14 @@ Form.Element.Serializers = { input: function(element) { switch (element.type.toLowerCase()) { case 'hidden': + case 'password': case 'text': return Form.Element.Serializers.textarea(element); case 'checkbox': case 'radio': return Form.Element.Serializers.inputSelector(element); } + return false; }, inputSelector: function(element) { @@ -288,12 +379,17 @@ Form.Element.Serializers = { select: function(element) { var index = element.selectedIndex; - return [element.name, (index >= 0) ? element.options[index].value : '']; + var value = element.options[index].value || element.options[index].text; + return [element.name, (index >= 0) ? value : '']; } } /*--------------------------------------------------------------------------*/ +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + Abstract.TimedObserver = function() {} Abstract.TimedObserver.prototype = { initialize: function(element, frequency, callback) { @@ -334,3 +430,336 @@ Form.Observer.prototype = (new Abstract.TimedObserver()).extend({ } }); + +/*--------------------------------------------------------------------------*/ + +document.getElementsByClassName = function(className) { + var children = document.getElementsByTagName('*') || document.all; + var elements = new Array(); + + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var classNames = child.className.split(' '); + for (var j = 0; j < classNames.length; j++) { + if (classNames[j] == className) { + elements.push(child); + break; + } + } + } + + return elements; +} + +/*--------------------------------------------------------------------------*/ + +var Element = { + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = + (element.style.display == 'none' ? '' : 'none'); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + } +} + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content; + + if (this.adjacency && this.element.insertAdjacentHTML) { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.fragment = this.range.createContextualFragment(this.content); + this.insertContent(); + } + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = (new Abstract.Insertion('beforeBegin')).extend({ + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function() { + this.element.parentNode.insertBefore(this.fragment, this.element); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = (new Abstract.Insertion('afterBegin')).extend({ + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function() { + this.element.insertBefore(this.fragment, this.element.firstChild); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = (new Abstract.Insertion('beforeEnd')).extend({ + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function() { + this.element.appendChild(this.fragment); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = (new Abstract.Insertion('afterEnd')).extend({ + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function() { + this.element.parentNode.insertBefore(this.fragment, + this.element.nextSibling); + } +}); + +/*--------------------------------------------------------------------------*/ + +var Effect = new Object(); + +Effect.Highlight = Class.create(); +Effect.Highlight.prototype = { + initialize: function(element) { + this.element = $(element); + this.start = 153; + this.finish = 255; + this.current = this.start; + this.fade(); + }, + + fade: function() { + if (this.isFinished()) return; + if (this.timer) clearTimeout(this.timer); + this.highlight(this.element, this.current); + this.current += 17; + this.timer = setTimeout(this.fade.bind(this), 250); + }, + + isFinished: function() { + return this.current > this.finish; + }, + + highlight: function(element, current) { + element.style.backgroundColor = "#ffff" + current.toColorPart(); + } +} + + +Effect.Fade = Class.create(); +Effect.Fade.prototype = { + initialize: function(element) { + this.element = $(element); + this.start = 100; + this.finish = 0; + this.current = this.start; + this.fade(); + }, + + fade: function() { + if (this.isFinished()) { this.element.style.display = 'none'; return; } + if (this.timer) clearTimeout(this.timer); + this.setOpacity(this.element, this.current); + this.current -= 10; + this.timer = setTimeout(this.fade.bind(this), 50); + }, + + isFinished: function() { + return this.current <= this.finish; + }, + + setOpacity: function(element, opacity) { + opacity = (opacity == 100) ? 99.999 : opacity; + element.style.filter = "alpha(opacity:"+opacity+")"; + element.style.opacity = opacity/100 /*//*/; + } +} + +Effect.Scale = Class.create(); +Effect.Scale.prototype = { + initialize: function(element, percent) { + this.element = $(element); + this.startScale = 1.0; + this.startHeight = this.element.offsetHeight; + this.startWidth = this.element.offsetWidth; + this.currentHeight = this.startHeight; + this.currentWidth = this.startWidth; + this.finishScale = (percent/100) /*//*/; + if (this.element.style.fontSize=="") this.sizeEm = 1.0; + if (this.element.style.fontSize.indexOf("em")>0) + this.sizeEm = parseFloat(this.element.style.fontSize); + if(this.element.effect_scale) { + clearTimeout(this.element.effect_scale.timer); + this.startScale = this.element.effect_scale.currentScale; + this.startHeight = this.element.effect_scale.startHeight; + this.startWidth = this.element.effect_scale.startWidth; + if(this.element.effect_scale.sizeEm) + this.sizeEm = this.element.effect_scale.sizeEm; + } + this.element.effect_scale = this; + this.currentScale = this.startScale; + this.factor = this.finishScale - this.startScale; + this.options = arguments[2] || {}; + this.scale(); + }, + + scale: function() { + if (this.isFinished()) { + this.setDimensions(this.element, this.startWidth*this.finishScale, this.startHeight*this.finishScale); + if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.finishScale + "em"; + if(this.options.complete) this.options.complete(this); + return; + } + if (this.timer) clearTimeout(this.timer); + if (this.options.step) this.options.step(this); + this.setDimensions(this.element, this.currentWidth, this.currentHeight); + if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.currentScale + "em"; + this.currentScale += (this.factor/10) /*//*/; + this.currentWidth = this.startWidth * this.currentScale; + this.currentHeight = this.startHeight * this.currentScale; + this.timer = setTimeout(this.scale.bind(this), 50); + }, + + isFinished: function() { + return (this.factor < 0) ? + this.currentScale <= this.finishScale : this.currentScale >= this.finishScale; + }, + + setDimensions: function(element, width, height) { + element.style.width = width + 'px'; + element.style.height = height + 'px'; + } +} + +Effect.Squish = Class.create(); +Effect.Squish.prototype = { + initialize: function(element) { + this.element = $(element); + new Effect.Scale(this.element, 1, { complete: this.hide.bind(this) } ); + }, + hide: function() { + this.element.style.display = 'none'; + } +} + +Effect.Puff = Class.create(); +Effect.Puff.prototype = { + initialize: function(element) { + this.element = $(element); + this.opacity = 100; + this.startTop = this.element.top || this.element.offsetTop; + this.startLeft = this.element.left || this.element.offsetLeft; + new Effect.Scale(this.element, 200, { step: this.fade.bind(this), complete: this.hide.bind(this) } ); + }, + fade: function(effect) { + topd = (((effect.currentScale)*effect.startHeight) - effect.startHeight)/2; + leftd = (((effect.currentScale)*effect.startWidth) - effect.startWidth)/2; + this.element.style.position='absolute'; + this.element.style.top = this.startTop-topd + "px"; + this.element.style.left = this.startLeft-leftd + "px"; + this.opacity -= 10; + this.setOpacity(this.element, this.opacity); + if(navigator.appVersion.indexOf('AppleWebKit')>0) this.element.innerHTML += ''; //force redraw on safari + }, + hide: function() { + this.element.style.display = 'none'; + }, + setOpacity: function(element, opacity) { + opacity = (opacity == 100) ? 99.999 : opacity; + element.style.filter = "alpha(opacity:"+opacity+")"; + element.style.opacity = opacity/100 /*//*/; + } +} + +Effect.Appear = Class.create(); +Effect.Appear.prototype = { + initialize: function(element) { + this.element = $(element); + this.start = 0; + this.finish = 100; + this.current = this.start; + this.fade(); + }, + + fade: function() { + if (this.isFinished()) return; + if (this.timer) clearTimeout(this.timer); + this.setOpacity(this.element, this.current); + this.current += 10; + this.timer = setTimeout(this.fade.bind(this), 50); + }, + + isFinished: function() { + return this.current > this.finish; + }, + + setOpacity: function(element, opacity) { + opacity = (opacity == 100) ? 99.999 : opacity; + element.style.filter = "alpha(opacity:"+opacity+")"; + element.style.opacity = opacity/100 /*//*/; + element.style.display = ''; + } +} + +Effect.ContentZoom = Class.create(); +Effect.ContentZoom.prototype = { + initialize: function(element, percent) { + this.element = $(element); + if (this.element.style.fontSize=="") this.sizeEm = 1.0; + if (this.element.style.fontSize.indexOf("em")>0) + this.sizeEm = parseFloat(this.element.style.fontSize); + if(this.element.effect_contentzoom) { + this.sizeEm = this.element.effect_contentzoom.sizeEm; + } + this.element.effect_contentzoom = this; + this.element.style.fontSize = this.sizeEm*(percent/100) + "em" /*//*/; + if(navigator.appVersion.indexOf('AppleWebKit')>0) { this.element.scrollTop -= 1; }; + } +} \ No newline at end of file diff --git a/tracks/public/javascripts/toggle_notes.js b/tracks/public/javascripts/toggle_notes.js index e4077578..333a72d6 100644 --- a/tracks/public/javascripts/toggle_notes.js +++ b/tracks/public/javascripts/toggle_notes.js @@ -5,11 +5,12 @@ function toggleAll(itemname,state) { } } -function toggle(idname) { - document.getElementById(idname).style.display = (document.getElementById(idname).style.display == 'none') ? 'block' : 'none'; +// Contributed by Andrew Williams +function toggleSingle(idname) +{ +document.getElementById(idname).style.display = (document.getElementById(idname).style.display == 'none') ? 'block' : 'none'; } -// Contributed by Andrew Williams function toggleAllImages() { var cookies = document.cookie.split(';'); @@ -23,18 +24,13 @@ function toggleAllImages() var id = str.split('_')[2]; if(getCookie(str) == 'collapsed') { - toggle('c'+id); + toggleSingle('c'+id); toggleImage('toggle_context_'+id); } } } } -function toggle(idname) -{ -document.getElementById(idname).style.display = (document.getElementById(idname).style.display == 'none') ? 'block' : 'none'; -} - function toggleImage(idname) { if(document.images) @@ -125,3 +121,4 @@ function markItemDone(rowId, uri, id) { } req.send(encodeURIComponent("id") + '=' + encodeURIComponent(id)); }; + diff --git a/tracks/public/stylesheets/calendar-system.css b/tracks/public/stylesheets/calendar-system.css new file mode 100644 index 00000000..ca33cde0 --- /dev/null +++ b/tracks/public/stylesheets/calendar-system.css @@ -0,0 +1,232 @@ +/* The main calendar widget. DIV containing a table. */ + +div.calendar { position: relative; } + +.calendar, .calendar table { + border: 1px solid #556; + font-size: 11px; + color: #000; + cursor: default; + background: #eef; + font-family: tahoma,verdana,sans-serif; +} + +/* Header part -- contains navigation buttons and day names. */ + +.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */ + text-align: center; /* They are the navigation buttons */ + padding: 2px; /* Make the buttons seem like they're pressing */ +} + +.calendar .nav { + background: #778 url(menuarrow.gif) no-repeat 100% 100%; +} + +.calendar thead .title { /* This holds the current "month, year" */ + font-weight: bold; /* Pressing it will take you to the current date */ + text-align: center; + background: #fff; + color: #000; + padding: 2px; +} + +.calendar thead .headrow { /* Row containing navigation buttons */ + background: #778; + color: #fff; +} + +.calendar thead .daynames { /* Row containing the day names */ + background: #bdf; +} + +.calendar thead .name { /* Cells containing the day names */ + border-bottom: 1px solid #556; + padding: 2px; + text-align: center; + color: #000; +} + +.calendar thead .weekend { /* How a weekend day name shows in header */ + color: #a66; +} + +.calendar thead .hilite { /* How do the buttons in header appear when hover */ + background-color: #aaf; + color: #000; + border: 1px solid #04f; + padding: 1px; +} + +.calendar thead .active { /* Active (pressed) buttons in header */ + background-color: #77c; + padding: 2px 0px 0px 2px; +} + +/* The body part -- contains all the days in month. */ + +.calendar tbody .day { /* Cells containing month days dates */ + width: 2em; + color: #456; + text-align: right; + padding: 2px 4px 2px 2px; +} +.calendar tbody .day.othermonth { + font-size: 80%; + color: #bbb; +} +.calendar tbody .day.othermonth.oweekend { + color: #fbb; +} + +.calendar table .wn { + padding: 2px 3px 2px 2px; + border-right: 1px solid #000; + background: #bdf; +} + +.calendar tbody .rowhilite td { + background: #def; +} + +.calendar tbody .rowhilite td.wn { + background: #eef; +} + +.calendar tbody td.hilite { /* Hovered cells */ + background: #def; + padding: 1px 3px 1px 1px; + border: 1px solid #bbb; +} + +.calendar tbody td.active { /* Active (pressed) cells */ + background: #cde; + padding: 2px 2px 0px 2px; +} + +.calendar tbody td.selected { /* Cell showing today date */ + font-weight: bold; + border: 1px solid #000; + padding: 1px 3px 1px 1px; + background: #fff; + color: #000; +} + +.calendar tbody td.weekend { /* Cells showing weekend days */ + color: #a66; +} + +.calendar tbody td.today { /* Cell showing selected date */ + font-weight: bold; + color: #00f; +} + +.calendar tbody .disabled { color: #999; } + +.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */ + visibility: hidden; +} + +.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */ + display: none; +} + +/* The footer part -- status bar and "Close" button */ + +.calendar tfoot .footrow { /* The in footer (only one right now) */ + text-align: center; + background: #556; + color: #fff; +} + +.calendar tfoot .ttip { /* Tooltip (status bar) cell */ + background: #fff; + color: #445; + border-top: 1px solid #556; + padding: 1px; +} + +.calendar tfoot .hilite { /* Hover style for buttons in footer */ + background: #aaf; + border: 1px solid #04f; + color: #000; + padding: 1px; +} + +.calendar tfoot .active { /* Active (pressed) style for buttons in footer */ + background: #77c; + padding: 2px 0px 0px 2px; +} + +/* Combo boxes (menus that display months/years for direct selection) */ + +.calendar .combo { + position: absolute; + display: none; + top: 0px; + left: 0px; + width: 4em; + cursor: default; + border: 1px solid #655; + background: #def; + color: #000; + font-size: 90%; + z-index: 100; +} + +.calendar .combo .label, +.calendar .combo .label-IEfix { + text-align: center; + padding: 1px; +} + +.calendar .combo .label-IEfix { + width: 4em; +} + +.calendar .combo .hilite { + background: #acf; +} + +.calendar .combo .active { + border-top: 1px solid #46a; + border-bottom: 1px solid #46a; + background: #eef; + font-weight: bold; +} + +.calendar td.time { + border-top: 1px solid #000; + padding: 1px 0px; + text-align: center; + background-color: #f4f0e8; +} + +.calendar td.time .hour, +.calendar td.time .minute, +.calendar td.time .ampm { + padding: 0px 3px 0px 4px; + border: 1px solid #889; + font-weight: bold; + background-color: #fff; +} + +.calendar td.time .ampm { + text-align: center; +} + +.calendar td.time .colon { + padding: 0px 2px 0px 3px; + font-weight: bold; +} + +.calendar td.time span.hilite { + border-color: #000; + background-color: #667; + color: #fff; +} + +.calendar td.time span.active { + border-color: #f00; + background-color: #000; + color: #0f0; +} diff --git a/tracks/public/stylesheets/standard.css b/tracks/public/stylesheets/standard.css index f8126042..301c3db0 100644 --- a/tracks/public/stylesheets/standard.css +++ b/tracks/public/stylesheets/standard.css @@ -12,7 +12,8 @@ body { p { padding: 2px; } - + +img {border: 0px;} a, a:link, a:active, a:visited { color: #cc3334; @@ -25,7 +26,7 @@ a:hover { color: #fff; background-color: #cc3334; } - + /* Structural divs */ #display_box { @@ -33,6 +34,12 @@ a:hover { width: 450px; margin: 0px 15px 50px 15px; } + +#full_width_display { + float: left; + width: 800px; + margin: 0px 15px 50px 15px; + } #display_box_projects { float: left; @@ -94,7 +101,20 @@ a:hover { color: #fff; text-shadow: rgba(0,0,0,.4) 0px 2px 5px; } + +.new_actions { + padding: 0px 5px 2px 5px; + background: #E7FDDE; + border: 1px solid #57A620; + padding: 5px; + margin-bottom: 15px; + } +.new_actions h2 { + padding: 0 px 5px 0px 5px; + color: #57A620; + } + h2 a, h2 a:link, h2 a:active, h2 a:visited { color: #fff; text-decoration: none; @@ -125,13 +145,21 @@ h2 a:hover { } +#input_box ul {list-style-type: circle; font-size: 0.9em;} + .box { float: left; width: 20px; } + +.big-box { + float: left; + width: 55px; + vertical-align: middle; + } .description { - margin-left: 25px; + margin-left: 60px; margin-right: 10px; } @@ -245,13 +273,45 @@ li { .even_row { background: #fff; + padding: 5px 5px 5px 10px; } .odd_row { background: #EDF3FE; + padding: 5px 5px 5px 10px; } +.edit-form { + background: #ccc; + padding: 5px; + border-top: 1px solid #999; + border-bottom: 1px solid #999; + } + +/* Right align labels in forms */ + +.label { + text-align: right; + } + +input { + vertical-align: middle; +} + +/* Positioning the 'cells' in the list */ +.position {text-align: left; width: 5%; float: left;} +.data {text-align: left; width: 65%; float: left;} +.buttons {text-align: right; width: 25%; margin-left: 75%;} + table.list { + margin-top: 0px; + border-top: 1px solid #ccc; + border-left: 1px solid #ccc; + border-right: 1px solid #ccc; + background: #fff; + } + +div#list-projects, div#list-contexts { border: 1px solid #999; } @@ -267,12 +327,19 @@ form { margin: 0px; width: 313px; } - + +.inline-form { + border: none; + padding: 3px; + width: 100%; + } + label { font-weight: bold; + padding: 0px 0px; } -input { +input, select { margin-bottom: 5px; } @@ -287,60 +354,4 @@ input { border-color: #FC9 #630 #330 #F96; padding:0px 3px 0px 3px; margin:0px; - } - -/* Popup calendar styles */ - -#calNavPY, #calNavPM {border:1px #999 solid} -#calNavNM, #calNavNY { border:1px #999 solid} - -#calNav td, #calNav td a { - text-align:center; - font-family:"Lucida Grande", Verdana, Geneva, Arial, sans-serif; - font-size:12px; - font-weight:bold; - background-color:#eaeaea; - text-decoration:none - } - -#calNav td a,#calNav td a:visited,#calTbl td a,#calTbl td a:visited {color:#cc3334 } -#calNav td a:hover,#calTbl td a:hover { color:#fff; background:#cc3334; } -#calNav {margin-bottom:5px;width:100%} -#calTbl {width:100%} -#calTbl td {text-align:center;font-family:"Lucida Grande", Verdana, Geneva, Arial, sans-serif;font-size:11px;border:1px #ccc solid} - -#calTbl thead td { - font-weight:bold; - background-color:#eaeaea; - padding: 2px; - border:1px #ccc solid - height: auto; - } - -#calDiv { - border:1px solid #ccc; - padding:2px; - width:200px; - position:absolute; - z-index:276; - top:300px; - left:300px; - background-color:#fff - } - -#calTopBar { - background-color:#ccc; - color:#fff; - padding:2px; - text-align:right; - font-family:"Lucida Grande", Verdana, Geneva, Arial, sans-serif; - font-size:11px; - font-weight:bold; - width:100% - } - -#calClose { - padding:0px 2px;border:1px #fff solid;cursor:pointer;cursor:hand - } - -#calPic {cursor:hand} \ No newline at end of file + } \ No newline at end of file diff --git a/tracks/script/benchmarker b/tracks/script/benchmarker new file mode 100755 index 00000000..b07ddcfc --- /dev/null +++ b/tracks/script/benchmarker @@ -0,0 +1,19 @@ +#!/usr/local/bin/ruby + +if ARGV.empty? + puts "Usage: benchmarker times 'Person.expensive_way' 'Person.another_expensive_way' ..." + exit +end + +require File.dirname(__FILE__) + '/../config/environment' +require 'benchmark' +include Benchmark + +# Don't include compilation in the benchmark +ARGV[1..-1].each { |expression| eval(expression) } + +bm(6) do |x| + ARGV[1..-1].each_with_index do |expression, idx| + x.report("##{idx + 1}") { ARGV[0].to_i.times { eval(expression) } } + end +end \ No newline at end of file diff --git a/tracks/script/console_sandbox b/tracks/script/console_sandbox new file mode 100755 index 00000000..9f754bd7 --- /dev/null +++ b/tracks/script/console_sandbox @@ -0,0 +1,7 @@ +#!/usr/local/bin/ruby +ActiveRecord::Base.lock_mutex +ActiveRecord::Base.connection.begin_db_transaction +at_exit do + ActiveRecord::Base.connection.rollback_db_transaction + ActiveRecord::Base.unlock_mutex +end diff --git a/tracks/script/profiler b/tracks/script/profiler new file mode 100755 index 00000000..f0f14a2b --- /dev/null +++ b/tracks/script/profiler @@ -0,0 +1,17 @@ +#!/usr/local/bin/ruby + +if ARGV.empty? + puts "Usage: profiler 'Person.expensive_method(10)' [times]" + exit +end + +require File.dirname(__FILE__) + '/../config/environment' +require "profiler" + +# Don't include compilation in the profile +eval(ARGV.first) + +Profiler__::start_profile +(ARGV[1] || 1).to_i.times { eval(ARGV.first) } +Profiler__::stop_profile +Profiler__::print_profile($stdout) \ No newline at end of file