Нема описа

cart.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. /* global wc_cart_params */
  2. jQuery( function( $ ) {
  3. // wc_cart_params is required to continue, ensure the object exists
  4. if ( typeof wc_cart_params === 'undefined' ) {
  5. return false;
  6. }
  7. // Utility functions for the file.
  8. /**
  9. * Gets a url for a given AJAX endpoint.
  10. *
  11. * @param {String} endpoint The AJAX Endpoint
  12. * @return {String} The URL to use for the request
  13. */
  14. var get_url = function( endpoint ) {
  15. return wc_cart_params.wc_ajax_url.toString().replace(
  16. '%%endpoint%%',
  17. endpoint
  18. );
  19. };
  20. /**
  21. * Check if a node is blocked for processing.
  22. *
  23. * @param {JQuery Object} $node
  24. * @return {bool} True if the DOM Element is UI Blocked, false if not.
  25. */
  26. var is_blocked = function( $node ) {
  27. return $node.is( '.processing' ) || $node.parents( '.processing' ).length;
  28. };
  29. /**
  30. * Block a node visually for processing.
  31. *
  32. * @param {JQuery Object} $node
  33. */
  34. var block = function( $node ) {
  35. if ( ! is_blocked( $node ) ) {
  36. $node.addClass( 'processing' ).block( {
  37. message: null,
  38. overlayCSS: {
  39. background: '#fff',
  40. opacity: 0.6
  41. }
  42. } );
  43. }
  44. };
  45. /**
  46. * Unblock a node after processing is complete.
  47. *
  48. * @param {JQuery Object} $node
  49. */
  50. var unblock = function( $node ) {
  51. $node.removeClass( 'processing' ).unblock();
  52. };
  53. /**
  54. * Removes duplicate notices.
  55. *
  56. * @param {JQuery Object} notices
  57. */
  58. var remove_duplicate_notices = function( notices ) {
  59. var seen = [];
  60. var new_notices = notices;
  61. notices.each( function( index ) {
  62. var text = $( this ).text();
  63. if ( 'undefined' === typeof seen[ text ] ) {
  64. seen[ text ] = true;
  65. } else {
  66. new_notices.splice( index, 1 );
  67. }
  68. } );
  69. return new_notices;
  70. };
  71. /**
  72. * Update the .woocommerce div with a string of html.
  73. *
  74. * @param {String} html_str The HTML string with which to replace the div.
  75. * @param {bool} preserve_notices Should notices be kept? False by default.
  76. */
  77. var update_wc_div = function( html_str, preserve_notices ) {
  78. var $html = $.parseHTML( html_str );
  79. var $new_form = $( '.woocommerce-cart-form', $html );
  80. var $new_totals = $( '.cart_totals', $html );
  81. var $notices = remove_duplicate_notices( $( '.woocommerce-error, .woocommerce-message, .woocommerce-info', $html ) );
  82. // No form, cannot do this.
  83. if ( $( '.woocommerce-cart-form' ).length === 0 ) {
  84. window.location.reload();
  85. return;
  86. }
  87. // Remove errors
  88. if ( ! preserve_notices ) {
  89. $( '.woocommerce-error, .woocommerce-message, .woocommerce-info' ).remove();
  90. }
  91. if ( $new_form.length === 0 ) {
  92. // If the checkout is also displayed on this page, trigger reload instead.
  93. if ( $( '.woocommerce-checkout' ).length ) {
  94. window.location.reload();
  95. return;
  96. }
  97. // No items to display now! Replace all cart content.
  98. var $cart_html = $( '.cart-empty', $html ).closest( '.woocommerce' );
  99. $( '.woocommerce-cart-form__contents' ).closest( '.woocommerce' ).replaceWith( $cart_html );
  100. // Display errors
  101. if ( $notices.length > 0 ) {
  102. show_notice( $notices );
  103. }
  104. // Notify plugins that the cart was emptied.
  105. $( document.body ).trigger( 'wc_cart_emptied' );
  106. } else {
  107. // If the checkout is also displayed on this page, trigger update event.
  108. if ( $( '.woocommerce-checkout' ).length ) {
  109. $( document.body ).trigger( 'update_checkout' );
  110. }
  111. $( '.woocommerce-cart-form' ).replaceWith( $new_form );
  112. $( '.woocommerce-cart-form' ).find( ':input[name="update_cart"]' ).prop( 'disabled', true ).attr( 'aria-disabled', true );
  113. if ( $notices.length > 0 ) {
  114. show_notice( $notices );
  115. }
  116. update_cart_totals_div( $new_totals );
  117. }
  118. $( document.body ).trigger( 'updated_wc_div' );
  119. };
  120. /**
  121. * Update the .cart_totals div with a string of html.
  122. *
  123. * @param {String} html_str The HTML string with which to replace the div.
  124. */
  125. var update_cart_totals_div = function( html_str ) {
  126. $( '.cart_totals' ).replaceWith( html_str );
  127. $( document.body ).trigger( 'updated_cart_totals' );
  128. };
  129. /**
  130. * Shows new notices on the page.
  131. *
  132. * @param {Object} The Notice HTML Element in string or object form.
  133. */
  134. var show_notice = function( html_element, $target ) {
  135. if ( ! $target ) {
  136. $target = $( '.woocommerce-notices-wrapper:first' ) ||
  137. $( '.cart-empty' ).closest( '.woocommerce' ) ||
  138. $( '.woocommerce-cart-form' );
  139. }
  140. $target.prepend( html_element );
  141. };
  142. /**
  143. * Object to handle AJAX calls for cart shipping changes.
  144. */
  145. var cart_shipping = {
  146. /**
  147. * Initialize event handlers and UI state.
  148. */
  149. init: function( cart ) {
  150. this.cart = cart;
  151. this.toggle_shipping = this.toggle_shipping.bind( this );
  152. this.shipping_method_selected = this.shipping_method_selected.bind( this );
  153. this.shipping_calculator_submit = this.shipping_calculator_submit.bind( this );
  154. $( document ).on(
  155. 'click',
  156. '.shipping-calculator-button',
  157. this.toggle_shipping
  158. );
  159. $( document ).on(
  160. 'change',
  161. 'select.shipping_method, :input[name^=shipping_method]',
  162. this.shipping_method_selected
  163. );
  164. $( document ).on(
  165. 'submit',
  166. 'form.woocommerce-shipping-calculator',
  167. this.shipping_calculator_submit
  168. );
  169. $( '.shipping-calculator-form' ).hide();
  170. },
  171. /**
  172. * Toggle Shipping Calculator panel
  173. */
  174. toggle_shipping: function() {
  175. $( '.shipping-calculator-form' ).slideToggle( 'slow' );
  176. $( 'select.country_to_state, input.country_to_state' ).trigger( 'change' );
  177. $( document.body ).trigger( 'country_to_state_changed' ); // Trigger select2 to load.
  178. return false;
  179. },
  180. /**
  181. * Handles when a shipping method is selected.
  182. */
  183. shipping_method_selected: function() {
  184. var shipping_methods = {};
  185. // eslint-disable-next-line max-len
  186. $( 'select.shipping_method, :input[name^=shipping_method][type=radio]:checked, :input[name^=shipping_method][type=hidden]' ).each( function() {
  187. shipping_methods[ $( this ).data( 'index' ) ] = $( this ).val();
  188. } );
  189. block( $( 'div.cart_totals' ) );
  190. var data = {
  191. security: wc_cart_params.update_shipping_method_nonce,
  192. shipping_method: shipping_methods
  193. };
  194. $.ajax( {
  195. type: 'post',
  196. url: get_url( 'update_shipping_method' ),
  197. data: data,
  198. dataType: 'html',
  199. success: function( response ) {
  200. update_cart_totals_div( response );
  201. },
  202. complete: function() {
  203. unblock( $( 'div.cart_totals' ) );
  204. $( document.body ).trigger( 'updated_shipping_method' );
  205. }
  206. } );
  207. },
  208. /**
  209. * Handles a shipping calculator form submit.
  210. *
  211. * @param {Object} evt The JQuery event.
  212. */
  213. shipping_calculator_submit: function( evt ) {
  214. evt.preventDefault();
  215. var $form = $( evt.currentTarget );
  216. block( $( 'div.cart_totals' ) );
  217. block( $form );
  218. // Provide the submit button value because wc-form-handler expects it.
  219. $( '<input />' ).attr( 'type', 'hidden' )
  220. .attr( 'name', 'calc_shipping' )
  221. .attr( 'value', 'x' )
  222. .appendTo( $form );
  223. // Make call to actual form post URL.
  224. $.ajax( {
  225. type: $form.attr( 'method' ),
  226. url: $form.attr( 'action' ),
  227. data: $form.serialize(),
  228. dataType: 'html',
  229. success: function( response ) {
  230. update_wc_div( response );
  231. },
  232. complete: function() {
  233. unblock( $form );
  234. unblock( $( 'div.cart_totals' ) );
  235. }
  236. } );
  237. }
  238. };
  239. /**
  240. * Object to handle cart UI.
  241. */
  242. var cart = {
  243. /**
  244. * Initialize cart UI events.
  245. */
  246. init: function() {
  247. this.update_cart_totals = this.update_cart_totals.bind( this );
  248. this.input_keypress = this.input_keypress.bind( this );
  249. this.cart_submit = this.cart_submit.bind( this );
  250. this.submit_click = this.submit_click.bind( this );
  251. this.apply_coupon = this.apply_coupon.bind( this );
  252. this.remove_coupon_clicked = this.remove_coupon_clicked.bind( this );
  253. this.quantity_update = this.quantity_update.bind( this );
  254. this.item_remove_clicked = this.item_remove_clicked.bind( this );
  255. this.item_restore_clicked = this.item_restore_clicked.bind( this );
  256. this.update_cart = this.update_cart.bind( this );
  257. $( document ).on(
  258. 'wc_update_cart added_to_cart',
  259. function() { cart.update_cart.apply( cart, [].slice.call( arguments, 1 ) ); } );
  260. $( document ).on(
  261. 'click',
  262. '.woocommerce-cart-form :input[type=submit]',
  263. this.submit_click );
  264. $( document ).on(
  265. 'keypress',
  266. '.woocommerce-cart-form :input[type=number]',
  267. this.input_keypress );
  268. $( document ).on(
  269. 'submit',
  270. '.woocommerce-cart-form',
  271. this.cart_submit );
  272. $( document ).on(
  273. 'click',
  274. 'a.woocommerce-remove-coupon',
  275. this.remove_coupon_clicked );
  276. $( document ).on(
  277. 'click',
  278. '.woocommerce-cart-form .product-remove > a',
  279. this.item_remove_clicked );
  280. $( document ).on(
  281. 'click',
  282. '.woocommerce-cart .restore-item',
  283. this.item_restore_clicked );
  284. $( document ).on(
  285. 'change input',
  286. '.woocommerce-cart-form .cart_item :input',
  287. this.input_changed );
  288. $( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 'disabled', true ).attr( 'aria-disabled', true );
  289. },
  290. /**
  291. * After an input is changed, enable the update cart button.
  292. */
  293. input_changed: function() {
  294. $( '.woocommerce-cart-form :input[name="update_cart"]' ).prop( 'disabled', false ).attr( 'aria-disabled', false );
  295. },
  296. /**
  297. * Update entire cart via ajax.
  298. */
  299. update_cart: function( preserve_notices ) {
  300. var $form = $( '.woocommerce-cart-form' );
  301. block( $form );
  302. block( $( 'div.cart_totals' ) );
  303. // Make call to actual form post URL.
  304. $.ajax( {
  305. type: $form.attr( 'method' ),
  306. url: $form.attr( 'action' ),
  307. data: $form.serialize(),
  308. dataType: 'html',
  309. success: function( response ) {
  310. update_wc_div( response, preserve_notices );
  311. },
  312. complete: function() {
  313. unblock( $form );
  314. unblock( $( 'div.cart_totals' ) );
  315. $.scroll_to_notices( $( '[role="alert"]' ) );
  316. }
  317. } );
  318. },
  319. /**
  320. * Update the cart after something has changed.
  321. */
  322. update_cart_totals: function() {
  323. block( $( 'div.cart_totals' ) );
  324. $.ajax( {
  325. url: get_url( 'get_cart_totals' ),
  326. dataType: 'html',
  327. success: function( response ) {
  328. update_cart_totals_div( response );
  329. },
  330. complete: function() {
  331. unblock( $( 'div.cart_totals' ) );
  332. }
  333. } );
  334. },
  335. /**
  336. * Handle the <ENTER> key for quantity fields.
  337. *
  338. * @param {Object} evt The JQuery event
  339. *
  340. * For IE, if you hit enter on a quantity field, it makes the
  341. * document.activeElement the first submit button it finds.
  342. * For us, that is the Apply Coupon button. This is required
  343. * to catch the event before that happens.
  344. */
  345. input_keypress: function( evt ) {
  346. // Catch the enter key and don't let it submit the form.
  347. if ( 13 === evt.keyCode ) {
  348. var $form = $( evt.currentTarget ).parents( 'form' );
  349. try {
  350. // If there are no validation errors, handle the submit.
  351. if ( $form[0].checkValidity() ) {
  352. evt.preventDefault();
  353. this.cart_submit( evt );
  354. }
  355. } catch( err ) {
  356. evt.preventDefault();
  357. this.cart_submit( evt );
  358. }
  359. }
  360. },
  361. /**
  362. * Handle cart form submit and route to correct logic.
  363. *
  364. * @param {Object} evt The JQuery event
  365. */
  366. cart_submit: function( evt ) {
  367. var $submit = $( document.activeElement ),
  368. $clicked = $( ':input[type=submit][clicked=true]' ),
  369. $form = $( evt.currentTarget );
  370. // For submit events, currentTarget is form.
  371. // For keypress events, currentTarget is input.
  372. if ( ! $form.is( 'form' ) ) {
  373. $form = $( evt.currentTarget ).parents( 'form' );
  374. }
  375. if ( 0 === $form.find( '.woocommerce-cart-form__contents' ).length ) {
  376. return;
  377. }
  378. if ( is_blocked( $form ) ) {
  379. return false;
  380. }
  381. if ( $clicked.is( ':input[name="update_cart"]' ) || $submit.is( 'input.qty' ) ) {
  382. evt.preventDefault();
  383. this.quantity_update( $form );
  384. } else if ( $clicked.is( ':input[name="apply_coupon"]' ) || $submit.is( '#coupon_code' ) ) {
  385. evt.preventDefault();
  386. this.apply_coupon( $form );
  387. }
  388. },
  389. /**
  390. * Special handling to identify which submit button was clicked.
  391. *
  392. * @param {Object} evt The JQuery event
  393. */
  394. submit_click: function( evt ) {
  395. $( ':input[type=submit]', $( evt.target ).parents( 'form' ) ).removeAttr( 'clicked' );
  396. $( evt.target ).attr( 'clicked', 'true' );
  397. },
  398. /**
  399. * Apply Coupon code
  400. *
  401. * @param {JQuery Object} $form The cart form.
  402. */
  403. apply_coupon: function( $form ) {
  404. block( $form );
  405. var cart = this;
  406. var $text_field = $( '#coupon_code' );
  407. var coupon_code = $text_field.val();
  408. var data = {
  409. security: wc_cart_params.apply_coupon_nonce,
  410. coupon_code: coupon_code
  411. };
  412. $.ajax( {
  413. type: 'POST',
  414. url: get_url( 'apply_coupon' ),
  415. data: data,
  416. dataType: 'html',
  417. success: function( response ) {
  418. $( '.woocommerce-error, .woocommerce-message, .woocommerce-info' ).remove();
  419. show_notice( response );
  420. $( document.body ).trigger( 'applied_coupon', [ coupon_code ] );
  421. },
  422. complete: function() {
  423. unblock( $form );
  424. $text_field.val( '' );
  425. cart.update_cart( true );
  426. }
  427. } );
  428. },
  429. /**
  430. * Handle when a remove coupon link is clicked.
  431. *
  432. * @param {Object} evt The JQuery event
  433. */
  434. remove_coupon_clicked: function( evt ) {
  435. evt.preventDefault();
  436. var cart = this;
  437. var $wrapper = $( evt.currentTarget ).closest( '.cart_totals' );
  438. var coupon = $( evt.currentTarget ).attr( 'data-coupon' );
  439. block( $wrapper );
  440. var data = {
  441. security: wc_cart_params.remove_coupon_nonce,
  442. coupon: coupon
  443. };
  444. $.ajax( {
  445. type: 'POST',
  446. url: get_url( 'remove_coupon' ),
  447. data: data,
  448. dataType: 'html',
  449. success: function( response ) {
  450. $( '.woocommerce-error, .woocommerce-message, .woocommerce-info' ).remove();
  451. show_notice( response );
  452. $( document.body ).trigger( 'removed_coupon', [ coupon ] );
  453. unblock( $wrapper );
  454. },
  455. complete: function() {
  456. cart.update_cart( true );
  457. }
  458. } );
  459. },
  460. /**
  461. * Handle a cart Quantity Update
  462. *
  463. * @param {JQuery Object} $form The cart form.
  464. */
  465. quantity_update: function( $form ) {
  466. block( $form );
  467. block( $( 'div.cart_totals' ) );
  468. // Provide the submit button value because wc-form-handler expects it.
  469. $( '<input />' ).attr( 'type', 'hidden' )
  470. .attr( 'name', 'update_cart' )
  471. .attr( 'value', 'Update Cart' )
  472. .appendTo( $form );
  473. // Make call to actual form post URL.
  474. $.ajax( {
  475. type: $form.attr( 'method' ),
  476. url: $form.attr( 'action' ),
  477. data: $form.serialize(),
  478. dataType: 'html',
  479. success: function( response ) {
  480. update_wc_div( response );
  481. },
  482. complete: function() {
  483. unblock( $form );
  484. unblock( $( 'div.cart_totals' ) );
  485. $.scroll_to_notices( $( '[role="alert"]' ) );
  486. }
  487. } );
  488. },
  489. /**
  490. * Handle when a remove item link is clicked.
  491. *
  492. * @param {Object} evt The JQuery event
  493. */
  494. item_remove_clicked: function( evt ) {
  495. evt.preventDefault();
  496. var $a = $( evt.currentTarget );
  497. var $form = $a.parents( 'form' );
  498. block( $form );
  499. block( $( 'div.cart_totals' ) );
  500. $.ajax( {
  501. type: 'GET',
  502. url: $a.attr( 'href' ),
  503. dataType: 'html',
  504. success: function( response ) {
  505. update_wc_div( response );
  506. },
  507. complete: function() {
  508. unblock( $form );
  509. unblock( $( 'div.cart_totals' ) );
  510. $.scroll_to_notices( $( '[role="alert"]' ) );
  511. }
  512. } );
  513. },
  514. /**
  515. * Handle when a restore item link is clicked.
  516. *
  517. * @param {Object} evt The JQuery event
  518. */
  519. item_restore_clicked: function( evt ) {
  520. evt.preventDefault();
  521. var $a = $( evt.currentTarget );
  522. var $form = $( 'form.woocommerce-cart-form' );
  523. block( $form );
  524. block( $( 'div.cart_totals' ) );
  525. $.ajax( {
  526. type: 'GET',
  527. url: $a.attr( 'href' ),
  528. dataType: 'html',
  529. success: function( response ) {
  530. update_wc_div( response );
  531. },
  532. complete: function() {
  533. unblock( $form );
  534. unblock( $( 'div.cart_totals' ) );
  535. }
  536. } );
  537. }
  538. };
  539. cart_shipping.init( cart );
  540. cart.init();
  541. } );