settings-views-html-settings-tax.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. /* global htmlSettingsTaxLocalizeScript, ajaxurl */
  2. /**
  3. * Used by woocommerce/includes/admin/settings/views/html-settings-tax.php
  4. */
  5. ( function( $, data, wp, ajaxurl ) {
  6. $( function() {
  7. if ( ! String.prototype.trim ) {
  8. String.prototype.trim = function () {
  9. return this.replace( /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '' );
  10. };
  11. }
  12. var rowTemplate = wp.template( 'wc-tax-table-row' ),
  13. rowTemplateEmpty = wp.template( 'wc-tax-table-row-empty' ),
  14. paginationTemplate = wp.template( 'wc-tax-table-pagination' ),
  15. $table = $( '.wc_tax_rates' ),
  16. $tbody = $( '#rates' ),
  17. $save_button = $( ':input[name="save"]' ),
  18. $pagination = $( '#rates-pagination' ),
  19. $search_field = $( '#rates-search .wc-tax-rates-search-field' ),
  20. $submit = $( '.submit .button-primary[type=submit]' ),
  21. WCTaxTableModelConstructor = Backbone.Model.extend({
  22. changes: {},
  23. setRateAttribute: function( rateID, attribute, value ) {
  24. var rates = _.indexBy( this.get( 'rates' ), 'tax_rate_id' ),
  25. changes = {};
  26. if ( rates[ rateID ][ attribute ] !== value ) {
  27. changes[ rateID ] = {};
  28. changes[ rateID ][ attribute ] = value;
  29. rates[ rateID ][ attribute ] = value;
  30. }
  31. this.logChanges( changes );
  32. },
  33. logChanges: function( changedRows ) {
  34. var changes = this.changes || {};
  35. _.each( changedRows, function( row, id ) {
  36. changes[ id ] = _.extend( changes[ id ] || {
  37. tax_rate_id : id
  38. }, row );
  39. } );
  40. this.changes = changes;
  41. this.trigger( 'change:rates' );
  42. },
  43. getFilteredRates: function() {
  44. var rates = this.get( 'rates' ),
  45. search = $search_field.val().toLowerCase();
  46. if ( search.length ) {
  47. rates = _.filter( rates, function( rate ) {
  48. var search_text = _.toArray( rate ).join( ' ' ).toLowerCase();
  49. return ( -1 !== search_text.indexOf( search ) );
  50. } );
  51. }
  52. rates = _.sortBy( rates, function( rate ) {
  53. return parseInt( rate.tax_rate_order, 10 );
  54. } );
  55. return rates;
  56. },
  57. block: function() {
  58. $( '.wc_tax_rates' ).block({
  59. message: null,
  60. overlayCSS: {
  61. background: '#fff',
  62. opacity: 0.6
  63. }
  64. });
  65. },
  66. unblock: function() {
  67. $( '.wc_tax_rates' ).unblock();
  68. },
  69. save: function() {
  70. var self = this;
  71. self.block();
  72. Backbone.ajax({
  73. method: 'POST',
  74. dataType: 'json',
  75. url: ajaxurl + ( ajaxurl.indexOf( '?' ) > 0 ? '&' : '?' ) + 'action=woocommerce_tax_rates_save_changes',
  76. data: {
  77. current_class: data.current_class,
  78. wc_tax_nonce: data.wc_tax_nonce,
  79. changes: self.changes
  80. },
  81. success: function( response, textStatus ) {
  82. if ( 'success' === textStatus && response.success ) {
  83. WCTaxTableModelInstance.set( 'rates', response.data.rates );
  84. WCTaxTableModelInstance.trigger( 'change:rates' );
  85. WCTaxTableModelInstance.changes = {};
  86. WCTaxTableModelInstance.trigger( 'saved:rates' );
  87. // Reload view.
  88. WCTaxTableInstance.render();
  89. }
  90. self.unblock();
  91. }
  92. });
  93. }
  94. } ),
  95. WCTaxTableViewConstructor = Backbone.View.extend({
  96. rowTemplate: rowTemplate,
  97. per_page: data.limit,
  98. page: data.page,
  99. initialize: function() {
  100. var qty_pages = Math.ceil( _.toArray( this.model.get( 'rates' ) ).length / this.per_page );
  101. this.qty_pages = 0 === qty_pages ? 1 : qty_pages;
  102. this.page = this.sanitizePage( data.page );
  103. this.listenTo( this.model, 'change:rates', this.setUnloadConfirmation );
  104. this.listenTo( this.model, 'saved:rates', this.clearUnloadConfirmation );
  105. $tbody.on( 'change autocompletechange', ':input', { view: this }, this.updateModelOnChange );
  106. $search_field.on( 'keyup search', { view: this }, this.onSearchField );
  107. $pagination.on( 'click', 'a', { view: this }, this.onPageChange );
  108. $pagination.on( 'change', 'input', { view: this }, this.onPageChange );
  109. $( window ).on( 'beforeunload', { view: this }, this.unloadConfirmation );
  110. $submit.on( 'click', { view: this }, this.onSubmit );
  111. $save_button.prop( 'disabled', true );
  112. // Can bind these directly to the buttons, as they won't get overwritten.
  113. $table.find( '.insert' ).on( 'click', { view: this }, this.onAddNewRow );
  114. $table.find( '.remove_tax_rates' ).on( 'click', { view: this }, this.onDeleteRow );
  115. $table.find( '.export' ).on( 'click', { view: this }, this.onExport );
  116. },
  117. render: function() {
  118. var rates = this.model.getFilteredRates(),
  119. qty_rates = _.size( rates ),
  120. qty_pages = Math.ceil( qty_rates / this.per_page ),
  121. first_index = 0 === qty_rates ? 0 : this.per_page * ( this.page - 1 ),
  122. last_index = this.per_page * this.page,
  123. paged_rates = _.toArray( rates ).slice( first_index, last_index ),
  124. view = this;
  125. // Blank out the contents.
  126. this.$el.empty();
  127. if ( paged_rates.length ) {
  128. // Populate $tbody with the current page of results.
  129. $.each( paged_rates, function( id, rowData ) {
  130. view.$el.append( view.rowTemplate( rowData ) );
  131. } );
  132. } else {
  133. view.$el.append( rowTemplateEmpty() );
  134. }
  135. // Initialize autocomplete for countries.
  136. this.$el.find( 'td.country input' ).autocomplete({
  137. source: data.countries,
  138. minLength: 2
  139. });
  140. // Initialize autocomplete for states.
  141. this.$el.find( 'td.state input' ).autocomplete({
  142. source: data.states,
  143. minLength: 3
  144. });
  145. // Postcode and city don't have `name` values by default. They're only created if the contents changes, to save on database queries (I think)
  146. this.$el.find( 'td.postcode input, td.city input' ).change( function() {
  147. $( this ).attr( 'name', $( this ).data( 'name' ) );
  148. });
  149. if ( qty_pages > 1 ) {
  150. // We've now displayed our initial page, time to render the pagination box.
  151. $pagination.html( paginationTemplate( {
  152. qty_rates: qty_rates,
  153. current_page: this.page,
  154. qty_pages: qty_pages
  155. } ) );
  156. } else {
  157. $pagination.empty();
  158. view.page = 1;
  159. }
  160. },
  161. updateUrl: function() {
  162. if ( ! window.history.replaceState ) {
  163. return;
  164. }
  165. var url = data.base_url,
  166. search = $search_field.val();
  167. if ( 1 < this.page ) {
  168. url += '&p=' + encodeURIComponent( this.page );
  169. }
  170. if ( search.length ) {
  171. url += '&s=' + encodeURIComponent( search );
  172. }
  173. window.history.replaceState( {}, '', url );
  174. },
  175. onSubmit: function( event ) {
  176. event.data.view.model.save();
  177. event.preventDefault();
  178. },
  179. onAddNewRow: function( event ) {
  180. var view = event.data.view,
  181. model = view.model,
  182. rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ),
  183. changes = {},
  184. size = _.size( rates ),
  185. newRow = _.extend( {}, data.default_rate, {
  186. tax_rate_id: 'new-' + size + '-' + Date.now(),
  187. newRow: true
  188. } ),
  189. $current, current_id, current_order, rates_to_reorder, reordered_rates;
  190. $current = $tbody.children( '.current' );
  191. if ( $current.length ) {
  192. current_id = $current.last().data( 'id' );
  193. current_order = parseInt( rates[ current_id ].tax_rate_order, 10 );
  194. newRow.tax_rate_order = 1 + current_order;
  195. rates_to_reorder = _.filter( rates, function( rate ) {
  196. if ( parseInt( rate.tax_rate_order, 10 ) > current_order ) {
  197. return true;
  198. }
  199. return false;
  200. } );
  201. reordered_rates = _.map( rates_to_reorder, function( rate ) {
  202. rate.tax_rate_order++;
  203. changes[ rate.tax_rate_id ] = _.extend( changes[ rate.tax_rate_id ] || {}, { tax_rate_order : rate.tax_rate_order } );
  204. return rate;
  205. } );
  206. } else {
  207. newRow.tax_rate_order = 1 + _.max(
  208. _.pluck( rates, 'tax_rate_order' ),
  209. function ( val ) {
  210. // Cast them all to integers, because strings compare funky. Sighhh.
  211. return parseInt( val, 10 );
  212. }
  213. );
  214. // Move the last page
  215. view.page = view.qty_pages;
  216. }
  217. rates[ newRow.tax_rate_id ] = newRow;
  218. changes[ newRow.tax_rate_id ] = newRow;
  219. model.set( 'rates', rates );
  220. model.logChanges( changes );
  221. view.render();
  222. },
  223. onDeleteRow: function( event ) {
  224. var view = event.data.view,
  225. model = view.model,
  226. rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ),
  227. changes = {},
  228. $current, current_id;
  229. event.preventDefault();
  230. if ( $current = $tbody.children( '.current' ) ) {
  231. $current.each(function(){
  232. current_id = $( this ).data('id');
  233. delete rates[ current_id ];
  234. changes[ current_id ] = _.extend( changes[ current_id ] || {}, { deleted : 'deleted' } );
  235. });
  236. model.set( 'rates', rates );
  237. model.logChanges( changes );
  238. view.render();
  239. } else {
  240. window.alert( data.strings.no_rows_selected );
  241. }
  242. },
  243. onSearchField: function( event ){
  244. event.data.view.updateUrl();
  245. event.data.view.render();
  246. },
  247. onPageChange: function( event ) {
  248. var $target = $( event.currentTarget );
  249. event.preventDefault();
  250. event.data.view.page = $target.data( 'goto' ) ? $target.data( 'goto' ) : $target.val();
  251. event.data.view.render();
  252. event.data.view.updateUrl();
  253. },
  254. onExport: function( event ) {
  255. var csv_data = 'data:application/csv;charset=utf-8,' + data.strings.csv_data_cols.join(',') + '\n';
  256. $.each( event.data.view.model.getFilteredRates(), function( id, rowData ) {
  257. var row = '';
  258. row += rowData.tax_rate_country + ',';
  259. row += rowData.tax_rate_state + ',';
  260. row += ( rowData.postcode ? rowData.postcode.join( '; ' ) : '' ) + ',';
  261. row += ( rowData.city ? rowData.city.join( '; ' ) : '' ) + ',';
  262. row += rowData.tax_rate + ',';
  263. row += rowData.tax_rate_name + ',';
  264. row += rowData.tax_rate_priority + ',';
  265. row += rowData.tax_rate_compound + ',';
  266. row += rowData.tax_rate_shipping + ',';
  267. row += data.current_class;
  268. csv_data += row + '\n';
  269. });
  270. $( this ).attr( 'href', encodeURI( csv_data ) );
  271. return true;
  272. },
  273. setUnloadConfirmation: function() {
  274. this.needsUnloadConfirm = true;
  275. $save_button.prop( 'disabled', false );
  276. },
  277. clearUnloadConfirmation: function() {
  278. this.needsUnloadConfirm = false;
  279. $save_button.prop( 'disabled', true );
  280. },
  281. unloadConfirmation: function( event ) {
  282. if ( event.data.view.needsUnloadConfirm ) {
  283. event.returnValue = data.strings.unload_confirmation_msg;
  284. window.event.returnValue = data.strings.unload_confirmation_msg;
  285. return data.strings.unload_confirmation_msg;
  286. }
  287. },
  288. updateModelOnChange: function( event ) {
  289. var model = event.data.view.model,
  290. $target = $( event.target ),
  291. id = $target.closest( 'tr' ).data( 'id' ),
  292. attribute = $target.data( 'attribute' ),
  293. val = $target.val();
  294. if ( 'city' === attribute || 'postcode' === attribute ) {
  295. val = val.split( ';' );
  296. val = $.map( val, function( thing ) {
  297. return thing.trim();
  298. });
  299. }
  300. if ( 'tax_rate_compound' === attribute || 'tax_rate_shipping' === attribute ) {
  301. if ( $target.is( ':checked' ) ) {
  302. val = 1;
  303. } else {
  304. val = 0;
  305. }
  306. }
  307. model.setRateAttribute( id, attribute, val );
  308. },
  309. sanitizePage: function( page_num ) {
  310. page_num = parseInt( page_num, 10 );
  311. if ( page_num < 1 ) {
  312. page_num = 1;
  313. } else if ( page_num > this.qty_pages ) {
  314. page_num = this.qty_pages;
  315. }
  316. return page_num;
  317. }
  318. } ),
  319. WCTaxTableModelInstance = new WCTaxTableModelConstructor({
  320. rates: data.rates
  321. } ),
  322. WCTaxTableInstance = new WCTaxTableViewConstructor({
  323. model: WCTaxTableModelInstance,
  324. el: '#rates'
  325. } );
  326. WCTaxTableInstance.render();
  327. });
  328. })( jQuery, htmlSettingsTaxLocalizeScript, wp, ajaxurl );