From c58f41775c34eb671378f674049cbd984a35350b Mon Sep 17 00:00:00 2001 From: bsag Date: Wed, 4 Jan 2006 19:49:15 +0000 Subject: [PATCH] Quite a few improvements to Ajax handling here: * Installed the RJS plugin http://www.codyfauser.com/articles/2005/12/05/rjs-templates-plugin-subversion-repository * Used the RJS templates to update multiple page elements on addition and deletion of actions: the new action gets added, the count 'badge' is updated correctly, and a status area provides helpful information. * If your data entry triggers validation errors e.g. no description for the next action), the errors are displayed in the status area (not very prettily as yet...) * The message about the context/project having no uncompleted actions automagically appears/disappears without refreshing the page. The editing and toggling of actions hasn't been updated yet. git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@171 a4c988fc-2ded-0310-b66e-134b36920a42 --- tracks/app/controllers/application.rb | 23 +- tracks/app/controllers/context_controller.rb | 77 + tracks/app/controllers/project_controller.rb | 79 +- tracks/app/controllers/todo_controller.rb | 69 +- tracks/app/helpers/todo_helper.rb | 13 +- tracks/app/views/context/_context.rhtml | 7 +- tracks/app/views/context/add_item.rjs | 14 + tracks/app/views/context/destroy_action.rjs | 11 + tracks/app/views/context/error.rjs | 1 + tracks/app/views/context/show.rhtml | 7 +- tracks/app/views/layouts/standard.rhtml | 2 +- tracks/app/views/project/_project.rhtml | 5 +- tracks/app/views/project/add_item.rjs | 14 + tracks/app/views/project/destroy_action.rjs | 11 + tracks/app/views/project/error.rjs | 1 + tracks/app/views/project/show.rhtml | 2 +- .../app/views/shared/_add_new_item_form.rhtml | 57 + .../app/views/shared/add_new_item_form.rhtml | 61 - tracks/app/views/todo/_item.rhtml | 2 +- tracks/app/views/todo/add_item.rjs | 13 + tracks/app/views/todo/destroy_action.rjs | 8 + tracks/app/views/todo/error.rjs | 1 + tracks/app/views/todo/list.rhtml | 9 +- tracks/config/routes.rb | 2 +- tracks/doc/CHANGELOG | 3 + tracks/public/stylesheets/standard.css | 47 +- .../javascript_generator_templates/CHANGELOG | 15 + .../MIT-LICENSE | 20 + .../javascript_generator_templates/README | 15 + .../javascript_generator_templates/Rakefile | 27 + .../javascript_generator_templates/init.rb | 3 + .../javascripts/prototype.js | 1785 +++++++++++++++++ .../lib/add_rjs_to_action_controller.rb | 67 + .../lib/add_rjs_to_action_view.rb | 142 ++ .../lib/add_rjs_to_javascript_helper.rb | 204 ++ .../test/abstract_unit.rb | 20 + .../test/controller/new_render_test.rb | 477 +++++ .../test/fixtures/fun/games/hello_world.rhtml | 1 + .../test/fixtures/helpers/abc_helper.rb | 5 + .../test/fixtures/helpers/fun/games_helper.rb | 3 + .../test/fixtures/helpers/fun/pdf_helper.rb | 3 + .../test/fixtures/layouts/builder.rxml | 3 + .../test/fixtures/layouts/standard.rhtml | 1 + .../fixtures/layouts/talk_from_action.rhtml | 2 + .../test/fixtures/layouts/yield.rhtml | 2 + .../test/fixtures/test/_customer.rhtml | 1 + .../fixtures/test/_customer_greeting.rhtml | 1 + .../test/fixtures/test/_hash_object.rhtml | 1 + .../test/fixtures/test/_partial_only.rhtml | 1 + .../test/fixtures/test/_person.rhtml | 2 + .../fixtures/test/action_talk_to_layout.rhtml | 2 + .../test/fixtures/test/capturing.rhtml | 4 + .../test/fixtures/test/content_for.rhtml | 2 + .../test/fixtures/test/delete_with_js.rjs | 2 + .../test/fixtures/test/greeting.rhtml | 1 + .../test/fixtures/test/hello.rxml | 4 + .../test/fixtures/test/hello_world.rhtml | 1 + .../test/fixtures/test/hello_xml_world.rxml | 11 + .../test/fixtures/test/list.rhtml | 1 + .../fixtures/test/potential_conflicts.rhtml | 4 + .../fixtures/test/render_file_with_ivar.rhtml | 1 + .../test/render_file_with_locals.rhtml | 1 + .../fixtures/test/render_to_string_test.rhtml | 1 + .../test/update_element_with_capture.rhtml | 9 + .../test/template/javascript_helper_test.rb | 279 +++ 65 files changed, 3539 insertions(+), 124 deletions(-) create mode 100644 tracks/app/views/context/add_item.rjs create mode 100644 tracks/app/views/context/destroy_action.rjs create mode 100644 tracks/app/views/context/error.rjs create mode 100644 tracks/app/views/project/add_item.rjs create mode 100644 tracks/app/views/project/destroy_action.rjs create mode 100644 tracks/app/views/project/error.rjs create mode 100644 tracks/app/views/shared/_add_new_item_form.rhtml delete mode 100644 tracks/app/views/shared/add_new_item_form.rhtml create mode 100644 tracks/app/views/todo/add_item.rjs create mode 100644 tracks/app/views/todo/destroy_action.rjs create mode 100644 tracks/app/views/todo/error.rjs create mode 100644 tracks/vendor/plugins/javascript_generator_templates/CHANGELOG create mode 100644 tracks/vendor/plugins/javascript_generator_templates/MIT-LICENSE create mode 100644 tracks/vendor/plugins/javascript_generator_templates/README create mode 100644 tracks/vendor/plugins/javascript_generator_templates/Rakefile create mode 100644 tracks/vendor/plugins/javascript_generator_templates/init.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/javascripts/prototype.js create mode 100644 tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_controller.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_view.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_javascript_helper.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/abstract_unit.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/controller/new_render_test.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/fun/games/hello_world.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/helpers/abc_helper.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/helpers/fun/games_helper.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/helpers/fun/pdf_helper.rb create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/layouts/builder.rxml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/layouts/standard.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/layouts/talk_from_action.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/layouts/yield.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/_customer.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/_customer_greeting.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/_hash_object.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/_partial_only.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/_person.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/action_talk_to_layout.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/capturing.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/content_for.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/delete_with_js.rjs create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/greeting.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/hello.rxml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/hello_world.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/hello_xml_world.rxml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/list.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/potential_conflicts.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/render_file_with_ivar.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/render_file_with_locals.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/render_to_string_test.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/fixtures/test/update_element_with_capture.rhtml create mode 100644 tracks/vendor/plugins/javascript_generator_templates/test/template/javascript_helper_test.rb diff --git a/tracks/app/controllers/application.rb b/tracks/app/controllers/application.rb index 068e8a88..b5938baa 100644 --- a/tracks/app/controllers/application.rb +++ b/tracks/app/controllers/application.rb @@ -30,12 +30,6 @@ class ApplicationController < ActionController::Base 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 - # Reverses the urlize() method by substituting underscores for spaces # def deurlize(name) @@ -59,4 +53,21 @@ class ApplicationController < ActionController::Base end end + # Renders the given hash as xml. Primarily used to send multiple + # partials back to an ajax request + # + # * +renders+ is a Hash where the keys are string identifiers, + # and the values are partials rendered as a strings (see + # render_to_string). + def renders_to_xml(renders) + xml = '' + renders.each_key do |key| + xml += "<" + key.to_s + + ">" + end + xml += '' + render(:text => xml) + end + end diff --git a/tracks/app/controllers/context_controller.rb b/tracks/app/controllers/context_controller.rb index 30ba3e82..a4a56d66 100644 --- a/tracks/app/controllers/context_controller.rb +++ b/tracks/app/controllers/context_controller.rb @@ -42,6 +42,71 @@ class ContextController < ApplicationController render :text => "#{flash["warning"]}" end end + + # Called by a form button + # Parameters from form fields are passed to create new action + # in the selected context. + def add_item + self.init + @item = @user.todos.build + @item.attributes = @params["todo"] + + if @item.due? + @item.due = Date.strptime(@params["todo"]["due"], DATE_FORMAT) + else + @item.due = "" + end + + @saved = @item.save + @on_page = "context" + @up_count = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 0 and todos.context_id IN (?)", @user.id, @item.context_id]).size.to_s + + return if request.xhr? + + # fallback for standard requests + if @saved + flash["warning"] = 'Added new next action' + redirect_to :action => 'show', :id => @item + else + #render :action => 'new' + end + + rescue + if request.xhr? # be sure to include an error.rjs + render :action => 'error' + else + flash["warning"] = 'An error occurred on the server.' + #render :action => 'new' + end + end + + # Delete a next action + # + def destroy_action + self.init + @item = check_user_return_item + + @saved = @item.destroy + @down_count = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 0 and todos.context_id IN (?)", @user.id, @item.context_id]).size.to_s + + return if request.xhr? + + # fallback for standard requests + if @saved + flash["warning"] = 'Successfully deleted next action' + redirect_to :controller => 'todo', :action => 'list' + else + render :controller => 'todo', :action => 'list' + end + + rescue + if request.xhr? # be sure to include an error.rjs + render :action => 'error' + else + flash["warning"] = 'An error occurred on the server.' + render :controller => 'todo', :action => 'list' + end + end # Edit the details of the context # @@ -112,11 +177,23 @@ class ContextController < ApplicationController render_text "" end end + + def check_user_return_item + item = Todo.find( @params['id'] ) + if @session['user'] == item.user + return item + else + flash["warning"] = "Item and session user mis-match: #{item.user.name} and #{@session['user'].name}!" + render_text "" + end + end def init @user = @session['user'] @projects = @user.projects.collect { |x| x.done? ? nil:x }.compact @contexts = @user.contexts + @todos = @user.todos + @done = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 1", @user.id], :include => [:project], :order => "completed DESC") end def init_todos diff --git a/tracks/app/controllers/project_controller.rb b/tracks/app/controllers/project_controller.rb index 60e9c4d9..1991c9ad 100644 --- a/tracks/app/controllers/project_controller.rb +++ b/tracks/app/controllers/project_controller.rb @@ -62,6 +62,71 @@ class ProjectController < ApplicationController end end + # Called by a form button + # Parameters from form fields are passed to create new action + # in the selected context. + def add_item + self.init + @item = @user.todos.build + @item.attributes = @params["todo"] + + if @item.due? + @item.due = Date.strptime(@params["todo"]["due"], DATE_FORMAT) + else + @item.due = "" + end + + @saved = @item.save + @on_page = "project" + @up_count = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 0 and todos.project_id IN (?)", @user.id, @item.project_id]).size.to_s + + return if request.xhr? + + # fallback for standard requests + if @saved + flash["warning"] = 'Added new next action' + redirect_to :action => 'show', :name => urlize(@item.project.name) + else + #render :action => 'new' + end + + rescue + if request.xhr? # be sure to include an error.rjs + render :action => 'error' + else + flash["warning"] = 'An error occurred on the server.' + #render :action => 'new' + end + end + + # Delete a next action + # + def destroy_action + self.init + @item = check_user_return_item + + @saved = @item.destroy + @down_count = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 0 and todos.project_id IN (?)", @user.id, @item.project_id]).size.to_s + + return if request.xhr? + + # fallback for standard requests + if @saved + flash["warning"] = 'Successfully deleted next action' + redirect_to :controller => 'todo', :action => 'list' + else + render :controller => 'todo', :action => 'list' + end + + rescue + if request.xhr? # be sure to include an error.rjs + render :action => 'error' + else + flash["warning"] = 'An error occurred on the server.' + render :controller => 'todo', :action => 'list' + end + end + # Edit the details of the project # def update @@ -142,10 +207,22 @@ class ProjectController < ApplicationController end end + def check_user_return_item + item = Todo.find( @params['id'] ) + if @session['user'] == item.user + return item + else + flash["warning"] = "Item and session user mis-match: #{item.user.name} and #{@session['user'].name}!" + render_text "" + end + end + def init @user = @session['user'] - @projects = @user.projects + @projects = @user.projects.collect { |x| x.done? ? nil:x }.compact @contexts = @user.contexts + @todos = @user.todos + @done = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 1", @user.id], :include => [:project], :order => "completed DESC") end def init_todos diff --git a/tracks/app/controllers/todo_controller.rb b/tracks/app/controllers/todo_controller.rb index fd87860e..15828955 100644 --- a/tracks/app/controllers/todo_controller.rb +++ b/tracks/app/controllers/todo_controller.rb @@ -32,29 +32,44 @@ class TodoController < ApplicationController @count = @todos.collect { |x| ( !x.done? and !x.context.hidden? ) ? x:nil }.compact.size end + def update_element + end + # Called by a form button # Parameters from form fields are passed to create new action # in the selected context. def add_item self.init - if @params["on_project_page"] - @on_page = "project" - end - item = @user.todos.build - item.attributes = @params["new_item"] + @item = @user.todos.build + @item.attributes = @params["todo"] - if item.due? - item.due = Date.strptime(@params["new_item"]["due"], DATE_FORMAT) + if @item.due? + @item.due = Date.strptime(@params["todo"]["due"], DATE_FORMAT) else - item.due = "" + @item.due = "" end - - if item.save - render :partial => 'item', :object => item + + @saved = @item.save + @on_page = "home" + @up_count = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 0", @user.id]).size.to_s + + return if request.xhr? + + # fallback for standard requests + if @saved + flash["warning"] = 'Added new next action' + redirect_to :action => 'list' else - flash["warning"] = "Couldn't add next action \"#{item.description}\"" - render_text "" + render :action => 'list' end + + rescue + if request.xhr? # be sure to include an error.rjs + render :action => 'error' + else + flash["warning"] = 'An error occurred on the server.' + render :action => 'list' + end end def edit_action @@ -101,16 +116,32 @@ class TodoController < ApplicationController end end - # Delete a next action in a context + # Delete a next action # def destroy_action - item = check_user_return_item - if item.destroy - render_text "" + self.init + @item = check_user_return_item + + @saved = @item.destroy + @down_count = Todo.find(:all, :conditions => ["todos.user_id = ? and todos.done = 0", @user.id]).size.to_s + + return if request.xhr? + + # fallback for standard requests + if @saved + flash["warning"] = 'Successfully deleted next action' + redirect_to :action => 'list' else - flash["warning"] = "Couldn't delete next action \"#{item.description}\"" - render_text "" + render :action => 'list' end + + rescue + if request.xhr? # be sure to include an error.rjs + render :action => 'error' + else + flash["warning"] = 'An error occurred on the server.' + render :action => 'list' + end end # List the completed tasks, sorted by completion date diff --git a/tracks/app/helpers/todo_helper.rb b/tracks/app/helpers/todo_helper.rb index dd22468a..3ead05e9 100644 --- a/tracks/app/helpers/todo_helper.rb +++ b/tracks/app/helpers/todo_helper.rb @@ -41,17 +41,10 @@ module TodoHelper ) end - def link_to_remote_todo( item ) + def link_to_remote_todo( item, handled_by) str = link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"), - { - :update => "item-#{item.id}-container", - :loading => visual_effect(:fade, "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}\'?" - }, - { - :class => "icon" - }) + "\n" + {:url => { :controller => handled_by, :action => "destroy_action", :id => item.id }}, + {:class => "icon"}) + "\n" if !item.done? str << link_to_remote( image_tag("blank", :title =>"Edit action", :class=>"edit_item", :id=>"action-#{item.id}-edit-icon"), { diff --git a/tracks/app/views/context/_context.rhtml b/tracks/app/views/context/_context.rhtml index a5106898..3ddad253 100644 --- a/tracks/app/views/context/_context.rhtml +++ b/tracks/app/views/context/_context.rhtml @@ -7,10 +7,9 @@ <%= link_to( sanitize("#{context.name}"), { :controller => "context", :action => "show", :name => urlize(context.name) }, { :title => "Go to the #{context.name} context page" } ) %>
-
- <%= render :partial => "shared/empty", - :locals => { :message => "Currently there are no uncompleted actions in this context"} %> +
+

