470 lines
18KB

  1. /**
  2. * Yii JavaScript module.
  3. *
  4. * @link http://www.yiiframework.com/
  5. * @copyright Copyright (c) 2008 Yii Software LLC
  6. * @license http://www.yiiframework.com/license/
  7. * @author Qiang Xue <qiang.xue@gmail.com>
  8. * @since 2.0
  9. */
  10. /**
  11. * yii is the root module for all Yii JavaScript modules.
  12. * It implements a mechanism of organizing JavaScript code in modules through the function "yii.initModule()".
  13. *
  14. * Each module should be named as "x.y.z", where "x" stands for the root module (for the Yii core code, this is "yii").
  15. *
  16. * A module may be structured as follows:
  17. *
  18. * ```javascript
  19. * yii.sample = (function($) {
  20. * var pub = {
  21. * // whether this module is currently active. If false, init() will not be called for this module
  22. * // it will also not be called for all its child modules. If this property is undefined, it means true.
  23. * isActive: true,
  24. * init: function() {
  25. * // ... module initialization code go here ...
  26. * },
  27. *
  28. * // ... other public functions and properties go here ...
  29. * };
  30. *
  31. * // ... private functions and properties go here ...
  32. *
  33. * return pub;
  34. * })(jQuery);
  35. * ```
  36. *
  37. * Using this structure, you can define public and private functions/properties for a module.
  38. * Private functions/properties are only visible within the module, while public functions/properties
  39. * may be accessed outside of the module. For example, you can access "yii.sample.isActive".
  40. *
  41. * You must call "yii.initModule()" once for the root module of all your modules.
  42. */
  43. window.yii = (function ($) {
  44. var pub = {
  45. /**
  46. * List of JS or CSS URLs that can be loaded multiple times via AJAX requests.
  47. * Each item may be represented as either an absolute URL or a relative one.
  48. * Each item may contain a wildcart matching character `*`, that means one or more
  49. * any characters on the position. For example:
  50. * - `/css/*.js` will match any file ending with `.js` in the `css` directory of the current web site
  51. * - `http*://cdn.example.com/*` will match any files on domain `cdn.example.com`, loaded with HTTP or HTTPS
  52. * - `/js/myCustomScript.js?realm=*` will match file `/js/myCustomScript.js` with defined `realm` parameter
  53. */
  54. reloadableScripts: [],
  55. /**
  56. * The selector for clickable elements that need to support confirmation and form submission.
  57. */
  58. clickableSelector: 'a, button, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"]',
  59. /**
  60. * The selector for changeable elements that need to support confirmation and form submission.
  61. */
  62. changeableSelector: 'select, input, textarea',
  63. /**
  64. * @return string|undefined the CSRF parameter name. Undefined is returned if CSRF validation is not enabled.
  65. */
  66. getCsrfParam: function () {
  67. return $('meta[name=csrf-param]').attr('content');
  68. },
  69. /**
  70. * @return string|undefined the CSRF token. Undefined is returned if CSRF validation is not enabled.
  71. */
  72. getCsrfToken: function () {
  73. return $('meta[name=csrf-token]').attr('content');
  74. },
  75. /**
  76. * Sets the CSRF token in the meta elements.
  77. * This method is provided so that you can update the CSRF token with the latest one you obtain from the server.
  78. * @param name the CSRF token name
  79. * @param value the CSRF token value
  80. */
  81. setCsrfToken: function (name, value) {
  82. $('meta[name=csrf-param]').attr('content', name);
  83. $('meta[name=csrf-token]').attr('content', value);
  84. },
  85. /**
  86. * Updates all form CSRF input fields with the latest CSRF token.
  87. * This method is provided to avoid cached forms containing outdated CSRF tokens.
  88. */
  89. refreshCsrfToken: function () {
  90. var token = pub.getCsrfToken();
  91. if (token) {
  92. $('form input[name="' + pub.getCsrfParam() + '"]').val(token);
  93. }
  94. },
  95. /**
  96. * Displays a confirmation dialog.
  97. * The default implementation simply displays a js confirmation dialog.
  98. * You may override this by setting `yii.confirm`.
  99. * @param message the confirmation message.
  100. * @param ok a callback to be called when the user confirms the message
  101. * @param cancel a callback to be called when the user cancels the confirmation
  102. */
  103. confirm: function (message, ok, cancel) {
  104. if (confirm(message)) {
  105. !ok || ok();
  106. } else {
  107. !cancel || cancel();
  108. }
  109. },
  110. /**
  111. * Handles the action triggered by user.
  112. * This method recognizes the `data-method` attribute of the element. If the attribute exists,
  113. * the method will submit the form containing this element. If there is no containing form, a form
  114. * will be created and submitted using the method given by this attribute value (e.g. "post", "put").
  115. * For hyperlinks, the form action will take the value of the "href" attribute of the link.
  116. * For other elements, either the containing form action or the current page URL will be used
  117. * as the form action URL.
  118. *
  119. * If the `data-method` attribute is not defined, the `href` attribute (if any) of the element
  120. * will be assigned to `window.location`.
  121. *
  122. * Starting from version 2.0.3, the `data-params` attribute is also recognized when you specify
  123. * `data-method`. The value of `data-params` should be a JSON representation of the data (name-value pairs)
  124. * that should be submitted as hidden inputs. For example, you may use the following code to generate
  125. * such a link:
  126. *
  127. * ```php
  128. * use yii\helpers\Html;
  129. * use yii\helpers\Json;
  130. *
  131. * echo Html::a('submit', ['site/foobar'], [
  132. * 'data' => [
  133. * 'method' => 'post',
  134. * 'params' => [
  135. * 'name1' => 'value1',
  136. * 'name2' => 'value2',
  137. * ],
  138. * ],
  139. * ];
  140. * ```
  141. *
  142. * @param $e the jQuery representation of the element
  143. */
  144. handleAction: function ($e, event) {
  145. var $form = $e.attr('data-form') ? $('#' + $e.attr('data-form')) : $e.closest('form'),
  146. method = !$e.data('method') && $form ? $form.attr('method') : $e.data('method'),
  147. action = $e.attr('href'),
  148. params = $e.data('params'),
  149. pjax = $e.data('pjax'),
  150. pjaxPushState = !!$e.data('pjax-push-state'),
  151. pjaxReplaceState = !!$e.data('pjax-replace-state'),
  152. pjaxTimeout = $e.data('pjax-timeout'),
  153. pjaxScrollTo = $e.data('pjax-scrollto'),
  154. pjaxPushRedirect = $e.data('pjax-push-redirect'),
  155. pjaxReplaceRedirect = $e.data('pjax-replace-redirect'),
  156. pjaxSkipOuterContainers = $e.data('pjax-skip-outer-containers'),
  157. pjaxContainer,
  158. pjaxOptions = {};
  159. if (pjax !== undefined && $.support.pjax) {
  160. if ($e.data('pjax-container')) {
  161. pjaxContainer = $e.data('pjax-container');
  162. } else {
  163. pjaxContainer = $e.closest('[data-pjax-container=""]');
  164. }
  165. // default to body if pjax container not found
  166. if (!pjaxContainer.length) {
  167. pjaxContainer = $('body');
  168. }
  169. pjaxOptions = {
  170. container: pjaxContainer,
  171. push: pjaxPushState,
  172. replace: pjaxReplaceState,
  173. scrollTo: pjaxScrollTo,
  174. pushRedirect: pjaxPushRedirect,
  175. replaceRedirect: pjaxReplaceRedirect,
  176. pjaxSkipOuterContainers: pjaxSkipOuterContainers,
  177. timeout: pjaxTimeout,
  178. originalEvent: event,
  179. originalTarget: $e
  180. }
  181. }
  182. if (method === undefined) {
  183. if (action && action != '#') {
  184. if (pjax !== undefined && $.support.pjax) {
  185. $.pjax.click(event, pjaxOptions);
  186. } else {
  187. window.location = action;
  188. }
  189. } else if ($e.is(':submit') && $form.length) {
  190. if (pjax !== undefined && $.support.pjax) {
  191. $form.on('submit',function(e){
  192. $.pjax.submit(e, pjaxOptions);
  193. })
  194. }
  195. $form.trigger('submit');
  196. }
  197. return;
  198. }
  199. var newForm = !$form.length;
  200. if (newForm) {
  201. if (!action || !action.match(/(^\/|:\/\/)/)) {
  202. action = window.location.href;
  203. }
  204. $form = $('<form/>', {method: method, action: action});
  205. var target = $e.attr('target');
  206. if (target) {
  207. $form.attr('target', target);
  208. }
  209. if (!method.match(/(get|post)/i)) {
  210. $form.append($('<input/>', {name: '_method', value: method, type: 'hidden'}));
  211. method = 'POST';
  212. }
  213. if (!method.match(/(get|head|options)/i)) {
  214. var csrfParam = pub.getCsrfParam();
  215. if (csrfParam) {
  216. $form.append($('<input/>', {name: csrfParam, value: pub.getCsrfToken(), type: 'hidden'}));
  217. }
  218. }
  219. $form.hide().appendTo('body');
  220. }
  221. var activeFormData = $form.data('yiiActiveForm');
  222. if (activeFormData) {
  223. // remember who triggers the form submission. This is used by yii.activeForm.js
  224. activeFormData.submitObject = $e;
  225. }
  226. // temporarily add hidden inputs according to data-params
  227. if (params && $.isPlainObject(params)) {
  228. $.each(params, function (idx, obj) {
  229. $form.append($('<input/>').attr({name: idx, value: obj, type: 'hidden'}));
  230. });
  231. }
  232. var oldMethod = $form.attr('method');
  233. $form.attr('method', method);
  234. var oldAction = null;
  235. if (action && action != '#') {
  236. oldAction = $form.attr('action');
  237. $form.attr('action', action);
  238. }
  239. if (pjax !== undefined && $.support.pjax) {
  240. $form.on('submit',function(e){
  241. $.pjax.submit(e, pjaxOptions);
  242. })
  243. }
  244. $form.trigger('submit');
  245. $.when($form.data('yiiSubmitFinalizePromise')).then(
  246. function () {
  247. if (oldAction != null) {
  248. $form.attr('action', oldAction);
  249. }
  250. $form.attr('method', oldMethod);
  251. // remove the temporarily added hidden inputs
  252. if (params && $.isPlainObject(params)) {
  253. $.each(params, function (idx, obj) {
  254. $('input[name="' + idx + '"]', $form).remove();
  255. });
  256. }
  257. if (newForm) {
  258. $form.remove();
  259. }
  260. }
  261. );
  262. },
  263. getQueryParams: function (url) {
  264. var pos = url.indexOf('?');
  265. if (pos < 0) {
  266. return {};
  267. }
  268. var pairs = url.substring(pos + 1).split('#')[0].split('&'),
  269. params = {},
  270. pair,
  271. i;
  272. for (i = 0; i < pairs.length; i++) {
  273. pair = pairs[i].split('=');
  274. var name = decodeURIComponent(pair[0].replace(/\+/g, '%20'));
  275. var value = decodeURIComponent(pair[1].replace(/\+/g, '%20'));
  276. if (name.length) {
  277. if (params[name] !== undefined) {
  278. if (!$.isArray(params[name])) {
  279. params[name] = [params[name]];
  280. }
  281. params[name].push(value || '');
  282. } else {
  283. params[name] = value || '';
  284. }
  285. }
  286. }
  287. return params;
  288. },
  289. initModule: function (module) {
  290. if (module.isActive === undefined || module.isActive) {
  291. if ($.isFunction(module.init)) {
  292. module.init();
  293. }
  294. $.each(module, function () {
  295. if ($.isPlainObject(this)) {
  296. pub.initModule(this);
  297. }
  298. });
  299. }
  300. },
  301. init: function () {
  302. initCsrfHandler();
  303. initRedirectHandler();
  304. initScriptFilter();
  305. initDataMethods();
  306. }
  307. };
  308. function initRedirectHandler() {
  309. // handle AJAX redirection
  310. $(document).ajaxComplete(function (event, xhr, settings) {
  311. var url = xhr && xhr.getResponseHeader('X-Redirect');
  312. if (url) {
  313. window.location = url;
  314. }
  315. });
  316. }
  317. function initCsrfHandler() {
  318. // automatically send CSRF token for all AJAX requests
  319. $.ajaxPrefilter(function (options, originalOptions, xhr) {
  320. if (!options.crossDomain && pub.getCsrfParam()) {
  321. xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken());
  322. }
  323. });
  324. pub.refreshCsrfToken();
  325. }
  326. function initDataMethods() {
  327. var handler = function (event) {
  328. var $this = $(this),
  329. method = $this.data('method'),
  330. message = $this.data('confirm'),
  331. form = $this.data('form');
  332. if (method === undefined && message === undefined && form === undefined) {
  333. return true;
  334. }
  335. if (message !== undefined) {
  336. $.proxy(pub.confirm, this)(message, function () {
  337. pub.handleAction($this, event);
  338. });
  339. } else {
  340. pub.handleAction($this, event);
  341. }
  342. event.stopImmediatePropagation();
  343. return false;
  344. };
  345. // handle data-confirm and data-method for clickable and changeable elements
  346. $(document).on('click.yii', pub.clickableSelector, handler)
  347. .on('change.yii', pub.changeableSelector, handler);
  348. }
  349. function isReloadable(url) {
  350. var hostInfo = getHostInfo();
  351. for (var i = 0; i < pub.reloadableScripts.length; i++) {
  352. var rule = pub.reloadableScripts[i];
  353. rule = rule.charAt(0) === '/' ? hostInfo + rule : rule;
  354. var match = new RegExp("^" + escapeRegExp(rule).split('\\*').join('.*') + "$").test(url);
  355. if (match === true) {
  356. return true;
  357. }
  358. }
  359. return false;
  360. }
  361. // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
  362. function escapeRegExp(str) {
  363. return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  364. }
  365. function getHostInfo() {
  366. return location.protocol + '//' + location.host;
  367. }
  368. function initScriptFilter() {
  369. var hostInfo = getHostInfo();
  370. var loadedScripts = {};
  371. var scripts = $('script[src]').map(function () {
  372. return this.src.charAt(0) === '/' ? hostInfo + this.src : this.src;
  373. }).toArray();
  374. for (var i = 0, len = scripts.length; i < len; i++) {
  375. loadedScripts[scripts[i]] = true;
  376. }
  377. $.ajaxPrefilter('script', function (options, originalOptions, xhr) {
  378. if (options.dataType == 'jsonp') {
  379. return;
  380. }
  381. var url = options.url.charAt(0) === '/' ? hostInfo + options.url : options.url;
  382. if (url in loadedScripts) {
  383. var item = loadedScripts[url];
  384. // If the concurrent XHR request is running and URL is not reloadable
  385. if (item !== true && !isReloadable(url)) {
  386. // Abort the current XHR request when previous finished successfully
  387. item.done(function () {
  388. if (xhr && xhr.readyState !== 4) {
  389. xhr.abort();
  390. }
  391. });
  392. // Or abort previous XHR if the current one is loaded faster
  393. xhr.done(function () {
  394. if (item && item.readyState !== 4) {
  395. item.abort();
  396. }
  397. });
  398. } else if (!isReloadable(url)) {
  399. xhr.abort();
  400. }
  401. } else {
  402. loadedScripts[url] = xhr.done(function () {
  403. loadedScripts[url] = true;
  404. }).fail(function () {
  405. delete loadedScripts[url];
  406. });
  407. }
  408. });
  409. $(document).ajaxComplete(function (event, xhr, settings) {
  410. var styleSheets = [];
  411. $('link[rel=stylesheet]').each(function () {
  412. if (isReloadable(this.href)) {
  413. return;
  414. }
  415. if ($.inArray(this.href, styleSheets) == -1) {
  416. styleSheets.push(this.href)
  417. } else {
  418. $(this).remove();
  419. }
  420. })
  421. });
  422. }
  423. return pub;
  424. })(jQuery);
  425. jQuery(function () {
  426. yii.initModule(yii);
  427. });