1 | """ |
---|
2 | Prototype Helpers |
---|
3 | |
---|
4 | Provides a set of helpers for calling Prototype JavaScript functions, |
---|
5 | including functionality to call remote methods using |
---|
6 | `Ajax <http://www.adaptivepath.com/publications/essays/archives/000385.php>`_. |
---|
7 | This means that you can call actions in your controllers without |
---|
8 | reloading the page, but still update certain parts of it using |
---|
9 | injections into the DOM. The common use case is having a form that adds |
---|
10 | a new element to a list without reloading the page. |
---|
11 | |
---|
12 | To be able to use these helpers, you must include the Prototype |
---|
13 | JavaScript framework in your pages. |
---|
14 | |
---|
15 | See `link_to_remote <module-railshelpers.helpers.javascript.html#link_to_function>`_ |
---|
16 | for documentation of options common to all Ajax helpers. |
---|
17 | |
---|
18 | See also `Scriptaculous <module-railshelpers.helpers.scriptaculous.html>`_ for |
---|
19 | helpers which work with the Scriptaculous controls and visual effects library. |
---|
20 | """ |
---|
21 | # Last synced with Rails copy at Revision 4235 on Aug 19th, 2006. |
---|
22 | |
---|
23 | import sys |
---|
24 | if sys.version < '2.4': |
---|
25 | from sets import ImmutableSet as frozenset |
---|
26 | |
---|
27 | from javascript import * |
---|
28 | from javascript import options_for_javascript |
---|
29 | from form_tag import form |
---|
30 | from tags import tag, camelize |
---|
31 | from urls import get_url |
---|
32 | |
---|
33 | CALLBACKS = frozenset(['uninitialized','loading','loaded', |
---|
34 | 'interactive','complete','failure','success'] + [str(x) for x in range(100,599)]) |
---|
35 | AJAX_OPTIONS = frozenset(['before','after','condition','url', |
---|
36 | 'asynchronous','method','insertion','position', |
---|
37 | 'form','with','update','script'] + list(CALLBACKS)) |
---|
38 | |
---|
39 | def link_to_remote(name, options={}, **html_options): |
---|
40 | """ |
---|
41 | Links to a remote function |
---|
42 | |
---|
43 | Returns a link to a remote action defined ``dict(url=url())`` |
---|
44 | (using the url() format) that's called in the background using |
---|
45 | XMLHttpRequest. The result of that request can then be inserted into a |
---|
46 | DOM object whose id can be specified with the ``update`` keyword. |
---|
47 | Usually, the result would be a partial prepared by the controller with |
---|
48 | either render_partial or render_partial_collection. |
---|
49 | |
---|
50 | Any keywords given after the second dict argument are considered html options |
---|
51 | and assigned as html attributes/values for the element. |
---|
52 | |
---|
53 | Example:: |
---|
54 | |
---|
55 | link_to_remote("Delete this post", dict(update="posts", |
---|
56 | url=url(action="destroy", id=post.id))) |
---|
57 | |
---|
58 | You can also specify a dict for ``update`` to allow for easy redirection |
---|
59 | of output to an other DOM element if a server-side error occurs: |
---|
60 | |
---|
61 | Example:: |
---|
62 | |
---|
63 | link_to_remote("Delete this post", |
---|
64 | dict(url=url(action="destroy", id=post.id), |
---|
65 | update=dict(success="posts", failure="error"))) |
---|
66 | |
---|
67 | Optionally, you can use the ``position`` parameter to influence how the |
---|
68 | target DOM element is updated. It must be one of 'before', 'top', 'bottom', |
---|
69 | or 'after'. |
---|
70 | |
---|
71 | By default, these remote requests are processed asynchronous during |
---|
72 | which various JavaScript callbacks can be triggered (for progress |
---|
73 | indicators and the likes). All callbacks get access to the |
---|
74 | ``request`` object, which holds the underlying XMLHttpRequest. |
---|
75 | |
---|
76 | To access the server response, use ``request.responseText``, to |
---|
77 | find out the HTTP status, use ``request.status``. |
---|
78 | |
---|
79 | Example:: |
---|
80 | |
---|
81 | link_to_remote(word, |
---|
82 | dict(url=url(action="undo", n=word_counter), |
---|
83 | complete="undoRequestCompleted(request)")) |
---|
84 | |
---|
85 | The callbacks that may be specified are (in order): |
---|
86 | |
---|
87 | ``loading`` |
---|
88 | Called when the remote document is being loaded with data by the browser. |
---|
89 | ``loaded`` |
---|
90 | Called when the browser has finished loading the remote document. |
---|
91 | ``interactive`` |
---|
92 | Called when the user can interact with the remote document, even |
---|
93 | though it has not finished loading. |
---|
94 | ``success`` |
---|
95 | Called when the XMLHttpRequest is completed, and the HTTP status |
---|
96 | code is in the 2XX range. |
---|
97 | ``failure`` |
---|
98 | Called when the XMLHttpRequest is completed, and the HTTP status code is |
---|
99 | not in the 2XX range. |
---|
100 | ``complete`` |
---|
101 | Called when the XMLHttpRequest is complete (fires after success/failure |
---|
102 | if they are present). |
---|
103 | |
---|
104 | You can further refine ``success`` and ``failure`` by |
---|
105 | adding additional callbacks for specific status codes. |
---|
106 | |
---|
107 | Example:: |
---|
108 | |
---|
109 | link_to_remote(word, |
---|
110 | dict(url=url(action="action"), |
---|
111 | 404="alert('Not found...? Wrong URL...?')", |
---|
112 | failure="alert('HTTP Error ' + request.status + '!')")) |
---|
113 | |
---|
114 | A status code callback overrides the success/failure handlers if |
---|
115 | present. |
---|
116 | |
---|
117 | If you for some reason or another need synchronous processing (that'll |
---|
118 | block the browser while the request is happening), you can specify |
---|
119 | ``type='synchronous'``. |
---|
120 | |
---|
121 | You can customize further browser side call logic by passing in |
---|
122 | JavaScript code snippets via some optional parameters. In their order |
---|
123 | of use these are: |
---|
124 | |
---|
125 | ``confirm`` |
---|
126 | Adds confirmation dialog. |
---|
127 | ``condition`` |
---|
128 | Perform remote request conditionally by this expression. Use this to |
---|
129 | describe browser-side conditions when request should not be initiated. |
---|
130 | ``before`` |
---|
131 | Called before request is initiated. |
---|
132 | ``after`` |
---|
133 | Called immediately after request was initiated and before ``loading``. |
---|
134 | ``submit`` |
---|
135 | Specifies the DOM element ID that's used as the parent of the form |
---|
136 | elements. By default this is the current form, but it could just as |
---|
137 | well be the ID of a table row or any other DOM element. |
---|
138 | """ |
---|
139 | return link_to_function(name, remote_function(**options), **html_options) |
---|
140 | |
---|
141 | def periodically_call_remote(**options): |
---|
142 | """ |
---|
143 | Periodically calls a remote function |
---|
144 | |
---|
145 | Periodically calls the specified ``url`` every ``frequency`` seconds |
---|
146 | (default is 10). Usually used to update a specified div ``update`` |
---|
147 | with the results of the remote call. The options for specifying the |
---|
148 | target with ``url`` and defining callbacks is the same as `link_to_remote <#link_to_remote>`_. |
---|
149 | """ |
---|
150 | frequency = options.get('frequency') or 10 |
---|
151 | code = "new PeriodicalExecuter(function() {%s}, %s)" % (remote_function(**options), frequency) |
---|
152 | return javascript_tag(code) |
---|
153 | |
---|
154 | def form_remote_tag(**options): |
---|
155 | """ |
---|
156 | Create a form tag using a remote function to submit the request |
---|
157 | |
---|
158 | Returns a form tag that will submit using XMLHttpRequest in the |
---|
159 | background instead of the regular reloading POST arrangement. Even |
---|
160 | though it's using JavaScript to serialize the form elements, the form |
---|
161 | submission will work just like a regular submission as viewed by the |
---|
162 | receiving side. The options for specifying the target with ``url`` |
---|
163 | and defining callbacks is the same as `link_to_remote <#link_to_remote>`_. |
---|
164 | |
---|
165 | A "fall-through" target for browsers that doesn't do JavaScript can be |
---|
166 | specified with the ``action/method`` options on ``html``. |
---|
167 | |
---|
168 | Example:: |
---|
169 | |
---|
170 | form_remote_tag(html=dict(action=url( |
---|
171 | controller="some", action="place"))) |
---|
172 | |
---|
173 | By default the fall-through action is the same as the one specified in |
---|
174 | the ``url`` (and the default method is ``post``). |
---|
175 | """ |
---|
176 | options['form'] = True |
---|
177 | if 'html' not in options: options['html'] = {} |
---|
178 | options['html']['onsubmit'] = "%s; return false;" % remote_function(**options) |
---|
179 | action = options['html'].get('action', get_url(options['url'])) |
---|
180 | options['html']['method'] = options['html'].get('method', 'post') |
---|
181 | |
---|
182 | return form(action, **options['html']) |
---|
183 | |
---|
184 | def submit_to_remote(name, value, **options): |
---|
185 | """ |
---|
186 | A submit button that submits via an XMLHttpRequest call |
---|
187 | |
---|
188 | Returns a button input tag that will submit form using XMLHttpRequest |
---|
189 | in the background instead of regular reloading POST arrangement. |
---|
190 | Keyword args are the same as in ``form_remote_tag``. |
---|
191 | """ |
---|
192 | options['with'] = options.get('form') or 'Form.serialize(this.form)' |
---|
193 | |
---|
194 | options['html'] = options.get('html') or {} |
---|
195 | options['html']['type'] = 'button' |
---|
196 | options['html']['onclick'] = "%s; return false;" % remote_function(**options) |
---|
197 | options['html']['name_'] = name |
---|
198 | options['html']['value'] = str(value) |
---|
199 | |
---|
200 | return tag("input", open=False, **options['html']) |
---|
201 | |
---|
202 | def update_element_function(element_id, **options): |
---|
203 | """ |
---|
204 | Returns a JavaScript function (or expression) that'll update a DOM |
---|
205 | element. |
---|
206 | |
---|
207 | ``content`` |
---|
208 | The content to use for updating. |
---|
209 | ``action`` |
---|
210 | Valid options are 'update' (assumed by default), 'empty', 'remove' |
---|
211 | ``position`` |
---|
212 | If the ``action`` is 'update', you can optionally specify one of the |
---|
213 | following positions: 'before', 'top', 'bottom', 'after'. |
---|
214 | |
---|
215 | Example:: |
---|
216 | |
---|
217 | <% javascript_tag(update_element_function("products", |
---|
218 | position='bottom', content="<p>New product!</p>")) %> |
---|
219 | |
---|
220 | This method can also be used in combination with remote method call |
---|
221 | where the result is evaluated afterwards to cause multiple updates on |
---|
222 | a page. Example:: |
---|
223 | |
---|
224 | # Calling view |
---|
225 | <% form_remote_tag(url=url(action="buy"), |
---|
226 | complete=evaluate_remote_response()) %> |
---|
227 | all the inputs here... |
---|
228 | |
---|
229 | # Controller action |
---|
230 | def buy(self, **params): |
---|
231 | c.product = Product.find(1) |
---|
232 | m.subexec('/buy.myt') |
---|
233 | |
---|
234 | # Returning view |
---|
235 | <% update_element_function( |
---|
236 | "cart", action='update', position='bottom', |
---|
237 | content="<p>New Product: %s</p>" % c.product.name) %> |
---|
238 | <% update_element_function("status", binding='binding', |
---|
239 | content="You've bought a new product!") %> |
---|
240 | """ |
---|
241 | content = escape_javascript(options.get('content', '')) |
---|
242 | opval = options.get('action', 'update') |
---|
243 | if opval == 'update': |
---|
244 | if options.get('position'): |
---|
245 | jsf = "new Insertion.%s('%s','%s')" % (camelize(options['position']), element_id, content) |
---|
246 | else: |
---|
247 | jsf = "$('%s').innerHTML = '%s'" % (element_id, content) |
---|
248 | elif opval == 'empty': |
---|
249 | jsf = "$('%s').innerHTML = ''" % element_id |
---|
250 | elif opval == 'remove': |
---|
251 | jsf = "Element.remove('%s')" % element_id |
---|
252 | else: |
---|
253 | raise "Invalid action, choose one of update, remove, or empty" |
---|
254 | |
---|
255 | jsf += ";\n" |
---|
256 | if options.get('binding'): |
---|
257 | return jsf + options['binding'] |
---|
258 | else: |
---|
259 | return jsf |
---|
260 | |
---|
261 | def evaluate_remote_response(): |
---|
262 | """ |
---|
263 | Returns a Javascript function that evals a request response |
---|
264 | |
---|
265 | Returns 'eval(request.responseText)' which is the JavaScript function |
---|
266 | that ``form_remote_tag`` can call in *complete* to evaluate a multiple |
---|
267 | update return document using ``update_element_function`` calls. |
---|
268 | """ |
---|
269 | return "eval(request.responseText)" |
---|
270 | |
---|
271 | def remote_function(**options): |
---|
272 | """ |
---|
273 | Returns the JavaScript needed for a remote function. |
---|
274 | |
---|
275 | Takes the same arguments as `link_to_remote <#link_to_remote>`_. |
---|
276 | |
---|
277 | Example:: |
---|
278 | |
---|
279 | <select id="options" onchange="<% remote_function(update="options", |
---|
280 | url=url(action='update_options')) %>"> |
---|
281 | <option value="0">Hello</option> |
---|
282 | <option value="1">World</option> |
---|
283 | </select> |
---|
284 | """ |
---|
285 | javascript_options = options_for_ajax(options) |
---|
286 | |
---|
287 | update = '' |
---|
288 | if options.get('update') and isinstance(options['update'], dict): |
---|
289 | update = [] |
---|
290 | if options['update'].has_key('success'): |
---|
291 | update.append("success:'%s'" % options['update']['success']) |
---|
292 | if options['update'].has_key('failure'): |
---|
293 | update.append("failure:'%s'" % options['update']['failure']) |
---|
294 | update = '{' + ','.join(update) + '}' |
---|
295 | elif options.get('update'): |
---|
296 | update += "'%s'" % options['update'] |
---|
297 | |
---|
298 | function = "new Ajax.Request(" |
---|
299 | if update: function = "new Ajax.Updater(%s, " % update |
---|
300 | |
---|
301 | function += "'%s'" % get_url(options['url']) |
---|
302 | function += ", %s)" % javascript_options |
---|
303 | |
---|
304 | if options.get('before'): |
---|
305 | function = "%s; %s" % (options['before'], function) |
---|
306 | if options.get('after'): |
---|
307 | function = "%s; %s" % (function, options['after']) |
---|
308 | if options.get('condition'): |
---|
309 | function = "if (%s) { %s; }" % (options['condition'], function) |
---|
310 | if options.get('confirm'): |
---|
311 | function = "if (confirm('%s')) { %s; }" % (escape_javascript(options['confirm']), function) |
---|
312 | |
---|
313 | return function |
---|
314 | |
---|
315 | def observe_field(field_id, **options): |
---|
316 | """ |
---|
317 | Observes the field with the DOM ID specified by ``field_id`` and makes |
---|
318 | an Ajax call when its contents have changed. |
---|
319 | |
---|
320 | Required keyword args are: |
---|
321 | |
---|
322 | ``url`` |
---|
323 | ``url()``-style options for the action to call when the |
---|
324 | field has changed. |
---|
325 | |
---|
326 | Additional keyword args are: |
---|
327 | |
---|
328 | ``frequency`` |
---|
329 | The frequency (in seconds) at which changes to this field will be |
---|
330 | detected. Not setting this option at all or to a value equal to or |
---|
331 | less than zero will use event based observation instead of time |
---|
332 | based observation. |
---|
333 | ``update`` |
---|
334 | Specifies the DOM ID of the element whose innerHTML should be |
---|
335 | updated with the XMLHttpRequest response text. |
---|
336 | ``with`` |
---|
337 | A JavaScript expression specifying the parameters for the |
---|
338 | XMLHttpRequest. This defaults to 'value', which in the evaluated |
---|
339 | context refers to the new field value. |
---|
340 | ``on`` |
---|
341 | Specifies which event handler to observe. By default, it's set to |
---|
342 | "changed" for text fields and areas and "click" for radio buttons |
---|
343 | and checkboxes. With this, you can specify it instead to be "blur" |
---|
344 | or "focus" or any other event. |
---|
345 | |
---|
346 | Additionally, you may specify any of the options documented in |
---|
347 | `link_to_remote <#link_to_remote>`_. |
---|
348 | """ |
---|
349 | if options.get('frequency') > 0: |
---|
350 | return build_observer('Form.Element.Observer', field_id, **options) |
---|
351 | else: |
---|
352 | return build_observer('Form.Element.EventObserver', field_id, **options) |
---|
353 | |
---|
354 | def observe_form(form_id, **options): |
---|
355 | """ |
---|
356 | Like `observe_field <#observe_field>`_, but operates on an entire form |
---|
357 | identified by the DOM ID ``form_id``. |
---|
358 | |
---|
359 | Keyword args are the same as observe_field, except the default value of |
---|
360 | the ``with`` keyword evaluates to the serialized (request string) value |
---|
361 | of the form. |
---|
362 | """ |
---|
363 | if options.get('frequency'): |
---|
364 | return build_observer('Form.Observer', form_id, **options) |
---|
365 | else: |
---|
366 | return build_observer('Form.EventObserver', form_id, **options) |
---|
367 | |
---|
368 | def options_for_ajax(options): |
---|
369 | js_options = build_callbacks(options) |
---|
370 | |
---|
371 | js_options['asynchronous'] = str(options.get('type') != 'synchronous').lower() |
---|
372 | if options.get('method'): |
---|
373 | if isinstance(options['method'], str) and options['method'].startswith("'"): |
---|
374 | js_options['method'] = options['method'] |
---|
375 | else: |
---|
376 | js_options['method'] = "'%s'" % options['method'] |
---|
377 | if options.get('position'): |
---|
378 | js_options['insertion'] = "Insertion.%s" % camelize(options['position']) |
---|
379 | js_options['evalScripts'] = str(options.get('script') is None or options['script']).lower() |
---|
380 | |
---|
381 | if options.get('form'): |
---|
382 | js_options['parameters'] = 'Form.serialize(this)' |
---|
383 | elif options.get('submit'): |
---|
384 | js_options['parameters'] = "Form.serialize('%s')" % options['submit'] |
---|
385 | elif options.get('with'): |
---|
386 | js_options['parameters'] = options['with'] |
---|
387 | |
---|
388 | return options_for_javascript(js_options) |
---|
389 | |
---|
390 | def build_observer(cls, name, **options): |
---|
391 | if options.get('update') is True: |
---|
392 | options['with'] = options.get('with', 'value') |
---|
393 | callback = remote_function(**options) |
---|
394 | javascript = "new %s('%s', " % (cls, name) |
---|
395 | if options.get('frequency'): |
---|
396 | javascript += "%s, " % options['frequency'] |
---|
397 | javascript += "function(element, value) {%s})" % callback |
---|
398 | return javascript_tag(javascript) |
---|
399 | |
---|
400 | def build_callbacks(options): |
---|
401 | callbacks = {} |
---|
402 | for callback, code in options.iteritems(): |
---|
403 | if callback in CALLBACKS: |
---|
404 | name = 'on' + callback.title() |
---|
405 | callbacks[name] = "function(request){%s}" % code |
---|
406 | return callbacks |
---|
407 | |
---|
408 | __all__ = ['link_to_remote', 'periodically_call_remote', 'form_remote_tag', 'submit_to_remote', 'update_element_function', |
---|
409 | 'evaluate_remote_response', 'remote_function', 'observe_field', 'observe_form'] |
---|