Currently there are no uncompleted actions in this context

<%= render :partial => "todo/item", :collection => @not_done %>
-
+ \ No newline at end of file diff --git a/tracks/app/views/context/add_item.rjs b/tracks/app/views/context/add_item.rjs new file mode 100644 index 00000000..3f1f8ad7 --- /dev/null +++ b/tracks/app/views/context/add_item.rjs @@ -0,0 +1,14 @@ +if @saved + page.insert_html :bottom, "c#{@item.context_id}", :partial => 'todo/item' + page.hide "status" + page.replace_html "status", content_tag("div", "Added new next action", "class" => "confirmation") + page.visual_effect :appear, 'status', :duration => 0.5 + page.replace_html "badge_count", @up_count + page.visual_effect :highlight, "item-#{@item.id}-container", :duration => 3 + page.hide "empty-nd" # If we are adding an new action, the uncompleted actions must be > 0 + page.send :record, "Form.reset('todo-form-new-action');Form.focusFirstElement('todo-form-new-action')" +else + page.hide "status" + page.replace_html "status", content_tag("div", content_tag("h2", "#{pluralize(@item.errors.count, "error")} prohibited this record from being saved") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @item.errors.each_full { |msg| content_tag("li", msg) }), "id" => "ErrorExplanation", "class" => "ErrorExplanation") + page.visual_effect :appear, 'status', :duration => 0.5 +end \ No newline at end of file diff --git a/tracks/app/views/context/destroy_action.rjs b/tracks/app/views/context/destroy_action.rjs new file mode 100644 index 00000000..fb3f340e --- /dev/null +++ b/tracks/app/views/context/destroy_action.rjs @@ -0,0 +1,11 @@ +if @saved + page.alert "Are you sure that you want to delete the next action: \'#{@item.description}\'?" + page.visual_effect :fade, "item-#{@item.id}-container", :duration => 2.0 + page.remove "item-#{@item.id}-container" + page.replace_html "badge_count", @down_count + if @down_count == "0" + page.show 'empty-nd' + end +else + page.replace_html "status", content_tag("div", content_tag("h2", "#{pluralize(@item.errors.count, "error")} prohibited this record from being saved") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @item.errors.each_full { |msg| content_tag("li", msg) }), "id" => "ErrorExplanation", "class" => "ErrorExplanation") +end \ No newline at end of file diff --git a/tracks/app/views/context/error.rjs b/tracks/app/views/context/error.rjs new file mode 100644 index 00000000..f4f6b35c --- /dev/null +++ b/tracks/app/views/context/error.rjs @@ -0,0 +1 @@ +page.replace_html "status", "A server error has occurred" \ No newline at end of file diff --git a/tracks/app/views/context/show.rhtml b/tracks/app/views/context/show.rhtml index e0461ef8..687d9e07 100644 --- a/tracks/app/views/context/show.rhtml +++ b/tracks/app/views/context/show.rhtml @@ -7,9 +7,10 @@
-<%= render "shared/add_new_item_form" %> +<%= render :partial => "shared/add_new_item_form", :locals => {:hide_link => false, :msg => ""} %> <%= render "shared/sidebar" %>
-<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> -<% if @flash["warning"] %>
<%= @flash["warning"] %>
<% end %> +<% if @flash["warning"] %> +
<%= @flash["warning"] %>
+<% end %> diff --git a/tracks/app/views/layouts/standard.rhtml b/tracks/app/views/layouts/standard.rhtml index dbdc8c6e..d2c59d0b 100644 --- a/tracks/app/views/layouts/standard.rhtml +++ b/tracks/app/views/layouts/standard.rhtml @@ -21,7 +21,7 @@

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

