/* hobo-jQuery initialization & utility functions */ (function($) { var page_data = {}; //used for javascript testing. var num_updates = 0; var methods = { /* call only once per document. */ initOnce: function() { if(typeof History === 'object') { // History.js installed $(window).on("statechange", function() { var state = History.getState(); if(state.data.length==3) { var form = $(state.data[0]); var roptions = form.hjq('buildRequestCallbacks', state.data[1], state.data[2]); $.ajax(state.url, roptions); } }) } return true; }, /* call for every new fragment */ init: function() { var top = this; this.find("[data-rapid-page-data]").each(function() { page_data = $(this).data('rapid-page-data'); }); this.find("[data-rapid]").each(function() { var that = jQuery(this); jQuery.each(jQuery(this).data('rapid'), function(tag, annotations) { tag = "hjq_"+tag.replace(/-/g, '_'); if(that[tag]) { that[tag](annotations); } }); }); return top; }, /* return the ID from the typed-id in the data-rapid-context attribute */ contextId: function() { return this.data('rapid-context').split(":")[1]; }, /* given annotations, turns the values in the _events_ object into functions, merges them into _options_ and returns _options_ */ getOptions: function(annotations) { for(var key in annotations.events) { if(annotations.events.hasOwnProperty(key)) { annotations.options[key] = methods.createFunction.call(this, annotations.events[key]); } } return annotations.options; }, /* return the global page_data: hobo_parts, form_auth_token, etc. */ pageData: function() { return page_data; }, /* return the number of current updates. Useful for javascript/integration testing */ numUpdates: function() { return num_updates; }, /* this function is called on an update of a part via Ajax. */ update: function(innerHtml) { num_updates += 1; var that=this; var replacement=this.clone().html(innerHtml).hide(); var hide_o, show_o; if(this.data('hjq-ajax')) { hide_o = this.data('hjq-ajax')['hide']; show_o = this.data('hjq-ajax')['show']; } methods.hideAndRemove.call(this, hide_o, function () { that.before(replacement); methods.show.call(replacement, show_o, function() {num_updates -= 1;}); }); methods.init.call(replacement); return replacement; }, /* this function is called on ajax update to update part context information */ updatePartContexts: function(contexts) { $.extend(page_data.hobo_parts, contexts); }, /* hide and removes the element. options is an array or * comma-separated string corresponding to the jQuery-UI hide * arguments: effect, options, speed & callback. The callback * argument is an additional callback called after the one in * the options hash. Removal is done after both callbacks. */ hideAndRemove: function(o, callback) { methods.hide.call(this, o, callback, true); }, /* hides the element. options is an array or * comma-separated string corresponding to the jQuery-UI hide * arguments: effect, options, speed & callback. The callback * argument is an additional callback called after the one in * the options hash. Removal is done after both callbacks. */ hide: function(o, callback, andRemove) { var that=this; var args = o; if(args===undefined) args=page_data.hide; if(args===undefined) args=[]; if (typeof args=='string') args=args.split(','); else if($.isArray(args)) args=args.slice(0); //shallow clone else args=[]; o_cb = args[3]; args[3] = function() { if(o_cb) methods.createFunction.call(that, o_cb).apply(this, arguments); if(callback) callback.apply(this, arguments); if(andRemove) that.remove(); }; if(args[0]) { that.hide.apply(that, args); } else { that.hide(); args[3].call(that); } return this; }, /* show the element. options is an array or * comma-separated string corresponding to the jQuery-UI show * arguments: effect, options, speed & callback. The callback * argument is an additional callback called after the one in * the options hash. */ show: function(o, callback) { var that=this; var args = o; if(args===undefined) args=page_data.show; if(args===undefined) args=[]; if (typeof args=='string') args=args.split(','); else if($.isArray(args)) args=args.slice(0); //shallow clone else args=[]; o_cb = args[3]; args[3] = function() { if(o_cb) methods.createFunction.call(that, o_cb).apply(this, arguments); if(callback) callback.apply(this, arguments); }; if(args[0]) { that.show.apply(that, args); } else { that.show(); args[3].call(that); } return this; }, /* given a global function name, find the function */ functionByName: function(name) { var descend = window; // find function by name on the root object jQuery.each(name.split("."), function() { if(descend) descend = descend[this]; }); return descend; }, /* Given a function name or javascript fragment, return a function */ createFunction: function(script) { if(!script) return function() {}; if($.isFunction(script)) return script; var f=methods.functionByName.call(this, script); if(f) return f; return function() { return eval(script); }; }, /* returns a jQuery selector for an element. One option would be to use something like http://stackoverflow.com/questions/2206958/best-way-to-reference-an-element-with-jquery However, if the DOM changes due to Ajax this isn't necessarily stable. So instead we give the element a unique ID if it doesn't already have one. */ getPath: function() { if(!this.attr("id")) { this.attr("id", Math.random().toString().replace("0.", "id")) } return "#"+this.attr("id"); }, /* Build an options object suitable for sending to * jQuery.ajax. (Note that the before & confirm callbacks * are called from this function, and the spinner is shown) * * The returned object will include a 'data' value * populated with a hash. * * This function has now been split into two parts to * better support push_state. buildRequestData is the * first part, which builds everything except the * callbacks (but it does execute the before callbacks). * buildRequestCallbacks builds the remaining callbacks. * * Options: * type: POST, GET * attrs: a hash containing the standard Hobo ajax attributes & callbacks * extra_options: merged into the hash sent to jQuery.ajax * extra_callbacks: the callbacks in attrs are generally specified by the DRYML; this allows the framework to add their own * function: passed to Hobo's ajax_update_response * preamble: passed to Hobo's ajax_update_response * postamble: passed to Hobo's ajax_update_response * content_type: passed to Hobo's ajax_update_response * */ buildRequest: function(o) { return methods.buildRequestCallbacks.call(this, methods.buildRequestData.call(this, o), o); }, buildRequestData: function(o) { var that = this; if (!o.attrs) o.attrs = {}; var result = {}; if(o.attrs.before) { if(!methods.createFunction.call(that, o.attrs.before).call(this)) { return false; } } var before_evt=jQuery.Event("rapid:ajax:before"); that.trigger(before_evt, [that]); if(before_evt.isPropagationStopped()) { return false; } if(o.attrs.confirm) { if(!confirm(o.attrs.confirm)) { return false; } } result.context = this; result.type = o.type || 'GET'; result.data = {"render_options[preamble]": o.preamble || '', "render_options[contexts_function]": 'hjq.ajax.updatePartContexts' }; if(o.postamble) result.data["render_options[postamble]"] = o.postamble; if(o.content_type) result.data["render_options[content_type]"] = o.content_type; if(o.attrs.errors_ok) result.data["render_options[errors_ok]"] = 1; result.dataType = 'script'; o.spec = jQuery.extend({'function': 'hjq.ajax.update', preamble: ''}, o.spec); var part_data = {}; if(o.attrs.hide!==undefined) part_data.hide = o.attrs.hide; if(o.attrs.show!==undefined) part_data.show = o.attrs.show; if($.isEmptyObject(part_data)) part_data = undefined; // we tell our controller which parts to return by sending it a "render" array. var ids=methods.getUpdateIds.call(this, o.attrs); for(var i=0; i<ids.length; i++) { if(part_data) $("#"+ids[i]).data('hjq-ajax', part_data); result.data["render["+i+"][part_context]"] = page_data.hobo_parts[ids[i]]; result.data["render["+i+"][id]"] = ids[i]; result.data["render["+i+"][function]"] = o['function'] || 'hjq.ajax.update'; } return result; }, buildRequestCallbacks: function(result, o) { var that = this; if (!o.attrs) o.attrs = {}; if (!o.extra_callbacks) o.extra_callbacks = {}; this.hjq_spinner(o.attrs, "Saving..."); var success_dfd = jQuery.Deferred(); if(o.attrs.success) success_dfd.done(methods.createFunction.call(that, o.attrs.success)); if(o.extra_callbacks.success) success_dfd.done(methods.createFunction.call(that, o.extra_callbacks.success)); success_dfd.done(function() { if(o.attrs.reset_form) that[0].reset(); // if we've been removed, all event handlers on us // have already been removed and we don't bubble // up, so triggering on that won't do any good if(that.parents("body").length==0) $(document).trigger('rapid:ajax:success', [that]); else that.trigger('rapid:ajax:success', [that]); }); result.success = success_dfd.resolve; var error_dfd = jQuery.Deferred(); if(o.attrs.error) error_dfd.done(methods.createFunction.call(that, o.attrs.error)); if(o.extra_callbacks.error) error_dfd.done(methods.createFunction.call(that, o.extra_callbacks.error)); error_dfd.done(function() { if(that.parents("body").length==0) $(document).trigger('rapid:ajax:error', [that]); else that.trigger('rapid:ajax:error', [that]); }); result.error = error_dfd.resolve; var complete_dfd = jQuery.Deferred(); if(o.attrs.complete) complete_dfd.done(methods.createFunction.call(that, o.attrs.complete)); if(o.extra_callbacks.complete) complete_dfd.done(methods.createFunction.call(that, o.extra_callbacks.complete)); complete_dfd.done(function() { if(that.parents("body").length==0) $(document).trigger('rapid:ajax:complete', [that]); else that.trigger('rapid:ajax:complete', [that]); that.hjq_spinner('remove'); if(o.attrs.refocus_form) that.find(":input[type!=hidden]:first").focus(); }); result.complete = complete_dfd.resolve; jQuery.extend(result, o.extra_options); return result; }, /* this: element to receive callbacks url: new location ajax_options: output from buildRequestData hobo_options: input to buildRequestData, buildRequestCallbacks */ changeLocationAjax: function (url, ajax_options, hobo_options) { if (hobo_options.attrs.push_state && typeof History==='object') { // if the history plugin is installed, it will fire the // changestate event immediately, which is where we // actually execute the ajax window.History.pushState([this.getPath(), ajax_options, hobo_options], hobo_options.attrs.new_title || null, url); } else { ajax_options = this.hjq('buildRequestCallbacks', ajax_options, hobo_options); $.ajax(url, ajax_options); } }, // given ajax_attrs (update, updates and/or ajax), return DOM id's. getUpdateIds: function(attrs) { var ids = attrs.update || []; if (typeof ids=='string') ids=ids.split(','); jQuery(attrs.updates).each(function () { ids.push(jQuery(this).attr('id')); }); if(attrs.ajax) { for(var el=this; el.length && !page_data.hobo_parts[el.attr("id")]; el=el.parent()); if(el.length) ids.push(el.attr('id')); } // validate for (var i=0; i<ids.length; i++) { if(!page_data.hobo_parts[ids[i]]) { ids.splice(i, 1); i -= 1; } } return ids; } }; $.fn.hjq = function( method ) { if ( methods[method] ) { return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); } else if ( typeof method === 'object' || ! method ) { return methods.initOnce.apply( this, arguments ) && methods.init.apply( this, arguments ); } else { $.error( 'Method ' + method + ' does not exist on hjq' ); } }; })( jQuery ); // to make the Ajax interface cleaner, these provide direct access to // a couple of plugin functions. var hjq=(function($) { return { ajax: { update: function (dom_id, innerHtml) { $("#"+dom_id).hjq('update',innerHtml); }, updatePartContexts: function(contexts) { $(document).hjq('updatePartContexts', contexts); } } }; })(jQuery);