/** * 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 * window.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 goes here ... * }, * * // ... other public functions and properties go here ... * }; * * // ... private functions and properties go here ... * * return pub; * })(window.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 wildcard matching character `*`, that means one or more * any characters on the position. For example: * - `/css/*.css` will match any file ending with `.css` 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 (window.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 * @param event Related event */ 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'), isValidAction = action && action !== '#', params = $e.data('params'), areValidParams = params && $.isPlainObject(params), pjax = $e.data('pjax'), usePjax = pjax !== undefined && pjax !== 0 && $.support.pjax, pjaxContainer, pjaxOptions = {}, conflictParams = ['submit', 'reset', 'elements', 'length', 'name', 'acceptCharset', 'action', 'enctype', 'method', 'target']; // Forms and their child elements should not use input names or ids that conflict with properties of a form, // such as submit, length, or method. $.each(conflictParams, function (index, param) { if (areValidParams && params.hasOwnProperty(param)) { console.error("Parameter name '" + param + "' conflicts with a same named form property. " + "Please use another name."); } }); if (usePjax) { pjaxContainer = $e.data('pjax-container'); if (pjaxContainer === undefined || !pjaxContainer.length) { pjaxContainer = $e.closest('[data-pjax-container]').attr('id') ? ('#' + $e.closest('[data-pjax-container]').attr('id')) : ''; } if (!pjaxContainer.length) { pjaxContainer = 'body'; } pjaxOptions = { container: pjaxContainer, push: !!$e.data('pjax-push-state'), replace: !!$e.data('pjax-replace-state'), scrollTo: $e.data('pjax-scrollto'), pushRedirect: $e.data('pjax-push-redirect'), replaceRedirect: $e.data('pjax-replace-redirect'), skipOuterContainers: $e.data('pjax-skip-outer-containers'), timeout: $e.data('pjax-timeout'), originalEvent: event, originalTarget: $e }; } if (method === undefined) { if (isValidAction) { usePjax ? $.pjax.click(event, pjaxOptions) : window.location.assign(action); } else if ($e.is(':submit') && $form.length) { if (usePjax) { $form.on('submit', function (e) { $.pjax.submit(e, pjaxOptions); }); } $form.trigger('submit'); } return; } var oldMethod, oldAction, newForm = !$form.length; if (!newForm) { oldMethod = $form.attr('method'); $form.attr('method', method); if (isValidAction) { oldAction = $form.attr('action'); $form.attr('action', action); } } else { if (!isValidAction) { action = pub.getCurrentUrl(); } $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'; $form.attr('method', method); } if (/post/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 the element triggered the form submission. This is used by yii.activeForm.js. activeFormData.submitObject = $e; } if (areValidParams) { $.each(params, function (name, value) { $form.append($('').attr({name: name, value: value, type: 'hidden'})); }); } if (usePjax) { $form.on('submit', function (e) { $.pjax.submit(e, pjaxOptions); }); } $form.trigger('submit'); $.when($form.data('yiiSubmitFinalizePromise')).done(function () { if (newForm) { $form.remove(); return; } if (oldAction !== undefined) { $form.attr('action', oldAction); } $form.attr('method', oldMethod); if (areValidParams) { $.each(params, function (name) { $('input[name="' + name + '"]', $form).remove(); }); } }); }, getQueryParams: function (url) { var pos = url.indexOf('?'); if (pos < 0) { return {}; } var pairs = $.grep(url.substring(pos + 1).split('#')[0].split('&'), function (value) { return value !== ''; }); var params = {}; for (var i = 0, len = pairs.length; i < len; i++) { var pair = pairs[i].split('='); var name = decodeURIComponent(pair[0].replace(/\+/g, '%20')); var value = decodeURIComponent(pair[1].replace(/\+/g, '%20')); if (!name.length) { continue; } if (params[name] === undefined) { params[name] = value || ''; } else { if (!$.isArray(params[name])) { params[name] = [params[name]]; } params[name].push(value || ''); } } return params; }, initModule: function (module) { if (module.isActive !== undefined && !module.isActive) { return; } if ($.isFunction(module.init)) { module.init(); } $.each(module, function () { if ($.isPlainObject(this)) { pub.initModule(this); } }); }, init: function () { initCsrfHandler(); initRedirectHandler(); initAssetFilters(); initDataMethods(); }, /** * Returns the URL of the current page without params and trailing slash. Separated and made public for testing. * @returns {string} */ getBaseCurrentUrl: function () { return window.location.protocol + '//' + window.location.host; }, /** * Returns the URL of the current page. Used for testing, you can always call `window.location.href` manually * instead. * @returns {string} */ getCurrentUrl: function () { return window.location.href; } }; 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 initRedirectHandler() { // handle AJAX redirection $(document).ajaxComplete(function (event, xhr) { var url = xhr && xhr.getResponseHeader('X-Redirect'); if (url) { window.location.assign(url); } }); } function initAssetFilters() { /** * Used for storing loaded scripts and information about loading each script if it's in the process of loading. * A single script can have one of the following values: * * - `undefined` - script was not loaded at all before or was loaded with error last time. * - `true` (boolean) - script was successfully loaded. * - object - script is currently loading. * * In case of a value being an object the properties are: * - `xhrList` - represents a queue of XHR requests sent to the same URL (related with this script) in the same * small period of time. * - `xhrDone` - boolean, acts like a locking mechanism. When one of the XHR requests in the queue is * successfully completed, it will abort the rest of concurrent requests to the same URL until cleanup is done * to prevent possible errors and race conditions. * @type {{}} */ var loadedScripts = {}; $('script[src]').each(function () { var url = getAbsoluteUrl(this.src); loadedScripts[url] = true; }); $.ajaxPrefilter('script', function (options, originalOptions, xhr) { if (options.dataType == 'jsonp') { return; } var url = getAbsoluteUrl(options.url), forbiddenRepeatedLoad = loadedScripts[url] === true && !isReloadableAsset(url), cleanupRunning = loadedScripts[url] !== undefined && loadedScripts[url]['xhrDone'] === true; if (forbiddenRepeatedLoad || cleanupRunning) { xhr.abort(); return; } if (loadedScripts[url] === undefined || loadedScripts[url] === true) { loadedScripts[url] = { xhrList: [], xhrDone: false }; } xhr.done(function (data, textStatus, jqXHR) { // If multiple requests were successfully loaded, perform cleanup only once if (loadedScripts[jqXHR.yiiUrl]['xhrDone'] === true) { return; } loadedScripts[jqXHR.yiiUrl]['xhrDone'] = true; for (var i = 0, len = loadedScripts[jqXHR.yiiUrl]['xhrList'].length; i < len; i++) { var singleXhr = loadedScripts[jqXHR.yiiUrl]['xhrList'][i]; if (singleXhr && singleXhr.readyState !== XMLHttpRequest.DONE) { singleXhr.abort(); } } loadedScripts[jqXHR.yiiUrl] = true; }).fail(function (jqXHR, textStatus) { if (textStatus === 'abort') { return; } delete loadedScripts[jqXHR.yiiUrl]['xhrList'][jqXHR.yiiIndex]; var allFailed = true; for (var i = 0, len = loadedScripts[jqXHR.yiiUrl]['xhrList'].length; i < len; i++) { if (loadedScripts[jqXHR.yiiUrl]['xhrList'][i]) { allFailed = false; } } if (allFailed) { delete loadedScripts[jqXHR.yiiUrl]; } }); // Use prefix for custom XHR properties to avoid possible conflicts with existing properties xhr.yiiIndex = loadedScripts[url]['xhrList'].length; xhr.yiiUrl = url; loadedScripts[url]['xhrList'][xhr.yiiIndex] = xhr; }); $(document).ajaxComplete(function () { var styleSheets = []; $('link[rel=stylesheet]').each(function () { var url = getAbsoluteUrl(this.href); if (isReloadableAsset(url)) { return; } $.inArray(url, styleSheets) === -1 ? styleSheets.push(url) : $(this).remove(); }); }); } 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 && message !== false && message !== '') { $.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 isReloadableAsset(url) { for (var i = 0; i < pub.reloadableScripts.length; i++) { var rule = getAbsoluteUrl(pub.reloadableScripts[i]); 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, "\\$&"); } /** * Returns absolute URL based on the given URL * @param {string} url Initial URL * @returns {string} */ function getAbsoluteUrl(url) { return url.charAt(0) === '/' ? pub.getBaseCurrentUrl() + url : url; } return pub; })(window.jQuery); window.jQuery(function () { window.yii.initModule(window.yii); });