diff --git a/tracks/app/views/project/_project.rhtml b/tracks/app/views/project/_project.rhtml index d36cfccf..9fbd4121 100644 --- a/tracks/app/views/project/_project.rhtml +++ b/tracks/app/views/project/_project.rhtml @@ -1,4 +1,5 @@ <% @not_done = project.find_not_done_todos -%> +

<% if collapsible %> @@ -14,10 +15,8 @@

Project has been marked as completed

<% end -%>
-
- <%= render :partial => "shared/empty", - :locals => { :message => "Currently there are no uncompleted actions in this project"} %> +

Currently there are no uncompleted actions in this project

<%= render :partial => "todo/item", :collection => @not_done %>
diff --git a/tracks/app/views/project/add_item.rjs b/tracks/app/views/project/add_item.rjs new file mode 100644 index 00000000..f94e5b31 --- /dev/null +++ b/tracks/app/views/project/add_item.rjs @@ -0,0 +1,14 @@ +if @saved + page.insert_html :bottom, "p#{@item.project_id}", :partial => 'todo/item' + page.hide "status" + page.replace_html "status", content_tag("div", "Added new next action", "class" => "confirmation") + page.visual_effect :appear, 'status', :duration => 0.5 + page.replace_html "badge_count", @up_count + page.visual_effect :highlight, "item-#{@item.id}-container", :duration => 3 + page.hide "empty-nd" # If we are adding an new action, the uncompleted actions must be > 0 + page.send :record, "Form.reset('todo-form-new-action');Form.focusFirstElement('todo-form-new-action')" +else + page.hide "status" + page.replace_html "status", content_tag("div", content_tag("h2", "#{pluralize(@item.errors.count, "error")} prohibited this record from being saved") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @item.errors.each_full { |msg| content_tag("li", msg) }), "id" => "ErrorExplanation", "class" => "ErrorExplanation") + page.visual_effect :appear, 'status', :duration => 0.5 +end \ No newline at end of file diff --git a/tracks/app/views/project/destroy_action.rjs b/tracks/app/views/project/destroy_action.rjs new file mode 100644 index 00000000..fb3f340e --- /dev/null +++ b/tracks/app/views/project/destroy_action.rjs @@ -0,0 +1,11 @@ +if @saved + page.alert "Are you sure that you want to delete the next action: \'#{@item.description}\'?" + page.visual_effect :fade, "item-#{@item.id}-container", :duration => 2.0 + page.remove "item-#{@item.id}-container" + page.replace_html "badge_count", @down_count + if @down_count == "0" + page.show 'empty-nd' + end +else + page.replace_html "status", content_tag("div", content_tag("h2", "#{pluralize(@item.errors.count, "error")} prohibited this record from being saved") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @item.errors.each_full { |msg| content_tag("li", msg) }), "id" => "ErrorExplanation", "class" => "ErrorExplanation") +end \ No newline at end of file diff --git a/tracks/app/views/project/error.rjs b/tracks/app/views/project/error.rjs new file mode 100644 index 00000000..f4f6b35c --- /dev/null +++ b/tracks/app/views/project/error.rjs @@ -0,0 +1 @@ +page.replace_html "status", "A server error has occurred" \ No newline at end of file diff --git a/tracks/app/views/project/show.rhtml b/tracks/app/views/project/show.rhtml index ba478be9..bc3501d2 100644 --- a/tracks/app/views/project/show.rhtml +++ b/tracks/app/views/project/show.rhtml @@ -39,7 +39,7 @@
-<%= render "shared/add_new_item_form" %> +<%= render "shared/_add_new_item_form" %> <%= render "shared/sidebar" %>
diff --git a/tracks/app/views/shared/_add_new_item_form.rhtml b/tracks/app/views/shared/_add_new_item_form.rhtml new file mode 100644 index 00000000..00ac4458 --- /dev/null +++ b/tracks/app/views/shared/_add_new_item_form.rhtml @@ -0,0 +1,57 @@ +<% + case controller.controller_name + when "context" + add_string = "Add a next action in this context »" + when "project" + add_string = "Add a next action in this project »" + else + add_string = "Add a next action »" + end +%> + +<% unless hide_link -%> +<%= link_to_function( + add_string, +"Element.toggle('todo_new_action');Form.focusFirstElement('todo-form-new-action');", + {:title => "Add the next action", :accesskey => "n"}) %> +<% end -%> + +
+
+
+ +<%= form_remote_tag( + :url => { :controller => controller.controller_name, :action => "add_item" }, + :html=> { :id=>'todo-form-new-action', :name=>'todo', :class => 'inline-form' }) %> + +
+<%= text_field( "todo", "description", "size" => 25, "tabindex" => 1) %>
+ +
+<%= text_area( "todo", "notes", "cols" => 25, "rows" => 10, "tabindex" => 2) %>
+ +<% unless controller.controller_name == "context" -%> +
+ <%= collection_select( "todo", "context_id", @contexts, "id", "name", + {}, {"tabindex" => 3}) %>
+<% end -%> + +<% unless controller.controller_name == "project" -%> +
+ <%= collection_select( "todo", "project_id", @projects, "id", "name", + { :include_blank => true }, {"tabindex" => 4}) %>
+<% end -%> + +
+<%= text_field("todo", "due", "size" => 10, "class" => "Date", "onFocus" => "Calendar.setup", "tabindex" => 5) %> + +<% if controller.controller_name == "project" -%> + <%= hidden_field( "todo", "project_id", "value" => "#{@project.id}") %> +<% elsif controller.controller_name == "context" -%> + <%= hidden_field( "todo", "context_id", "value" => "#{@context.id}") %> +<% end -%> +

