""" Prototype Helpers Provides a set of helpers for calling Prototype JavaScript functions, including functionality to call remote methods using `Ajax `_. This means that you can call actions in your controllers without reloading the page, but still update certain parts of it using injections into the DOM. The common use case is having a form that adds a new element to a list without reloading the page. To be able to use these helpers, you must include the Prototype JavaScript framework in your pages. See `link_to_remote `_ for documentation of options common to all Ajax helpers. See also `Scriptaculous `_ for helpers which work with the Scriptaculous controls and visual effects library. """ # Last synced with Rails copy at Revision 4235 on Aug 19th, 2006. import sys if sys.version < '2.4': from sets import ImmutableSet as frozenset from javascript import * from javascript import options_for_javascript from form_tag import form from tags import tag, camelize from urls import get_url CALLBACKS = frozenset(['uninitialized','loading','loaded', 'interactive','complete','failure','success'] + [str(x) for x in range(100,599)]) AJAX_OPTIONS = frozenset(['before','after','condition','url', 'asynchronous','method','insertion','position', 'form','with','update','script'] + list(CALLBACKS)) def link_to_remote(name, options={}, **html_options): """ Links to a remote function Returns a link to a remote action defined ``dict(url=url())`` (using the url() format) that's called in the background using XMLHttpRequest. The result of that request can then be inserted into a DOM object whose id can be specified with the ``update`` keyword. Usually, the result would be a partial prepared by the controller with either render_partial or render_partial_collection. Any keywords given after the second dict argument are considered html options and assigned as html attributes/values for the element. Example:: link_to_remote("Delete this post", dict(update="posts", url=url(action="destroy", id=post.id))) You can also specify a dict for ``update`` to allow for easy redirection of output to an other DOM element if a server-side error occurs: Example:: link_to_remote("Delete this post", dict(url=url(action="destroy", id=post.id), update=dict(success="posts", failure="error"))) Optionally, you can use the ``position`` parameter to influence how the target DOM element is updated. It must be one of 'before', 'top', 'bottom', or 'after'. By default, these remote requests are processed asynchronous during which various JavaScript callbacks can be triggered (for progress indicators and the likes). All callbacks get access to the ``request`` object, which holds the underlying XMLHttpRequest. To access the server response, use ``request.responseText``, to find out the HTTP status, use ``request.status``. Example:: link_to_remote(word, dict(url=url(action="undo", n=word_counter), complete="undoRequestCompleted(request)")) The callbacks that may be specified are (in order): ``loading`` Called when the remote document is being loaded with data by the browser. ``loaded`` Called when the browser has finished loading the remote document. ``interactive`` Called when the user can interact with the remote document, even though it has not finished loading. ``success`` Called when the XMLHttpRequest is completed, and the HTTP status code is in the 2XX range. ``failure`` Called when the XMLHttpRequest is completed, and the HTTP status code is not in the 2XX range. ``complete`` Called when the XMLHttpRequest is complete (fires after success/failure if they are present). You can further refine ``success`` and ``failure`` by adding additional callbacks for specific status codes. Example:: link_to_remote(word, dict(url=url(action="action"), 404="alert('Not found...? Wrong URL...?')", failure="alert('HTTP Error ' + request.status + '!')")) A status code callback overrides the success/failure handlers if present. If you for some reason or another need synchronous processing (that'll block the browser while the request is happening), you can specify ``type='synchronous'``. You can customize further browser side call logic by passing in JavaScript code snippets via some optional parameters. In their order of use these are: ``confirm`` Adds confirmation dialog. ``condition`` Perform remote request conditionally by this expression. Use this to describe browser-side conditions when request should not be initiated. ``before`` Called before request is initiated. ``after`` Called immediately after request was initiated and before ``loading``. ``submit`` Specifies the DOM element ID that's used as the parent of the form elements. By default this is the current form, but it could just as well be the ID of a table row or any other DOM element. """ return link_to_function(name, remote_function(**options), **html_options) def periodically_call_remote(**options): """ Periodically calls a remote function Periodically calls the specified ``url`` every ``frequency`` seconds (default is 10). Usually used to update a specified div ``update`` with the results of the remote call. The options for specifying the target with ``url`` and defining callbacks is the same as `link_to_remote <#link_to_remote>`_. """ frequency = options.get('frequency') or 10 code = "new PeriodicalExecuter(function() {%s}, %s)" % (remote_function(**options), frequency) return javascript_tag(code) def form_remote_tag(**options): """ Create a form tag using a remote function to submit the request Returns a form tag that will submit using XMLHttpRequest in the background instead of the regular reloading POST arrangement. Even though it's using JavaScript to serialize the form elements, the form submission will work just like a regular submission as viewed by the receiving side. The options for specifying the target with ``url`` and defining callbacks is the same as `link_to_remote <#link_to_remote>`_. A "fall-through" target for browsers that doesn't do JavaScript can be specified with the ``action/method`` options on ``html``. Example:: form_remote_tag(html=dict(action=url( controller="some", action="place"))) By default the fall-through action is the same as the one specified in the ``url`` (and the default method is ``post``). """ options['form'] = True if 'html' not in options: options['html'] = {} options['html']['onsubmit'] = "%s; return false;" % remote_function(**options) action = options['html'].get('action', get_url(options['url'])) options['html']['method'] = options['html'].get('method', 'post') return form(action, **options['html']) def submit_to_remote(name, value, **options): """ A submit button that submits via an XMLHttpRequest call Returns a button input tag that will submit form using XMLHttpRequest in the background instead of regular reloading POST arrangement. Keyword args are the same as in ``form_remote_tag``. """ options['with'] = options.get('form') or 'Form.serialize(this.form)' options['html'] = options.get('html') or {} options['html']['type'] = 'button' options['html']['onclick'] = "%s; return false;" % remote_function(**options) options['html']['name_'] = name options['html']['value'] = str(value) return tag("input", open=False, **options['html']) def update_element_function(element_id, **options): """ Returns a JavaScript function (or expression) that'll update a DOM element. ``content`` The content to use for updating. ``action`` Valid options are 'update' (assumed by default), 'empty', 'remove' ``position`` If the ``action`` is 'update', you can optionally specify one of the following positions: 'before', 'top', 'bottom', 'after'. Example:: <% javascript_tag(update_element_function("products", position='bottom', content="

