You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
522 lines
20 KiB
522 lines
20 KiB
/** |
|
* Yii JavaScript module. |
|
* |
|
* @link http://www.yiiframework.com/ |
|
* @copyright Copyright (c) 2008 Yii Software LLC |
|
* @license http://www.yiiframework.com/license/ |
|
* @author Qiang Xue <qiang.xue@gmail.com> |
|
* @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 = {}; |
|
|
|
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 = $('<form/>', {method: method, action: action}); |
|
var target = $e.attr('target'); |
|
if (target) { |
|
$form.attr('target', target); |
|
} |
|
if (!/(get|post)/i.test(method)) { |
|
$form.append($('<input/>', {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($('<input/>', {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($('<input/>').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')).then(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) { |
|
$.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); |
|
});
|
|
|