cart.js 15 KB

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