+ +<%= end_form_tag %> +<%= calendar_setup( "todo_due" ) %> +
diff --git a/tracks/app/views/shared/add_new_item_form.rhtml b/tracks/app/views/shared/add_new_item_form.rhtml deleted file mode 100644 index b9134026..00000000 --- a/tracks/app/views/shared/add_new_item_form.rhtml +++ /dev/null @@ -1,61 +0,0 @@ -<% - case controller.controller_name - when "context" - add_string = "Add a next action in this context »" - update_div = "c" + @context.id.to_s - when "project" - add_string = "Add a next action in this project »" - update_div = "p" + @project.id.to_s - else - add_string = "Add a next action »" - update_div = "new_actions" - end -%> - -<%= link_to_function( - add_string, -"Element.toggle('todo_new_action');Form.focusFirstElement('todo-form-new-action');", - {:title => "Add the next action", :accesskey => "n"}) %> - - diff --git a/tracks/app/views/todo/_item.rhtml b/tracks/app/views/todo/_item.rhtml index 1cb492b7..7db42e8f 100644 --- a/tracks/app/views/todo/_item.rhtml +++ b/tracks/app/views/todo/_item.rhtml @@ -4,7 +4,7 @@ <%= form_remote_tag_toggle_todo( item ) %> <%= form_tag( { :controller => "todo", :action => "toggle_check", :id => item.id }, { :class => "inline-form" }) %> - <%= link_to_remote_todo( item ) %> + <%= link_to_remote_todo( item, controller.controller_name ) %> checked="checked" <% end %> />
<% # start of div which has a class 'description', and possibly 'stale_11', 'stale_12', 'stale_13' etc %> <% if item.done? -%> diff --git a/tracks/app/views/todo/add_item.rjs b/tracks/app/views/todo/add_item.rjs new file mode 100644 index 00000000..83caeb65 --- /dev/null +++ b/tracks/app/views/todo/add_item.rjs @@ -0,0 +1,13 @@ +if @saved + page.insert_html :bottom, "c#{@item.context_id}", :partial => 'todo/item' + page.hide "status" + page.replace_html "status", content_tag("div", "Added new next action", "class" => "confirmation") + page.visual_effect :appear, 'status', :duration => 0.5 + page.replace_html "badge_count", @up_count + page.visual_effect :highlight, "item-#{@item.id}-container", :duration => 3 + page.send :record, "Form.reset('todo-form-new-action');Form.focusFirstElement('todo-form-new-action')" +else + page.hide "status" + page.replace_html "status", content_tag("div", content_tag("h2", "#{pluralize(@item.errors.count, "error")} prohibited this record from being saved") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @item.errors.each_full { |msg| content_tag("li", msg) }), "id" => "ErrorExplanation", "class" => "ErrorExplanation") + page.visual_effect :appear, 'status', :duration => 0.5 +end \ No newline at end of file diff --git a/tracks/app/views/todo/destroy_action.rjs b/tracks/app/views/todo/destroy_action.rjs new file mode 100644 index 00000000..4dd2b03a --- /dev/null +++ b/tracks/app/views/todo/destroy_action.rjs @@ -0,0 +1,8 @@ +if @saved + page.alert "Are you sure that you want to delete the next action: \'#{@item.description}\'?" + page.visual_effect :fade, "item-#{@item.id}-container", :duration => 2.0 + page.remove "item-#{@item.id}-container" + page.replace_html "badge_count", @down_count +else + page.replace_html "status", content_tag("div", content_tag("h2", "#{pluralize(@item.errors.count, "error")} prohibited this record from being saved") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @item.errors.each_full { |msg| content_tag("li", msg) }), "id" => "ErrorExplanation", "class" => "ErrorExplanation") +end \ No newline at end of file diff --git a/tracks/app/views/todo/error.rjs b/tracks/app/views/todo/error.rjs new file mode 100644 index 00000000..90996ca6 --- /dev/null +++ b/tracks/app/views/todo/error.rjs @@ -0,0 +1 @@ +page.replace_html "status", "An error occurred on the server." \ No newline at end of file diff --git a/tracks/app/views/todo/list.rhtml b/tracks/app/views/todo/list.rhtml index 16c7bc28..70f56e6e 100644 --- a/tracks/app/views/todo/list.rhtml +++ b/tracks/app/views/todo/list.rhtml @@ -1,7 +1,4 @@
- <%= render :partial => "context/context", :collection => @contexts_to_show, :locals => { :collapsible => true } %> <%= render :partial => "todo/completed", @@ -9,13 +6,11 @@
- <%= render "shared/add_new_item_form" %> + <%= render :partial => "shared/add_new_item_form", :locals => {:hide_link => false, :msg => ""} %> <%= render "shared/sidebar" %>
-<% if @flash["confirmation"] -%> -
<%= @flash["confirmation"] %>
-<% end -%> +
<% if @flash["warning"] -%>
<%= @flash["warning"] %>
<% end -%> diff --git a/tracks/config/routes.rb b/tracks/config/routes.rb index 46263ff7..aabc08a5 100644 --- a/tracks/config/routes.rb +++ b/tracks/config/routes.rb @@ -53,7 +53,7 @@ ActionController::Routing::Routes.draw do |map| map.connect 'feed/:action/:name/:user', :controller => 'feed' - map.connect 'add_item', :controller => 'todo', :action => 'add_item' + #map.connect 'add_item', :controller => 'todo', :action => 'add_item' # Install the default route as the lowest priority. map.connect ':controller/:action/:id' diff --git a/tracks/doc/CHANGELOG b/tracks/doc/CHANGELOG index a6e310dc..56d96f8a 100644 --- a/tracks/doc/CHANGELOG +++ b/tracks/doc/CHANGELOG @@ -31,6 +31,9 @@ Wiki (deprecated - please use Trac): http://www.rousette.org.uk/projects/wiki/ 13. The TXT view is now sorted by position, just as the home page is. 14. Contributed by lolindrath: Items that are overdue are coloured red, and have the text 'Overdue by X days' in the badge. Other due dates are given as days from now (up to a week away in orange, more than a week away in green), and the tool tip shows the actual date. 15. Projects and Contexts in [tracks_url]/projects and [tracks_url]/contexts can now be rearranged in order by dragging and dropping. Just pick the item up by the 'DRAG' label and drop it where you want. +16. Got rid of the 'fresh actions' box on the home page. When you create a new action, it is now automatically inserted (via the magic of Ajax and RJS templates) at the bottom of the correct context box. +17. The next action count badge is now dynamically updated whenever you add or delete a next action, so that you don't have to refresh to see the count updated. +18. Validation errors are now reported in a status box (just above the new next action form). == Version 1.03 diff --git a/tracks/public/stylesheets/standard.css b/tracks/public/stylesheets/standard.css index 40abb715..80c71b67 100644 --- a/tracks/public/stylesheets/standard.css +++ b/tracks/public/stylesheets/standard.css @@ -273,21 +273,17 @@ a.footer_link:hover {color: #fff; background-color: #cc3334 !important;} /* The alert box notifications */ .confirmation { - margin: 20px 50px 20px 490px; border: 1px solid #007E00; background-color: #c2ffc2; - padding: 5px; color: #007E00; text-align: center; } .warning { - margin: 20px 50px 20px 490px; border: 1px solid #ED2E38; background-color: #F6979C; - padding: 5px; color: #ED2E38; - text-align: center; + text-align: left; } .project_completed { @@ -519,4 +515,43 @@ div.message { text-align: center; font-size: 1em; color: #666; -} \ No newline at end of file +} + +/* Error message styles */ +.fieldWithErrors { + padding: 2px; + background-color: red; + display: table; +} + +#ErrorExplanation { + width: 400px; + border: 2px solid #red; + padding: 7px; + padding-bottom: 12px; + margin-bottom: 20px; + background-color: #f0f0f0; +} + +#ErrorExplanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + background-color: #c00; + color: #fff; +} + +#ErrorExplanation p { + color: #333; + margin-bottom: 0; + padding: 5px; +} + +#ErrorExplanation ul li { + font-size: 12px; + list-style: square; +} + +ul.warning { list-style-type: circle; font-size: 1em; } diff --git a/tracks/vendor/plugins/javascript_generator_templates/CHANGELOG b/tracks/vendor/plugins/javascript_generator_templates/CHANGELOG new file mode 100644 index 00000000..843fccfe --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/CHANGELOG @@ -0,0 +1,15 @@ +December 25, 2005 +* Added changeset 3316: http://dev.rubyonrails.org/changeset/3316 +* Added tests for javascript_generator_templates up to changeset 3116 +* Added changeset 3319: http://dev.rubyonrails.org/changeset/3319 + * Adds support for alert, redirect_to, call, assign, << +* Added changeset 3329 and 3335 + * Adds support for toggle, delay + +December 15, 2005 +* Updated prototype.js to 1.4.0 +* Enabled the test test_render_file_with_locals +* Add CHANGELOG file +* Update README to reflect version 1.0 of Rails +* Added MIT-LICENSE + diff --git a/tracks/vendor/plugins/javascript_generator_templates/MIT-LICENSE b/tracks/vendor/plugins/javascript_generator_templates/MIT-LICENSE new file mode 100644 index 00000000..5919c288 --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2004 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/tracks/vendor/plugins/javascript_generator_templates/README b/tracks/vendor/plugins/javascript_generator_templates/README new file mode 100644 index 00000000..c4973e70 --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/README @@ -0,0 +1,15 @@ +JavaScriptGeneratorTemplates +============================ + +This plugin allows the usage of the new RJS templates without having to run +edge rails. For more information about RJS templates please check out these +resources on the web: + +http://rails.techno-weenie.net/tip/2005/11/29/ajaxed_forms_with_rjs_templates +http://www.codyfauser.com/articles/2005/11/20/rails-rjs-templates +http://dev.rubyonrails.org/changeset/3078 + +The RJS templates need at least version 1.4.0_rc4 of the prototype library to +function correctly. Run rake update_prototype from this source directory to +update your project's version of prototype to 1.4.0. + diff --git a/tracks/vendor/plugins/javascript_generator_templates/Rakefile b/tracks/vendor/plugins/javascript_generator_templates/Rakefile new file mode 100644 index 00000000..30f44248 --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/Rakefile @@ -0,0 +1,27 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the javascript_generator_templates plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the javascript_generator_templates plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'JavaScriptGeneratorTemplates' + rdoc.options << '--line-numbers --inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end + +desc "Install prototype.js file to public/javascripts" +task :update_prototype do + FileUtils.cp(File.dirname(__FILE__) + "/javascripts/prototype.js", File.dirname(__FILE__) + '/../../../public/javascripts/') +end diff --git a/tracks/vendor/plugins/javascript_generator_templates/init.rb b/tracks/vendor/plugins/javascript_generator_templates/init.rb new file mode 100644 index 00000000..6272338b --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/init.rb @@ -0,0 +1,3 @@ +require 'add_rjs_to_action_view' +require 'add_rjs_to_action_controller' +require 'add_rjs_to_javascript_helper' diff --git a/tracks/vendor/plugins/javascript_generator_templates/javascripts/prototype.js b/tracks/vendor/plugins/javascript_generator_templates/javascripts/prototype.js new file mode 100644 index 00000000..e9ccd3c8 --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/javascripts/prototype.js @@ -0,0 +1,1785 @@ +/* Prototype JavaScript framework, version 1.4.0 + * (c) 2005 Sam Stephenson + * + * 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/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.4.0', + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} + +/*--------------------------------------------------------------------------*/ + +function $() { + var elements = new Array(); + + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + + if (arguments.length == 1) + return element; + + elements.push(element); + } + + return elements; +} +Object.extend(String.prototype, { + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(eval); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + } +}); + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value >= (result || value)) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value <= (result || value)) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + iterator(value = collections.pluck(index)); + return value; + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + shift: function() { + var result = this[0]; + for (var i = 0; i < this.length - 1; i++) + this[i] = this[i + 1]; + this.length--; + return result; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')}, + function() {return new XMLHttpRequest()} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version]; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval(this.header('X-JSON')); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(child); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) { + var Element = new Object(); +} + +Object.extend(Element, { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + 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); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +}); + +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.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + if (this.element.tagName.toLowerCase() == 'tbody') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
'; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + 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 = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + 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) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) + value = opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) + optValue = opt.text; + value.push(optValue); + } + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_controller.rb b/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_controller.rb new file mode 100644 index 00000000..91d79c15 --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_controller.rb @@ -0,0 +1,67 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ +module ActionController #:nodoc: + class Base + protected + def render_action(action_name, status = nil, with_layout = true) + template = default_template_name(action_name) + if with_layout && !template_exempt_from_layout?(template) + render_with_layout(template, status) + else + render_without_layout(template, status) + end + end + + private + def template_exempt_from_layout?(template_name = default_template_name) + @template.javascript_template_exists?(template_name) + end + + def default_template_name(default_action_name = action_name) + default_action_name = default_action_name.dup + strip_out_controller!(default_action_name) if template_path_includes_controller?(default_action_name) + "#{self.class.controller_path}/#{default_action_name}" + end + + def strip_out_controller!(path) + path.replace path.split('/', 2).last + end + + def template_path_includes_controller?(path) + path.to_s['/'] && self.class.controller_path.split('/')[-1] == path.split('/')[0] + end + end + + module Layout #:nodoc: + private + def apply_layout?(template_with_options, options) + template_with_options ? candidate_for_layout?(options) : !template_exempt_from_layout? + end + + def candidate_for_layout?(options) + (options.has_key?(:layout) && options[:layout] != false) || + options.values_at(:text, :file, :inline, :partial, :nothing).compact.empty? && + !template_exempt_from_layout?(default_template_name(options[:action] || options[:template])) + end + end +end diff --git a/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_view.rb b/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_view.rb new file mode 100644 index 00000000..7b2c2de2 --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_action_view.rb @@ -0,0 +1,142 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ +module ActionView + class Base + def pick_template_extension(template_path)#:nodoc: + if match = delegate_template_exists?(template_path) + match.first + elsif erb_template_exists?(template_path): 'rhtml' + elsif builder_template_exists?(template_path): 'rxml' + elsif javascript_template_exists?(template_path): 'rjs' + else + raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path}" + end + end + + def javascript_template_exists?(template_path)#:nodoc: + template_exists?(template_path, :rjs) + end + + def file_exists?(template_path)#:nodoc: + %w(erb builder javascript delegate).any? do |template_type| + send("#{template_type}_template_exists?", template_path) + end + end + + private + # Create source code for given template + def create_template_source(extension, template, render_symbol, locals) + if template_requires_setup?(extension) + body = case extension.to_sym + when :rxml + "xml = Builder::XmlMarkup.new(:indent => 2)\n" + + "@controller.headers['Content-Type'] ||= 'text/xml'\n" + + template + when :rjs + "@controller.headers['Content-Type'] ||= 'text/javascript'\n" + + "update_page do |page|\n#{template}\nend" + end + else + body = ERB.new(template, nil, @@erb_trim_mode).src + end + + @@template_args[render_symbol] ||= {} + locals_keys = @@template_args[render_symbol].keys | locals + @@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h } + + locals_code = "" + locals_keys.each do |key| + locals_code << "#{key} = local_assigns[:#{key}] if local_assigns.has_key?(:#{key})\n" + end + + "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend" + end + + def template_requires_setup?(extension) + templates_requiring_setup.include? extension.to_s + end + + def templates_requiring_setup + %w(rxml rjs) + end + + def assign_method_name(extension, template, file_name) + method_name = '_run_' + method_name << "#{extension}_" if extension + + if file_name + file_path = File.expand_path(file_name) + base_path = File.expand_path(@base_path) + + i = file_path.index(base_path) + l = base_path.length + + method_name_file_part = i ? file_path[i+l+1,file_path.length-l-1] : file_path.clone + method_name_file_part.sub!(/\.r(html|xml|js)$/,'') + method_name_file_part.tr!('/:-', '_') + method_name_file_part.gsub!(/[^a-zA-Z0-9_]/){|s| s[0].to_s} + + method_name += method_name_file_part + else + @@inline_template_count += 1 + method_name << @@inline_template_count.to_s + end + + @@method_names[file_name || template] = method_name.intern + end + + def compile_template(extension, template, file_name, local_assigns) + method_key = file_name || template + + render_symbol = @@method_names[method_key] || assign_method_name(extension, template, file_name) + render_source = create_template_source(extension, template, render_symbol, local_assigns.keys) + + line_offset = @@template_args[render_symbol].size + if extension + case extension.to_sym + when :rxml, :rjs + line_offset += 2 + end + end + + begin + unless file_name.blank? + CompiledTemplates.module_eval(render_source, file_name, -line_offset) + else + CompiledTemplates.module_eval(render_source, 'compiled-template', -line_offset) + end + rescue Object => e + if logger + logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}" + logger.debug "Function body: #{render_source}" + logger.debug "Backtrace: #{e.backtrace.join("\n")}" + end + + raise TemplateError.new(@base_path, method_key, @assigns, template, e) + end + + @@compile_time[render_symbol] = Time.now + # logger.debug "Compiled template #{method_key}\n ==> #{render_symbol}" if logger + end + end +end diff --git a/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_javascript_helper.rb b/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_javascript_helper.rb new file mode 100644 index 00000000..04201e46 --- /dev/null +++ b/tracks/vendor/plugins/javascript_generator_templates/lib/add_rjs_to_javascript_helper.rb @@ -0,0 +1,204 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ +module ActionView + module Helpers + module JavaScriptHelper + # JavaScriptGenerator generates blocks of JavaScript code that allow you + # to change the content and presentation of multiple DOM elements. Use + # this in your Ajax response bodies, either in a ), + periodically_call_remote(:update => "schremser_bier", :url => { :action => "mehr_bier" }) + end + + def test_form_remote_tag + assert_dom_equal %(
), + form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }) + assert_dom_equal %(), + form_remote_tag(:update => { :success => "glass_of_beer" }, :url => { :action => :fast }) + assert_dom_equal %(), + form_remote_tag(:update => { :failure => "glass_of_water" }, :url => { :action => :fast }) + assert_dom_equal %(), + form_remote_tag(:update => { :success => 'glass_of_beer', :failure => "glass_of_water" }, :url => { :action => :fast }) + end + + def test_on_callbacks + callbacks = [:uninitialized, :loading, :loaded, :interactive, :complete, :success, :failure] + callbacks.each do |callback| + assert_dom_equal %(), + form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }, callback=>"monkeys();") + assert_dom_equal %(), + form_remote_tag(:update => { :success => "glass_of_beer" }, :url => { :action => :fast }, callback=>"monkeys();") + assert_dom_equal %(), + form_remote_tag(:update => { :failure => "glass_of_beer" }, :url => { :action => :fast }, callback=>"monkeys();") + assert_dom_equal %(), + form_remote_tag(:update => { :success => "glass_of_beer", :failure => "glass_of_water" }, :url => { :action => :fast }, callback=>"monkeys();") + end + + #HTTP status codes 200 up to 599 have callbacks + #these should work + 100.upto(599) do |callback| + assert_dom_equal %(), + form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }, callback=>"monkeys();") + end + + #test 200 and 404 + assert_dom_equal %(), + form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }, 200=>"monkeys();", 404=>"bananas();") + + #these shouldn't + 1.upto(99) do |callback| + assert_dom_equal %(), + form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }, callback=>"monkeys();") + end + 600.upto(999) do |callback| + assert_dom_equal %(), + form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }, callback=>"monkeys();") + end + + #test ultimate combo + assert_dom_equal %(), + form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast }, :loading => "c1()", :success => "s()", :failure => "f();", :complete => "c();", 200=>"monkeys();", 404=>"bananas();") + + end + + def test_submit_to_remote + assert_dom_equal %(), + submit_to_remote("More beer!", 1_000_000, :update => "empty_bottle") + end + + def test_observe_field + assert_dom_equal %(), + observe_field("glass", :frequency => 5.minutes, :url => { :action => "reorder_if_empty" }) + end + + def test_observe_form + assert_dom_equal %(), + observe_form("cart", :frequency => 2, :url => { :action => "cart_changed" }) + end + + def test_effect + assert_equal "new Effect.Highlight('posts',{});", visual_effect(:highlight, "posts") + assert_equal "new Effect.Highlight('posts',{});", visual_effect("highlight", :posts) + assert_equal "new Effect.Highlight('posts',{});", visual_effect(:highlight, :posts) + assert_equal "new Effect.Fade('fademe',{duration:4.0});", visual_effect(:fade, "fademe", :duration => 4.0) + assert_equal "new Effect.Shake(element,{});", visual_effect(:shake) + assert_equal "new Effect.DropOut('dropme',{queue:'end'});", visual_effect(:drop_out, 'dropme', :queue => :end) + end + + def test_sortable_element + assert_dom_equal %(), + sortable_element("mylist", :url => { :action => "order" }) + assert_equal %(), + sortable_element("mylist", :tag => "div", :constraint => "horizontal", :url => { :action => "order" }) + assert_dom_equal %||, + sortable_element("mylist", :containment => ['list1','list2'], :constraint => "horizontal", :url => { :action => "order" }) + assert_dom_equal %(), + sortable_element("mylist", :containment => 'list1', :constraint => "horizontal", :url => { :action => "order" }) + end + + def test_draggable_element + assert_dom_equal %(), + draggable_element('product_13') + assert_equal %(), + draggable_element('product_13', :revert => true) + end + + def test_drop_receiving_element + assert_dom_equal %(), + drop_receiving_element('droptarget1') + assert_dom_equal %(), + drop_receiving_element('droptarget1', :accept => 'products') + assert_dom_equal %(), + drop_receiving_element('droptarget1', :accept => 'products', :update => 'infobox') + assert_dom_equal %(), + drop_receiving_element('droptarget1', :accept => ['tshirts','mugs'], :update => 'infobox') + end + + def test_update_element_function + assert_equal %($('myelement').innerHTML = 'blub';\n), + update_element_function('myelement', :content => 'blub') + assert_equal %($('myelement').innerHTML = 'blub';\n), + update_element_function('myelement', :action => :update, :content => 'blub') + assert_equal %($('myelement').innerHTML = '';\n), + update_element_function('myelement', :action => :empty) + assert_equal %(Element.remove('myelement');\n), + update_element_function('myelement', :action => :remove) + + assert_equal %(new Insertion.Bottom('myelement','blub');\n), + update_element_function('myelement', :position => 'bottom', :content => 'blub') + assert_equal %(new Insertion.Bottom('myelement','blub');\n), + update_element_function('myelement', :action => :update, :position => :bottom, :content => 'blub') + + _erbout = "" + assert_equal %($('myelement').innerHTML = 'test';\n), + update_element_function('myelement') { _erbout << "test" } + + _erbout = "" + assert_equal %($('myelement').innerHTML = 'blockstuff';\n), + update_element_function('myelement', :content => 'paramstuff') { _erbout << "blockstuff" } + end + + def test_update_page + block = Proc.new { |page| page.replace_html('foo', 'bar') } + assert_equal create_generator(&block).to_s, update_page(&block) + end + + def test_update_page_tag + block = Proc.new { |page| page.replace_html('foo', 'bar') } + assert_equal javascript_tag(create_generator(&block).to_s), update_page_tag(&block) + end +end + +class JavaScriptGeneratorTest < Test::Unit::TestCase + include BaseTest + + def setup + super + @generator = create_generator + end + + def test_insert_html_with_string + assert_equal 'new Insertion.Top("element", "

