# Undo helper plugins for Rails # # Copyright (c) 2006 Assaf Arkin, under Creative Commons Attribution and/or MIT License # Developed for http://co.mments.com # Code and documention: http://labnotes.org # Use the undo helper to maintain a list of undo actions and render # a button to perform the last undo action. # # For example: # # Remove undo action from stack. # before_filter do |controller| # controller.undo.pop(controller.params) if controller.params[:undo] # true # end # # def create() # # Do some action that can be undone. # record = Record.create(@params) # # Create a new undo action (if not itself an undo). # unless @params[:undo] # undo.push("Delete newly created record", # :action=>"delete", :id=>record.id) # end # # Using XHR unpdate the undo action on the page. # render :update do |page| # page["undo"].replace_html undo.render # end # end module UndoHelper # Class representing the undo stack and operations that can be performed # on the stack (push, pop and render). Use UndoHelper.undo to create new # undo objects. class Undo # Default number of undo levels. Use #levels= to change the number # of undo levels. unless const_defined? :UNDO_LEVELS UNDO_LEVELS = 1 end def initialize(view) #:nodoc: @view = view @session = view.session end # Set the number of undo levels. # # The default value is one, storing only the last undo action. # # For example, in environment.rb: # UndoHelper::Undo.levels = 5 def self.levels=(levels) @@levels = levels end # Returns the number of undo levels. def self.levels() @@levels || UNDO_LEVELS end # :call-seq: # undo.push(title, url) # # Push a new undo action on the stack. # # The +title+ argument is used when rendering the undo button. # The +url+ argument is a hash used for the form action URL. # The parameter :undo=>true is automatically added. # # For example: # undo.push("Delete newly created record", # :controller=>"main", :action=>"delete", :id=>id) def push(title, url) if @session undos = @session[:undos] ||= [] undos.shift while undos.size >= Undo.levels url = Hash[*url.collect{|k,v| [k.to_sym, v.to_s]}.flatten] url[:undo] = "true" undos << {:title=>title, :url=>url} end return end # :call-seq: # undo.pop(params) # # Pop an undo entry from the stack. Call this when performing an undo # action to remove it from the stack, making the last undo action (or # no undo action) available. # # The request parameters are used to remove a specific undo action, # to deal with multiple pages at the same time. # # For example: # undo.pop(@params) if @params[:undo] def pop(params = nil) if @session and undos = @session[:undos] undos.delete_if do |undo| undo[:url].all? { |k,v| params[k] == v } end end return end # :call-seq: # undo.render(caption?, options?) => string # undo.render(options?) { |undo, options| ... } => string # # Returns an undo form with a single button to invoke the last # undo action, or an empty string if there are no undo actions # and no disabled options specified. # # This method does not remove the undo action from the stack. # # When called without a block, returns a form for the last undo # action with a single button. Uses the specified caption and # formatting options. If missing, the default caption is "Undo". # # If there are no undo actions, returns an empty string. If there # are no undo actions but the :disabled option is specified, # returns a form with a button formatted using these options. # # The following options are supported: # * :form -- HTML options to format the +form+ tag. # * :button -- HTML options to format the +submit+ tag # for an undo action. # * :disabled -- HTML options to format the +submit+ tag # if there is no undo action. # # For example: # undo.render "Undo", :form=>{:class=>"undo-form"}, # :button=>{:class=>"undo-button"} # # When called with a block, yields the undo action and arguments to # block and returns the result. Yields a hash with the keys # :url and :title for the last undo action. Yields # +nil+ if there is no undo action on the stack. def render(*args) undos = @session[:undos] undo = undos.last if undos if block_given? return yield(undo, *args) end options = args[1] || {} form_html = options[:form] || {} form_html[:class] ||= "button" if undo button_html = options[:button] || {} button_html[:title] = undo[:title] return @view.form_remote_tag(:url=>undo[:url], :html=>form_html) + @view.submit_tag(args[0] || "Undo", button_html) + @view.end_form_tag elsif button_html = options[:disabled] button_html[:disabled] = true return @view.form_remote_tag(:html=>form_html) + @view.submit_tag(args[0] || "Undo", button_html) + @view.end_form_tag else return "" end end end # Returns an Undo object which you can use to push and pop undo actions. # # See Undo.push, Undo.pop and Undo.render. def undo Undo.new(self) end end