/** * Yii JavaScript module. * * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ * @author Qiang Xue * @since 2.0 */ /** * yii is the root module for all Yii JavaScript modules. * It implements a mechanism of organizing JavaScript code in modules through the function "yii.initModule()". * * Each module should be named as "x.y.z", where "x" stands for the root module (for the Yii core code, this is "yii"). * * A module may be structured as follows: * * ```javascript * yii.sample = (function($) { * var pub = { * // whether this module is currently active. If false, init() will not be called for this module * // it will also not be called for all its child modules. If this property is undefined, it means true. * isActive: true, * init: function() { * // ... module initialization code go here ... * }, * * // ... other public functions and properties go here ... * }; * * // ... private functions and properties go here ... * * return pub; * })(jQuery); * ``` * * Using this structure, you can define public and private functions/properties for a module. * Private functions/properties are only visible within the module, while public functions/properties * may be accessed outside of the module. For example, you can access "yii.sample.isActive". * * You must call "yii.initModule()" once for the root module of all your modules. */ window.yii = (function ($) { var pub = { /** * List of JS or CSS URLs that can be loaded multiple times via AJAX requests. * Each item may be represented as either an absolute URL or a relative one. * Each item may contain a wildcart matching character `*`, that means one or more * any characters on the position. For example: * - `/css/*.js` will match any file ending with `.js` in the `css` directory of the current web site * - `http*://cdn.example.com/*` will match any files on domain `cdn.example.com`, loaded with HTTP or HTTPS * - `/js/myCustomScript.js?realm=*` will match file `/js/myCustomScript.js` with defined `realm` parameter */ reloadableScripts: [], /** * The selector for clickable elements that need to support confirmation and form submission. */ clickableSelector: 'a, button, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"]', /** * The selector for changeable elements that need to support confirmation and form submission. */ changeableSelector: 'select, input, textarea', /** * @return string|undefined the CSRF parameter name. Undefined is returned if CSRF validation is not enabled. */ getCsrfParam: function () { return $('meta[name=csrf-param]').attr('content'); }, /** * @return string|undefined the CSRF token. Undefined is returned if CSRF validation is not enabled. */ getCsrfToken: function () { return $('meta[name=csrf-token]').attr('content'); }, /** * Sets the CSRF token in the meta elements. * This method is provided so that you can update the CSRF token with the latest one you obtain from the server. * @param name the CSRF token name * @param value the CSRF token value */ setCsrfToken: function (name, value) { $('meta[name=csrf-param]').attr('content', name); $('meta[name=csrf-token]').attr('content', value); }, /** * Updates all form CSRF input fields with the latest CSRF token. * This method is provided to avoid cached forms containing outdated CSRF tokens. */ refreshCsrfToken: function () { var token = pub.getCsrfToken(); if (token) { $('form input[name="' + pub.getCsrfParam() + '"]').val(token); } }, /** * Displays a confirmation dialog. * The default implementation simply displays a js confirmation dialog. * You may override this by setting `yii.confirm`. * @param message the confirmation message. * @param ok a callback to be called when the user confirms the message * @param cancel a callback to be called when the user cancels the confirmation */ confirm: function (message, ok, cancel) { if (confirm(message)) { !ok || ok(); } else { !cancel || cancel(); } }, /** * Handles the action triggered by user. * This method recognizes the `data-method` attribute of the element. If the attribute exists, * the method will submit the form containing this element. If there is no containing form, a form * will be created and submitted using the method given by this attribute value (e.g. "post", "put"). * For hyperlinks, the form action will take the value of the "href" attribute of the link. * For other elements, either the containing form action or the current page URL will be used * as the form action URL. * * If the `data-method` attribute is not defined, the `href` attribute (if any) of the element * will be assigned to `window.location`. * * Starting from version 2.0.3, the `data-params` attribute is also recognized when you specify * `data-method`. The value of `data-params` should be a JSON representation of the data (name-value pairs) * that should be submitted as hidden inputs. For example, you may use the following code to generate * such a link: * * ```php * use yii\helpers\Html; * use yii\helpers\Json; * * echo Html::a('submit', ['site/foobar'], [ * 'data' => [ * 'method' => 'post', * 'params' => [ * 'name1' => 'value1', * 'name2' => 'value2', * ], * ], * ]; * ``` * * @param $e the jQuery representation of the element */ handleAction: function ($e, event) { var $form = $e.attr('data-form') ? $('#' + $e.attr('data-form')) : $e.closest('form'), method = !$e.data('method') && $form ? $form.attr('method') : $e.data('method'), action = $e.attr('href'), params = $e.data('params'), pjax = $e.data('pjax') || 0, pjaxPushState = !!$e.data('pjax-push-state'), pjaxReplaceState = !!$e.data('pjax-replace-state'), pjaxTimeout = $e.data('pjax-timeout'), pjaxScrollTo = $e.data('pjax-scrollto'), pjaxPushRedirect = $e.data('pjax-push-redirect'), pjaxReplaceRedirect = $e.data('pjax-replace-redirect'), pjaxSkipOuterContainers = $e.data('pjax-skip-outer-containers'), pjaxContainer, pjaxOptions = {}; if (pjax !== 0 && $.support.pjax) { if ($e.data('pjax-container')) { pjaxContainer = $e.data('pjax-container'); } else { pjaxContainer = $e.closest('[data-pjax-container=""]'); } // default to body if pjax container not found if (!pjaxContainer.length) { pjaxContainer = $('body'); } pjaxOptions = { container: pjaxContainer, push: pjaxPushState, replace: pjaxReplaceState, scrollTo: pjaxScrollTo, pushRedirect: pjaxPushRedirect, replaceRedirect: pjaxReplaceRedirect, pjaxSkipOuterContainers: pjaxSkipOuterContainers, timeout: pjaxTimeout, originalEvent: event, originalTarget: $e } } if (method === undefined) { if (action && action != '#') { if (pjax !== 0 && $.support.pjax) { $.pjax.click(event, pjaxOptions); } else { window.location = action; } } else if ($e.is(':submit') && $form.length) { if (pjax !== 0 && $.support.pjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) } $form.trigger('submit'); } return; } var newForm = !$form.length; if (newForm) { if (!action || !/(^\/|:\/\/)/.test(action)) { action = window.location.href; } $form = $('
', {method: method, action: action}); var target = $e.attr('target'); if (target) { $form.attr('target', target); } if (!/(get|post)/i.test(method)) { $form.append($('', {name: '_method', value: method, type: 'hidden'})); method = 'POST'; } if (!/(get|head|options)/i.test(method)) { var csrfParam = pub.getCsrfParam(); if (csrfParam) { $form.append($('', {name: csrfParam, value: pub.getCsrfToken(), type: 'hidden'})); } } $form.hide().appendTo('body'); } var activeFormData = $form.data('yiiActiveForm'); if (activeFormData) { // remember who triggers the form submission. This is used by yii.activeForm.js activeFormData.submitObject = $e; } // temporarily add hidden inputs according to data-params if (params && $.isPlainObject(params)) { $.each(params, function (idx, obj) { $form.append($('').attr({name: idx, value: obj, type: 'hidden'})); }); } var oldMethod = $form.attr('method'); $form.attr('method', method); var oldAction = null; if (action && action != '#') { oldAction = $form.attr('action'); $form.attr('action', action); } if (pjax !== 0 && $.support.pjax) { $form.on('submit',function(e){ $.pjax.submit(e, pjaxOptions); }) } $form.trigger('submit'); $.when($form.data('yiiSubmitFinalizePromise')).then( function () { if (oldAction != null) { $form.attr('action', oldAction); } $form.attr('method', oldMethod); // remove the temporarily added hidden inputs if (params && $.isPlainObject(params)) { $.each(params, function (idx, obj) { $('input[name="' + idx + '"]', $form).remove(); }); } if (newForm) { $form.remove(); } } ); }, getQueryParams: function (url) { var pos = url.indexOf('?'); if (pos < 0) { return {}; } var pairs = url.substring(pos + 1).split('#')[0].split('&'), params = {}, pair, i; for (i = 0; i < pairs.length; i++) { pair = pairs[i].split('='); var name = decodeURIComponent(pair[0].replace(/\+/g, '%20')); var value = decodeURIComponent(pair[1].replace(/\+/g, '%20')); if (name.length) { if (params[name] !== undefined) { if (!$.isArray(params[name])) { params[name] = [params[name]]; } params[name].push(value || ''); } else { params[name] = value || ''; } } } return params; }, initModule: function (module) { if (module.isActive === undefined || module.isActive) { if ($.isFunction(module.init)) { module.init(); } $.each(module, function () { if ($.isPlainObject(this)) { pub.initModule(this); } }); } }, init: function () { initCsrfHandler(); initRedirectHandler(); initScriptFilter(); initDataMethods(); } }; function initRedirectHandler() { // handle AJAX redirection $(document).ajaxComplete(function (event, xhr, settings) { var url = xhr && xhr.getResponseHeader('X-Redirect'); if (url) { window.location = url; } }); } function initCsrfHandler() { // automatically send CSRF token for all AJAX requests $.ajaxPrefilter(function (options, originalOptions, xhr) { if (!options.crossDomain && pub.getCsrfParam()) { xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); } }); pub.refreshCsrfToken(); } function initDataMethods() { var handler = function (event) { var $this = $(this), method = $this.data('method'), message = $this.data('confirm'), form = $this.data('form'); if (method === undefined && message === undefined && form === undefined) { return true; } if (message !== undefined) { $.proxy(pub.confirm, this)(message, function () { pub.handleAction($this, event); }); } else { pub.handleAction($this, event); } event.stopImmediatePropagation(); return false; }; // handle data-confirm and data-method for clickable and changeable elements $(document).on('click.yii', pub.clickableSelector, handler) .on('change.yii', pub.changeableSelector, handler); } function isReloadable(url) { var hostInfo = getHostInfo(); for (var i = 0; i < pub.reloadableScripts.length; i++) { var rule = pub.reloadableScripts[i]; rule = rule.charAt(0) === '/' ? hostInfo + rule : rule; var match = new RegExp("^" + escapeRegExp(rule).split('\\*').join('.*') + "$").test(url); if (match === true) { return true; } } return false; } // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } function getHostInfo() { return location.protocol + '//' + location.host; } function initScriptFilter() { var hostInfo = getHostInfo(); var loadedScripts = {}; var scripts = $('script[src]').map(function () { return this.src.charAt(0) === '/' ? hostInfo + this.src : this.src; }).toArray(); for (var i = 0, len = scripts.length; i < len; i++) { loadedScripts[scripts[i]] = true; } $.ajaxPrefilter('script', function (options, originalOptions, xhr) { if (options.dataType == 'jsonp') { return; } var url = options.url.charAt(0) === '/' ? hostInfo + options.url : options.url; if (url in loadedScripts) { var item = loadedScripts[url]; // If the concurrent XHR request is running and URL is not reloadable if (item !== true && !isReloadable(url)) { // Abort the current XHR request when previous finished successfully item.done(function () { if (xhr && xhr.readyState !== 4) { xhr.abort(); } }); // Or abort previous XHR if the current one is loaded faster xhr.done(function () { if (item && item.readyState !== 4) { item.abort(); } }); } else if (!isReloadable(url)) { xhr.abort(); } } else { loadedScripts[url] = xhr.done(function () { loadedScripts[url] = true; }).fail(function () { delete loadedScripts[url]; }); } }); $(document).ajaxComplete(function (event, xhr, settings) { var styleSheets = []; $('link[rel=stylesheet]').each(function () { if (isReloadable(this.href)) { return; } if ($.inArray(this.href, styleSheets) == -1) { styleSheets.push(this.href) } else { $(this).remove(); } }) }); } return pub; })(jQuery); jQuery(function () { yii.initModule(yii); });