This is a test

");', + @generator.insert_html(:top, 'element', '

This is a test

') + assert_equal 'new Insertion.Bottom("element", "

This is a test

");', + @generator.insert_html(:bottom, 'element', '

This is a test

') + assert_equal 'new Insertion.Before("element", "

This is a test

");', + @generator.insert_html(:before, 'element', '

This is a test

') + assert_equal 'new Insertion.After("element", "

This is a test

");', + @generator.insert_html(:after, 'element', '

This is a test

') + end + + def test_replace_html_with_string + assert_equal 'Element.update("element", "

This is a test

");', + @generator.replace_html('element', '

This is a test

') + end + + def test_remove + assert_equal '["foo"].each(Element.remove);', + @generator.remove('foo') + assert_equal '["foo", "bar", "baz"].each(Element.remove);', + @generator.remove('foo', 'bar', 'baz') + end + + def test_show + assert_equal 'Element.show("foo");', + @generator.show('foo') + assert_equal 'Element.show("foo", "bar", "baz");', + @generator.show('foo', 'bar', 'baz') + end + + def test_hide + assert_equal 'Element.hide("foo");', + @generator.hide('foo') + assert_equal 'Element.hide("foo", "bar", "baz");', + @generator.hide('foo', 'bar', 'baz') + end + + def test_alert + assert_equal 'alert("hello");', @generator.alert('hello') + end + + def test_redirect_to + assert_equal 'window.location.href = "http://www.example.com/welcome";', + @generator.redirect_to(:action => 'welcome') + end + + def test_delay + @generator.delay(20) do + @generator.hide('foo') + end + + assert_equal "setTimeout(function() {\n;\nElement.hide(\"foo\");\n}, 20000);", @generator.to_s + end + + def test_to_s + @generator.insert_html(:top, 'element', '

This is a test

') + @generator.insert_html(:bottom, 'element', '

This is a test

') + @generator.remove('foo', 'bar') + @generator.replace_html('baz', '

This is a test

') + + assert_equal <<-EOS.chomp, @generator.to_s +new Insertion.Top("element", "

This is a test

"); +new Insertion.Bottom("element", "

This is a test

"); +["foo", "bar"].each(Element.remove); +Element.update("baz", "

This is a test

"); + EOS + end +end