0001"""
0002Prototype Helpers
0003
0004Provides a set of helpers for calling Prototype JavaScript functions, 
0005including functionality to call remote methods using 
0006`Ajax <http://www.adaptivepath.com/publications/essays/archives/000385.php>`_. 
0007This means that you can call actions in your controllers without
0008reloading the page, but still update certain parts of it using
0009injections into the DOM. The common use case is having a form that adds
0010a new element to a list without reloading the page.
0011
0012To be able to use these helpers, you must include the Prototype 
0013JavaScript framework in your pages.
0014
0015See `link_to_remote <module-railshelpers.helpers.javascript.html#link_to_function>`_ 
0016for documentation of options common to all Ajax helpers.
0017
0018See also `Scriptaculous <module-railshelpers.helpers.scriptaculous.html>`_ for
0019helpers which work with the Scriptaculous controls and visual effects library.
0020"""
0021# Last synced with Rails copy at Revision 6057 on Feb 9th, 2007.
0022
0023import sys
0024if sys.version < '2.4':
0025    from sets import ImmutableSet as frozenset
0026
0027from javascript import *
0028from javascript import options_for_javascript
0029from form_tag import form
0030from tags import tag, camelize
0031from urls import get_url
0032
0033CALLBACKS = frozenset(['uninitialized', 'loading', 'loaded',
0034                       'interactive', 'complete', 'failure', 'success'] +                             [str(x) for x in range(100,599)])
0036AJAX_OPTIONS = frozenset(['before', 'after', 'condition', 'url',
0037                          'asynchronous', 'method', 'insertion', 'position',
0038                          'form', 'with', 'with_', 'update', 'script'] +                                list(CALLBACKS))
0040
0041def link_to_remote(name, options=None, **html_options):
0042    """
0043    Links to a remote function
0044    
0045    Returns a link to a remote action defined ``dict(url=url())``
0046    (using the url() format) that's called in the background using 
0047    XMLHttpRequest. The result of that request can then be inserted into a
0048    DOM object whose id can be specified with the ``update`` keyword. 
0049    
0050    Any keywords given after the second dict argument are considered html options
0051    and assigned as html attributes/values for the element.
0052    
0053    Example::
0054    
0055        link_to_remote("Delete this post", dict(update="posts", 
0056                       url=url(action="destroy", id=post.id)))
0057    
0058    You can also specify a dict for ``update`` to allow for easy redirection
0059    of output to an other DOM element if a server-side error occurs:
0060    
0061    Example::
0062
0063        link_to_remote("Delete this post",
0064                dict(url=url(action="destroy", id=post.id),
0065                     update=dict(success="posts", failure="error")))
0066    
0067    Optionally, you can use the ``position`` parameter to influence how the
0068    target DOM element is updated. It must be one of 'before', 'top', 'bottom',
0069    or 'after'.
0070    
0071    By default, these remote requests are processed asynchronous during 
0072    which various JavaScript callbacks can be triggered (for progress 
0073    indicators and the likes). All callbacks get access to the 
0074    ``request`` object, which holds the underlying XMLHttpRequest. 
0075    
0076    To access the server response, use ``request.responseText``, to
0077    find out the HTTP status, use ``request.status``.
0078    
0079    Example::
0080
0081        link_to_remote(word,
0082                dict(url=url(action="undo", n=word_counter),
0083                     complete="undoRequestCompleted(request)"))
0084    
0085    The callbacks that may be specified are (in order):
0086    
0087    ``loading``
0088        Called when the remote document is being loaded with data by the browser.
0089    ``loaded``
0090        Called when the browser has finished loading the remote document.
0091    ``interactive``
0092        Called when the user can interact with the remote document, even
0093        though it has not finished loading.
0094    ``success``
0095        Called when the XMLHttpRequest is completed, and the HTTP status
0096        code is in the 2XX range.
0097    ``failure``
0098        Called when the XMLHttpRequest is completed, and the HTTP status code is
0099        not in the 2XX range.
0100    ``complete``
0101        Called when the XMLHttpRequest is complete (fires after success/failure
0102        if they are present).
0103                        
0104    You can further refine ``success`` and ``failure`` by 
0105    adding additional callbacks for specific status codes.
0106    
0107    Example::
0108    
0109        link_to_remote(word,
0110                dict(url=url(action="action"),
0111                     404="alert('Not found...? Wrong URL...?')",
0112                     failure="alert('HTTP Error ' + request.status + '!')"))
0113    
0114    A status code callback overrides the success/failure handlers if 
0115    present.
0116    
0117    If you for some reason or another need synchronous processing (that'll
0118    block the browser while the request is happening), you can specify 
0119    ``type='synchronous'``.
0120    
0121    You can customize further browser side call logic by passing in
0122    JavaScript code snippets via some optional parameters. In their order 
0123    of use these are:
0124    
0125    ``confirm``
0126        Adds confirmation dialog.
0127    ``condition``
0128        Perform remote request conditionally by this expression. Use this to
0129        describe browser-side conditions when request should not be initiated.
0130    ``before``
0131        Called before request is initiated.
0132    ``after``
0133        Called immediately after request was initiated and before ``loading``.
0134    ``submit``
0135        Specifies the DOM element ID that's used as the parent of the form
0136        elements. By default this is the current form, but it could just as
0137        well be the ID of a table row or any other DOM element.    
0138    """
0139    if options is None:
0140        options = {}
0141    return link_to_function(name, remote_function(**options), **html_options)
0142
0143def periodically_call_remote(**options):
0144    """
0145    Periodically calls a remote function
0146    
0147    Periodically calls the specified ``url`` every ``frequency`` seconds
0148    (default is 10). Usually used to update a specified div ``update``
0149    with the results of the remote call. The options for specifying the
0150    target with ``url`` and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.    
0151    """
0152    frequency = options.get('frequency') or 10
0153    code = "new PeriodicalExecuter(function() {%s}, %s)" % (remote_function(**options), frequency)
0154    return javascript_tag(code)
0155
0156def form_remote_tag(**options):
0157    """
0158    Create a form tag using a remote function to submit the request
0159    
0160    Returns a form tag that will submit using XMLHttpRequest in the 
0161    background instead of the regular reloading POST arrangement. Even 
0162    though it's using JavaScript to serialize the form elements, the form
0163    submission will work just like a regular submission as viewed by the
0164    receiving side. The options for specifying the target with ``url``
0165    and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.
0166    
0167    A "fall-through" target for browsers that doesn't do JavaScript can be
0168    specified with the ``action/method`` options on ``html``.
0169    
0170    Example::
0171
0172        form_remote_tag(html=dict(action=url(
0173                                    controller="some", action="place")))
0174    
0175    By default the fall-through action is the same as the one specified in 
0176    the ``url`` (and the default method is ``POST``).
0177    """
0178    options['form'] = True
0179    if 'html' not in options: options['html'] = {}
0180    options['html']['onsubmit'] = "%s; return false;" % remote_function(**options)
0181    action = options['html'].get('action', get_url(options['url']))
0182    options['html']['method'] = options['html'].get('method', 'POST')
0183
0184    return form(action, **options['html'])
0185
0186def submit_to_remote(name, value, **options):
0187    """
0188    A submit button that submits via an XMLHttpRequest call
0189    
0190    Returns a button input tag that will submit form using XMLHttpRequest 
0191    in the background instead of regular reloading POST arrangement. 
0192    Keyword args are the same as in ``form_remote_tag``.    
0193    """
0194    options['with_'] = options.get('form') or 'Form.serialize(this.form)'
0195
0196    options['html'] = options.get('html') or {}
0197    options['html']['type'] = 'button'
0198    options['html']['onclick'] = "%s; return false;" % remote_function(**options)
0199    options['html']['name_'] = name
0200    options['html']['value'] = '%s' % value
0201
0202    return tag("input", open=False, **options['html'])
0203
0204def update_element_function(element_id, **options):
0205    """
0206    Returns a JavaScript function (or expression) that'll update a DOM 
0207    element.
0208    
0209    ``content``
0210        The content to use for updating.
0211    ``action``
0212        Valid options are 'update' (assumed by default), 'empty', 'remove'
0213    ``position``
0214        If the ``action`` is 'update', you can optionally specify one of the
0215        following positions: 'before', 'top', 'bottom', 'after'.
0216    
0217    Example::
0218    
0219        <% javascript_tag(update_element_function("products", 
0220            position='bottom', content="<p>New product!</p>")) %>
0221    
0222    This method can also be used in combination with remote method call 
0223    where the result is evaluated afterwards to cause multiple updates on
0224    a page. Example::
0225    
0226        # Calling view
0227        <% form_remote_tag(url=url(action="buy"), 
0228                complete=evaluate_remote_response()) %>
0229            all the inputs here...
0230    
0231        # Controller action
0232        def buy(self, **params):
0233            c.product = Product.find(1)
0234            return render_response('/buy.myt')
0235    
0236        # Returning view (buy.myt)
0237        <% update_element_function(
0238                "cart", action='update', position='bottom', 
0239                content="<p>New Product: %s</p>" % c.product.name) %>
0240        <% update_element_function("status", binding='binding',
0241                content="You've bought a new product!") %>
0242    """
0243    content = escape_javascript(options.get('content', ''))
0244    opval = options.get('action', 'update')
0245    if opval == 'update':
0246        if options.get('position'):
0247            jsf = "new Insertion.%s('%s','%s')" % (camelize(options['position']), element_id, content)
0248        else:
0249            jsf = "$('%s').innerHTML = '%s'" % (element_id, content)
0250    elif opval == 'empty':
0251        jsf = "$('%s').innerHTML = ''" % element_id
0252    elif opval == 'remove':
0253        jsf = "Element.remove('%s')" % element_id
0254    else:
0255        raise ValueError("Invalid action, choose one of update, remove, or empty")
0256
0257    jsf += ";\n"
0258    if options.get('binding'):
0259        return jsf + options['binding']
0260    else:
0261        return jsf
0262
0263def evaluate_remote_response():
0264    """
0265    Returns a Javascript function that evals a request response
0266    
0267    Returns 'eval(request.responseText)' which is the JavaScript function
0268    that ``form_remote_tag`` can call in *complete* to evaluate a multiple
0269    update return document using ``update_element_function`` calls.    
0270    """
0271    return "eval(request.responseText)"
0272
0273def remote_function(**options):
0274    """
0275    Returns the JavaScript needed for a remote function.
0276    
0277    Takes the same options that can be passed as ``options`` to
0278    `link_to_remote <#link_to_remote>`_.
0279    
0280    Example::
0281    
0282        <select id="options" onchange="<% remote_function(update="options", 
0283                url=url(action='update_options')) %>">
0284            <option value="0">Hello</option>
0285            <option value="1">World</option>
0286        </select>    
0287    """
0288    javascript_options = options_for_ajax(options)
0289
0290    update = ''
0291    if options.get('update') and isinstance(options['update'], dict):
0292        update = []
0293        if options['update'].has_key('success'):
0294            update.append("success:'%s'" % options['update']['success'])
0295        if options['update'].has_key('failure'):
0296            update.append("failure:'%s'" % options['update']['failure'])
0297        update = '{' + ','.join(update) + '}'
0298    elif options.get('update'):
0299        update += "'%s'" % options['update']
0300
0301    function = "new Ajax.Request("
0302    if update: function = "new Ajax.Updater(%s, " % update
0303
0304    function += "'%s'" % get_url(options['url'])
0305    function += ", %s)" % javascript_options
0306
0307    if options.get('before'):
0308        function = "%s; %s" % (options['before'], function)
0309    if options.get('after'):
0310        function = "%s; %s" % (function, options['after'])
0311    if options.get('condition'):
0312        function = "if (%s) { %s; }" % (options['condition'], function)
0313    if options.get('confirm'):
0314        function = "if (confirm('%s')) { %s; }" % (escape_javascript(options['confirm']), function)
0315
0316    return function
0317
0318def observe_field(field_id, **options):
0319    """
0320    Observes the field with the DOM ID specified by ``field_id`` and makes
0321    an Ajax call when its contents have changed.
0322    
0323    Required keyword args are:
0324    
0325    ``url``
0326        ``url()``-style options for the action to call when the
0327        field has changed.
0328    
0329    Additional keyword args are:
0330    
0331    ``frequency``
0332        The frequency (in seconds) at which changes to this field will be
0333        detected. Not setting this option at all or to a value equal to or
0334        less than zero will use event based observation instead of time
0335        based observation.
0336    ``update``
0337        Specifies the DOM ID of the element whose innerHTML should be
0338        updated with the XMLHttpRequest response text.
0339    ``with_``
0340        A JavaScript expression specifying the parameters for the
0341        XMLHttpRequest. This defaults to 'value', which in the evaluated
0342        context refers to the new field value.
0343    
0344    Additionally, you may specify any of the options documented in
0345    `link_to_remote <#link_to_remote>`_.
0346    """
0347    if options.get('frequency') > 0:
0348        class_ = 'Form.Element.Observer'
0349    else:
0350        class_ = 'Form.Element.EventObserver'
0351    return build_observer(class_, field_id, **options)
0352
0353def observe_form(form_id, **options):
0354    """
0355    Like `observe_field <#observe_field>`_, but operates on an entire form
0356    identified by the DOM ID ``form_id``.
0357    
0358    Keyword args are the same as observe_field, except the default value of
0359    the ``with_`` keyword evaluates to the serialized (request string) value
0360    of the form.
0361    """
0362    if options.get('frequency'):
0363        class_ = 'Form.Observer'
0364    else:
0365        class_ = 'Form.EventObserver'
0366    return build_observer(class_, form_id, submit=form_id, **options)
0367
0368def options_for_ajax(options):
0369    js_options = build_callbacks(options)
0370
0371    js_options['asynchronous'] = str(options.get('type') != 'synchronous').lower()
0372    if options.get('method'):
0373        if isinstance(options['method'], str) and options['method'].startswith("'"):
0374            js_options['method'] = options['method']
0375        else:
0376            js_options['method'] = "'%s'" % options['method']
0377    if options.get('position'):
0378        js_options['insertion'] = "Insertion.%s" % camelize(options['position'])
0379    js_options['evalScripts'] = str(options.get('script') is None or options['script']).lower()
0380
0381    if options.get('form'):
0382        js_options['parameters'] = 'Form.serialize(this)'
0383    elif options.get('submit'):
0384        js_options['parameters'] = "Form.serialize('%s')" % options['submit']
0385    elif options.get('with_'):
0386        js_options['parameters'] = options['with_']
0387
0388    return options_for_javascript(js_options)
0389
0390def build_observer(cls, name, **options):
0391    if options.get('update') is True:
0392        options['with_'] = options.get('with', options.get('with_', 'value'))
0393    callback = remote_function(**options)
0394    javascript = "new %s('%s', " % (cls, name)
0395    if options.get('frequency'):
0396        javascript += "%s, " % options['frequency']
0397    javascript += "function(element, value) {%s}" % callback
0398    if options.get('on'):
0399        # FIXME: our prototype isn't supporting the on arg
0400        javascript +=", '%s'" % options['on']
0401    javascript += ")"
0402    return javascript_tag(javascript)
0403
0404def build_callbacks(options):
0405    callbacks = {}
0406    for callback, code in options.iteritems():
0407        if callback in CALLBACKS:
0408            name = 'on' + callback.title()
0409            callbacks[name] = "function(request){%s}" % code
0410    return callbacks
0411
0412__all__ = ['link_to_remote', 'periodically_call_remote', 'form_remote_tag', 'submit_to_remote', 'update_element_function',
0413           'evaluate_remote_response', 'remote_function', 'observe_field', 'observe_form']