New product!

")) %> This method can also be used in combination with remote method call where the result is evaluated afterwards to cause multiple updates on a page. Example:: # Calling view <% form_remote_tag(url=url(action="buy"), complete=evaluate_remote_response()) %> all the inputs here... # Controller action def buy(self, **params): c.product = Product.find(1) m.subexec('/buy.myt') # Returning view <% update_element_function( "cart", action='update', position='bottom', content="

New Product: %s

" % c.product.name) %> <% update_element_function("status", binding='binding', content="You've bought a new product!") %> """ content = escape_javascript(options.get('content', '')) opval = options.get('action', 'update') if opval == 'update': if options.get('position'): jsf = "new Insertion.%s('%s','%s')" % (camelize(options['position']), element_id, content) else: jsf = "$('%s').innerHTML = '%s'" % (element_id, content) elif opval == 'empty': jsf = "$('%s').innerHTML = ''" % element_id elif opval == 'remove': jsf = "Element.remove('%s')" % element_id else: raise "Invalid action, choose one of update, remove, or empty" jsf += ";\n" if options.get('binding'): return jsf + options['binding'] else: return jsf def evaluate_remote_response(): """ Returns a Javascript function that evals a request response Returns 'eval(request.responseText)' which is the JavaScript function that ``form_remote_tag`` can call in *complete* to evaluate a multiple update return document using ``update_element_function`` calls. """ return "eval(request.responseText)" def remote_function(**options): """ Returns the JavaScript needed for a remote function. Takes the same arguments as `link_to_remote <#link_to_remote>`_. Example:: """ javascript_options = options_for_ajax(options) update = '' if options.get('update') and isinstance(options['update'], dict): update = [] if options['update'].has_key('success'): update.append("success:'%s'" % options['update']['success']) if options['update'].has_key('failure'): update.append("failure:'%s'" % options['update']['failure']) update = '{' + ','.join(update) + '}' elif options.get('update'): update += "'%s'" % options['update'] function = "new Ajax.Request(" if update: function = "new Ajax.Updater(%s, " % update function += "'%s'" % get_url(options['url']) function += ", %s)" % javascript_options if options.get('before'): function = "%s; %s" % (options['before'], function) if options.get('after'): function = "%s; %s" % (function, options['after']) if options.get('condition'): function = "if (%s) { %s; }" % (options['condition'], function) if options.get('confirm'): function = "if (confirm('%s')) { %s; }" % (escape_javascript(options['confirm']), function) return function def observe_field(field_id, **options): """ Observes the field with the DOM ID specified by ``field_id`` and makes an Ajax call when its contents have changed. Required keyword args are: ``url`` ``url()``-style options for the action to call when the field has changed. Additional keyword args are: ``frequency`` The frequency (in seconds) at which changes to this field will be detected. Not setting this option at all or to a value equal to or less than zero will use event based observation instead of time based observation. ``update`` Specifies the DOM ID of the element whose innerHTML should be updated with the XMLHttpRequest response text. ``with`` A JavaScript expression specifying the parameters for the XMLHttpRequest. This defaults to 'value', which in the evaluated context refers to the new field value. ``on`` Specifies which event handler to observe. By default, it's set to "changed" for text fields and areas and "click" for radio buttons and checkboxes. With this, you can specify it instead to be "blur" or "focus" or any other event. Additionally, you may specify any of the options documented in `link_to_remote <#link_to_remote>`_. """ if options.get('frequency') > 0: return build_observer('Form.Element.Observer', field_id, **options) else: return build_observer('Form.Element.EventObserver', field_id, **options) def observe_form(form_id, **options): """ Like `observe_field <#observe_field>`_, but operates on an entire form identified by the DOM ID ``form_id``. Keyword args are the same as observe_field, except the default value of the ``with`` keyword evaluates to the serialized (request string) value of the form. """ if options.get('frequency'): return build_observer('Form.Observer', form_id, **options) else: return build_observer('Form.EventObserver', form_id, **options) def options_for_ajax(options): js_options = build_callbacks(options) js_options['asynchronous'] = str(options.get('type') != 'synchronous').lower() if options.get('method'): if isinstance(options['method'], str) and options['method'].startswith("'"): js_options['method'] = options['method'] else: js_options['method'] = "'%s'" % options['method'] if options.get('position'): js_options['insertion'] = "Insertion.%s" % camelize(options['position']) js_options['evalScripts'] = str(options.get('script') is None or options['script']).lower() if options.get('form'): js_options['parameters'] = 'Form.serialize(this)' elif options.get('submit'): js_options['parameters'] = "Form.serialize('%s')" % options['submit'] elif options.get('with'): js_options['parameters'] = options['with'] return options_for_javascript(js_options) def build_observer(cls, name, **options): if options.get('update') is True: options['with'] = options.get('with', 'value') callback = remote_function(**options) javascript = "new %s('%s', " % (cls, name) if options.get('frequency'): javascript += "%s, " % options['frequency'] javascript += "function(element, value) {%s})" % callback return javascript_tag(javascript) def build_callbacks(options): callbacks = {} for callback, code in options.iteritems(): if callback in CALLBACKS: name = 'on' + callback.title() callbacks[name] = "function(request){%s}" % code return callbacks __all__ = ['link_to_remote', 'periodically_call_remote', 'form_remote_tag', 'submit_to_remote', 'update_element_function', 'evaluate_remote_response', 'remote_function', 'observe_field', 'observe_form']