Advertisement
Guest User

nette.ajax.js

a guest
Jun 6th, 2013
126
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2.  * AJAX Nette Framework plugin for jQuery
  3.  *
  4.  * @copyright Copyright (c) 2009, 2010 Jan Marek
  5.  * @copyright Copyright (c) 2009, 2010 David Grudl
  6.  * @copyright Copyright (c) 2012 Vojtěch Dobeš
  7.  * @license MIT
  8.  *
  9.  * @version 1.2.2
  10.  */
  11.  
  12. (function(window, $, undefined) {
  13.  
  14. if (typeof $ != 'function') {
  15.     return console.error('nette.ajax.js: jQuery is missing, load it please');
  16. }
  17.  
  18. var nette = function () {
  19.     var inner = {
  20.         self: this,
  21.         initialized: false,
  22.         contexts: {},
  23.         on: {
  24.             init: {},
  25.             load: {},
  26.             prepare: {},
  27.             before: {},
  28.             start: {},
  29.             success: {},
  30.             complete: {},
  31.             error: {}
  32.         },
  33.         fire: function () {
  34.             var result = true;
  35.             var args = Array.prototype.slice.call(arguments);
  36.             var props = args.shift();
  37.             var name = (typeof props == 'string') ? props : props.name;
  38.             var off = (typeof props == 'object') ? props.off || {} : {};
  39.             args.push(inner.self);
  40.             $.each(inner.on[name], function (index, reaction) {
  41.                 if (reaction === undefined || $.inArray(index, off) !== -1) return true;
  42.                 var temp = reaction.apply(inner.contexts[index], args);
  43.                 return result = (temp === undefined || temp);
  44.             });
  45.             return result;
  46.         },
  47.         requestHandler: function (e) {
  48.             if (!inner.self.ajax({}, this, e)) return;
  49.         },
  50.         ext: function (callbacks, context, name) {
  51.             while (!name) {
  52.                 name = 'ext_' + Math.random();
  53.                 if (inner.contexts[name]) {
  54.                     name = undefined;
  55.                 }
  56.             }
  57.  
  58.             $.each(callbacks, function (event, callback) {
  59.                 inner.on[event][name] = callback;
  60.             });
  61.             inner.contexts[name] = $.extend(context ? context : {}, {
  62.                 name: function () {
  63.                     return name;
  64.                 },
  65.                 ext: function (name, force) {
  66.                     var ext = inner.contexts[name];
  67.                     if (!ext && force) throw "Extension '" + this.name() + "' depends on disabled extension '" + name + "'.";
  68.                     return ext;
  69.                 }
  70.             });
  71.         }
  72.     };
  73.  
  74.     /**
  75.      * Allows manipulation with extensions.
  76.      * When called with 1. argument only, it returns extension with given name.
  77.      * When called with 2. argument equal to false, it removes extension entirely.
  78.      * When called with 2. argument equal to hash of event callbacks, it adds new extension.
  79.      *
  80.      * @param  {string} Name of extension
  81.      * @param  {bool|object|null} Set of callbacks for any events OR false for removing extension.
  82.      * @param  {object|null} Context for added extension
  83.      * @return {$.nette|object} Provides a fluent interface OR returns extensions with given name
  84.      */
  85.     this.ext = function (name, callbacks, context) {
  86.         if (typeof name == 'object') {
  87.             inner.ext(name, callbacks);
  88.         } else if (callbacks === undefined) {
  89.             return inner.contexts[name];
  90.         } else if (!callbacks) {
  91.             $.each(['init', 'load', 'prepare', 'before', 'start', 'success', 'complete', 'error'], function (index, event) {
  92.                 inner.on[event][name] = undefined;
  93.             });
  94.             inner.contexts[name] = undefined;
  95.         } else if (typeof name == 'string' && inner.contexts[name] !== undefined) {
  96.             throw "Cannot override already registered nette-ajax extension '" + name + "'.";
  97.         } else {
  98.             inner.ext(callbacks, context, name);
  99.         }
  100.         return this;
  101.     };
  102.  
  103.     /**
  104.      * Initializes the plugin:
  105.      * - fires 'init' event, then 'load' event
  106.      * - when called with any arguments, it will override default 'init' extension
  107.      *   with provided callbacks
  108.      *
  109.      * @param  {function|object|null} Callback for 'load' event or entire set of callbacks for any events
  110.      * @param  {object|null} Context provided for callbacks in first argument
  111.      * @return {$.nette} Provides a fluent interface
  112.      */
  113.     this.init = function (load, loadContext) {
  114.         if (inner.initialized) throw 'Cannot initialize nette-ajax twice.';
  115.  
  116.         if (typeof load == 'function') {
  117.             this.ext('init', null);
  118.             this.ext('init', {
  119.                 load: load
  120.             }, loadContext);
  121.         } else if (typeof load == 'object') {
  122.             this.ext('init', null);
  123.             this.ext('init', load, loadContext);
  124.         } else if (load !== undefined) {
  125.             throw 'Argument of init() can be function or function-hash only.';
  126.         }
  127.  
  128.         inner.initialized = true;
  129.  
  130.         inner.fire('init');
  131.         this.load();
  132.         return this;
  133.     };
  134.  
  135.     /**
  136.      * Fires 'load' event
  137.      *
  138.      * @return {$.nette} Provides a fluent interface
  139.      */
  140.     this.load = function () {
  141.         inner.fire('load', inner.requestHandler);
  142.         return this;
  143.     };
  144.  
  145.     /**
  146.      * Executes AJAX request. Attaches listeners and events.
  147.      *
  148.      * @param  {object} settings
  149.      * @param  {Element|null} ussually Anchor or Form
  150.      * @param  {event|null} event causing the request
  151.      * @return {jqXHR|null}
  152.      */
  153.     this.ajax = function (settings, ui, e) {
  154.         if (!settings.nette && ui && e) {
  155.             var $el = $(ui), xhr, originalBeforeSend;
  156.             var analyze = settings.nette = {
  157.                 e: e,
  158.                 ui: ui,
  159.                 el: $el,
  160.                 isForm: $el.is('form'),
  161.                 isSubmit: $el.is('input[type=submit]') || $el.is('button[type=submit]'),
  162.                 isImage: $el.is('input[type=image]'),
  163.                 form: null
  164.             };
  165.  
  166.             if (analyze.isSubmit || analyze.isImage) {
  167.                 analyze.form = analyze.el.closest('form');
  168.             } else if (analyze.isForm) {
  169.                 analyze.form = analyze.el;
  170.             }
  171.  
  172.             if (!settings.url) {
  173.                 settings.url = analyze.form ? analyze.form.attr('action') : ui.href;
  174.             }
  175.             if (!settings.type) {
  176.                 settings.type = analyze.form ? analyze.form.attr('method') : 'get';
  177.             }
  178.  
  179.             if ($el.is('[data-ajax-off]')) {
  180.                 settings.off = $el.data('ajaxOff');
  181.                 if (typeof settings.off == 'string') settings.off = [settings.off];
  182.             }
  183.         }
  184.  
  185.         inner.fire({
  186.             name: 'prepare',
  187.             off: settings.off || {}
  188.         }, settings);
  189.         if (settings.prepare) {
  190.             settings.prepare(settings);
  191.         }
  192.  
  193.         originalBeforeSend = settings.beforeSend;
  194.         settings.beforeSend = function (xhr, settings) {
  195.             var result = inner.fire({
  196.                 name: 'before',
  197.                 off: settings.off || {}
  198.             }, xhr, settings);
  199.             if ((result || result === undefined) && originalBeforeSend) {
  200.                 result = originalBeforeSend(xhr, settings);
  201.             }
  202.             return result;
  203.         };
  204.  
  205.         return this.handleXHR($.ajax(settings), settings);
  206.     };
  207.  
  208.     /**
  209.      * Binds extension callbacks to existing XHR object
  210.      *
  211.      * @param  {jqXHR|null}
  212.      * @param  {object} settings
  213.      * @return {jqXHR|null}
  214.      */
  215.     this.handleXHR = function (xhr, settings) {
  216.         settings = settings || {};
  217.  
  218.         if (xhr && (typeof xhr.statusText === 'undefined' || xhr.statusText !== 'canceled')) {
  219.             xhr.done(function (payload, status, xhr) {
  220.                 inner.fire({
  221.                     name: 'success',
  222.                     off: settings.off || {}
  223.                 }, payload, status, xhr);
  224.             }).fail(function (xhr, status, error) {
  225.                 inner.fire({
  226.                     name: 'error',
  227.                     off: settings.off || {}
  228.                 }, xhr, status, error);
  229.             }).always(function (xhr, status) {
  230.                 inner.fire({
  231.                     name: 'complete',
  232.                     off: settings.off || {}
  233.                 }, xhr, status);
  234.             });
  235.             inner.fire({
  236.                 name: 'start',
  237.                 off: settings.off || {}
  238.             }, xhr, settings);
  239.             if (settings.start) {
  240.                 settings.start(xhr, settings);
  241.             }
  242.         }
  243.         return xhr;
  244.     };
  245. };
  246.  
  247. $.nette = new ($.extend(nette, $.nette ? $.nette : {}));
  248.  
  249. $.fn.netteAjax = function (e, options) {
  250.     return $.nette.ajax(options || {}, this[0], e);
  251. };
  252.  
  253. $.fn.netteAjaxOff = function () {
  254.     return this.off('.nette');
  255. };
  256.  
  257. $.nette.ext('validation', {
  258.     before: function (xhr, settings) {
  259.         if (!settings.nette) return true;
  260.         else var analyze = settings.nette;
  261.         var e = analyze.e;
  262.  
  263.         var validate = $.extend({
  264.             keys: true,
  265.             url: true,
  266.             form: true
  267.         }, settings.validate || (function () {
  268.             if (!analyze.el.is('[data-ajax-validate]')) return;
  269.             var attr = analyze.el.data('ajaxValidate');
  270.             if (attr === false) return {
  271.                 keys: false,
  272.                 url: false,
  273.                 form: false
  274.             }; else if (typeof attr == 'object') return attr;
  275.         })() || {});
  276.  
  277.         var passEvent = false;
  278.         if (analyze.el.attr('data-ajax-pass') !== undefined) {
  279.             passEvent = analyze.el.data('ajaxPass');
  280.             passEvent = typeof passEvent == 'bool' ? passEvent : true;
  281.         }
  282.  
  283.         if (validate.keys) {
  284.             // thx to @vrana
  285.             var explicitNoAjax = e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;
  286.  
  287.             if (analyze.form) {
  288.                 if (explicitNoAjax && analyze.isSubmit) {
  289.                     this.explicitNoAjax = true;
  290.                     return false;
  291.                 } else if (analyze.isForm && this.explicitNoAjax) {
  292.                     this.explicitNoAjax = false;
  293.                     return false;
  294.                 }
  295.             } else if (explicitNoAjax) return false;
  296.         }
  297.  
  298.         if (validate.form && analyze.form && !((analyze.isSubmit || analyze.isImage) && analyze.el.attr('formnovalidate') !== undefined)) {
  299.             if (analyze.form.get(0).onsubmit && analyze.form.get(0).onsubmit(e) === false) {
  300.                 e.stopImmediatePropagation();
  301.                 e.preventDefault();
  302.                 return false;
  303.             }
  304.         }
  305.  
  306.         if (validate.url) {
  307.             // thx to @vrana
  308.             if (/:|^#/.test(analyze.form ? settings.url : analyze.el.attr('href'))) return false;
  309.         }
  310.  
  311.         if (!passEvent) {
  312.             e.stopPropagation();
  313.             e.preventDefault();
  314.         }
  315.         return true;
  316.     }
  317. }, {
  318.     explicitNoAjax: false
  319. });
  320.  
  321. $.nette.ext('forms', {
  322.     init: function () {
  323.         var snippets;
  324.         if (!window.Nette || !(snippets = this.ext('snippets'))) return;
  325.  
  326.         snippets.after(function ($el) {
  327.             $el.find('form').each(function() {
  328.                 window.Nette.initForm(this);
  329.             });
  330.         });
  331.     },
  332.     prepare: function (settings) {
  333.         var analyze = settings.nette;
  334.         if (!analyze || !analyze.form) return;
  335.         var e = analyze.e;
  336.         var originalData = settings.data || {};
  337.         var formData = {};
  338.  
  339.         if (analyze.isSubmit) {
  340.             formData[analyze.el.attr('name')] = analyze.el.val() || '';
  341.         } else if (analyze.isImage) {
  342.             var offset = analyze.el.offset();
  343.             var name = analyze.el.attr('name');
  344.             var dataOffset = [ Math.max(0, e.pageX - offset.left), Math.max(0, e.pageY - offset.top) ];
  345.  
  346.             if (name.indexOf('[', 0) !== -1) { // inside a container
  347.                 formData[name] = dataOffset;
  348.             } else {
  349.                 formData[name + '.x'] = dataOffset[0];
  350.                 formData[name + '.y'] = dataOffset[1];
  351.             }
  352.         }
  353.  
  354.         if (typeof originalData != 'string') {
  355.             originalData = $.param(originalData);
  356.         }
  357.         formData = $.param(formData);
  358.         settings.data = analyze.form.serialize() + (formData ? '&' + formData : '') + '&' + originalData;
  359.     }
  360. });
  361.  
  362. // default snippet handler
  363. $.nette.ext('snippets', {
  364.     success: function (payload) {
  365.         var snippets = [];
  366.         var elements = [];
  367.         if (payload.snippets) {
  368.             for (var i in payload.snippets) {
  369.                 var $el = this.getElement(i);
  370.                 elements.push($el.get(0));
  371.                 $.each(this.beforeQueue, function (index, callback) {
  372.                     if (typeof callback == 'function') {
  373.                         callback($el);
  374.                     }
  375.                 });
  376.                 this.updateSnippet($el, payload.snippets[i]);
  377.                 $.each(this.afterQueue, function (index, callback) {
  378.                     if (typeof callback == 'function') {
  379.                         callback($el);
  380.                     }
  381.                 });
  382.             }
  383.             var defer = $(elements).promise();
  384.             $.each(this.completeQueue, function (index, callback) {
  385.                 if (typeof callback == 'function') {
  386.                     defer.done(callback);
  387.                 }
  388.             });
  389.         }
  390.     }
  391. }, {
  392.     beforeQueue: [],
  393.     afterQueue: [],
  394.     completeQueue: [],
  395.     before: function (callback) {
  396.         this.beforeQueue.push(callback);
  397.     },
  398.     after: function (callback) {
  399.         this.afterQueue.push(callback);
  400.     },
  401.     complete: function (callback) {
  402.         this.completeQueue.push(callback);
  403.     },
  404.     updateSnippet: function ($el, html, back) {
  405.         if (typeof $el == 'string') {
  406.             $el = this.getElement($el);
  407.         }
  408.         // Fix for setting document title in IE
  409.         if ($el.is('title')) {
  410.             document.title = html;
  411.         } else {
  412.             this.applySnippet($el, html, back);
  413.         }
  414.     },
  415.     getElement: function (id) {
  416.         return $('#' + this.escapeSelector(id));
  417.     },
  418.     applySnippet: function ($el, html, back) {
  419.         if (!back && $el.is('[data-ajax-append]')) {
  420.             $el.append(html);
  421.         } else {
  422.             $el.html(html);
  423.         }
  424.     },
  425.     escapeSelector: function (selector) {
  426.         // thx to @uestla (https://github.com/uestla)
  427.         return selector.replace(/[\!"#\$%&'\(\)\*\+,\.\/:;<=>\?@\[\\\]\^`\{\|\}~]/g, '\\$&');
  428.     }
  429. });
  430.  
  431. // support $this->redirect()
  432. $.nette.ext('redirect', {
  433.     success: function (payload) {
  434.         if (payload.redirect) {
  435.             window.location.href = payload.redirect;
  436.             return false;
  437.         }
  438.     }
  439. });
  440.  
  441. // current page state
  442. $.nette.ext('state', {
  443.     success: function (payload) {
  444.         if (payload.state) {
  445.             this.state = payload.state;
  446.         }
  447.     }
  448. }, {state: null});
  449.  
  450. // abort last request if new started
  451. $.nette.ext('unique', {
  452.     start: function (xhr) {
  453.         if (this.xhr) {
  454.             this.xhr.abort();
  455.         }
  456.         this.xhr = xhr;
  457.     },
  458.     complete: function () {
  459.         this.xhr = null;
  460.     }
  461. }, {xhr: null});
  462.  
  463. // option to abort by ESC (thx to @vrana)
  464. $.nette.ext('abort', {
  465.     init: function () {
  466.         $('body').keydown($.proxy(function (e) {
  467.             if (this.xhr && (e.keyCode == 27 // Esc
  468.             && !(e.ctrlKey || e.shiftKey || e.altKey || e.metaKey))
  469.             ) {
  470.                 this.xhr.abort();
  471.             }
  472.         }, this));
  473.     },
  474.     start: function (xhr) {
  475.         this.xhr = xhr;
  476.     },
  477.     complete: function () {
  478.         this.xhr = null;
  479.     }
  480. }, {xhr: null});
  481.  
  482. $.nette.ext('load', {
  483.     success: function () {
  484.         $.nette.load();
  485.     }
  486. });
  487.  
  488. // default ajaxification (can be overridden in init())
  489. $.nette.ext('init', {
  490.     load: function (rh) {
  491.         $(this.linkSelector).off('click.nette', rh).on('click.nette', rh);
  492.         $(this.formSelector).off('submit.nette', rh).on('submit.nette', rh)
  493.             .off('click.nette', ':image', rh).on('click.nette', ':image', rh)
  494.             .off('click.nette', ':submit', rh).on('click.nette', ':submit', rh);
  495.         $(this.buttonSelector).closest('form')
  496.             .off('click.nette', this.buttonSelector, rh).on('click.nette', this.buttonSelector, rh);
  497.     }
  498. }, {
  499.     linkSelector: 'a.ajax',
  500.     formSelector: 'form.ajax',
  501.     buttonSelector: 'input.ajax[type="submit"], button.ajax[type="submit"], input.ajax[type="image"]'
  502. });
  503.  
  504. })(window, window.jQuery);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement