PageSourceSearch

https://www.vieuxboucau-surflodge.com/wp-content/plugins/woocommerce/assets/js/frontend/cart.js?ver=10.7.0

js vieuxboucau-surflodge.com collected 2026-10-10 02:04:17 UTC 20,962 bytes, 822 lines download raw bytes

1/* global wc_cart_params */
2jQuery( 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
8	// Utility functions for the file.
9
10	/**
11	 * Gets a url for a given AJAX endpoint.
12	 *
13	 * @param {String} endpoint The AJAX Endpoint
14	 * @return {String} The URL to use for the request
15	 */
16	var get_url = function ( endpoint ) {
17		return wc_cart_params.wc_ajax_url
18			.toString()
19			.replace( '%%endpoint%%', endpoint );
20	};
21
22	/**
23	 * Check if a node is blocked for processing.
24	 *
25	 * @param {JQuery Object} $node
26	 * @return {bool} True if the DOM Element is UI Blocked, false if not.
27	 */
28	var is_blocked = function ( $node ) {
29		return (
30			$node.is( '.processing' ) || $node.parents( '.processing' ).length
31		);
32	};
33
34	/**
35	 * Block a node visually for processing.
36	 *
37	 * @param {JQuery Object} $node
38	 */
39	var block = function ( $node ) {
40		if ( ! is_blocked( $node ) ) {
41			$node.addClass( 'processing' ).block( {
42				message: null,
43				overlayCSS: {
44					background: '#fff',
45					opacity: 0.6,
46				},
47			} );
48		}
49	};
50
51	/**
52	 * Unblock a node after processing is complete.
53	 *
54	 * @param {JQuery Object} $node
55	 */
56	var unblock = function ( $node ) {
57		$node.removeClass( 'processing' ).unblock();
58	};
59
60	/**
61	 * Removes duplicate notices.
62	 *
63	 * @param {JQuery Object} $notices
64	 */
65	var remove_duplicate_notices = function ( $notices ) {
66		var seen = new Set();
67		var deduplicated_notices = [];
68
69		$notices.each( function () {
70			const text = $( this ).text();
71
72			if ( ! seen.has( text ) ) {
73				seen.add( text );
74				deduplicated_notices.push( this );
75			}
76		} );
77
78		return $( deduplicated_notices );
79	};
80
81	/**
82	 * Update the .woocommerce div with a string of html.
83	 *
84	 * @param {String} html_str The HTML string with which to replace the div.
85	 * @param {bool} preserve_notices Should notices be kept? False by default.
86	 */
87	var update_wc_div = function ( html_str, preserve_notices ) {
88		var $html = $.parseHTML( html_str );
89		var $new_form = $( '.woocommerce-cart-form', $html );
90		var $new_totals = $( '.cart_totals', $html );
91		var $notices = remove_duplicate_notices(
92			$(
93				'.woocommerce-error, .woocommerce-message, .woocommerce-info, .is-error, .is-info, .is-success',
94				$html
95			)
96		);
97
98		// No form, cannot do this.
99		if ( $( '.woocommerce-cart-form' ).length === 0 ) {
100			window.location.reload();
101			return;
102		}
103
104		// Remove errors
105		if ( ! preserve_notices ) {
106			$(
107				'.woocommerce-error, .woocommerce-message, .woocommerce-info, .is-error, .is-info, .is-success, .coupon-error-notice'
108			).remove();
109		}
110
111		if ( $new_form.length === 0 ) {
112			// If the checkout is also displayed on this page, trigger reload instead.
113			if ( $( '.woocommerce-checkout' ).length ) {
114				window.location.reload();
115				return;
116			}
117
118			// No items to display now! Replace all cart content.
119			var $cart_html = $( '.wc-empty-cart-message', $html ).closest(
120				'.woocommerce'
121			);
122			$( '.woocommerce-cart-form__contents' )
123				.closest( '.woocommerce' )
124				.replaceWith( $cart_html );
125
126			// Display errors
127			if ( $notices.length > 0 ) {
128				show_notice( $notices );
129			}
130
131			// Notify plugins that the cart was emptied.
132			$( document.body ).trigger( 'wc_cart_emptied' );
133		} else {
134			// If the checkout is also displayed on this page, trigger update event.
135			if ( $( '.woocommerce-checkout' ).length ) {
136				$( document.body ).trigger( 'update_checkout' );
137			}
138
139			// Store the old coupon error message and value before the
140			// .woocommerce-cart-form is replaced with the new form.
141			var $old_coupon_field_val = $( '#coupon_code' ).val();
142			var $old_coupon_error_msg = $( '#coupon_code' )
143					.closest( '.coupon' )
144					.find( '.coupon-error-notice' );
145
146			$( '.woocommerce-cart-form' ).replaceWith( $new_form );
147			$( '.woocommerce-cart-form' )
148				.find( ':input[name="update_cart"]' )
149				.prop( 'disabled', true );
150
151			if ( preserve_notices && $old_coupon_error_msg.length > 0 ) {
152				var $new_coupon_field = $( '.woocommerce-cart-form' ).find( '#coupon_code' );
153				var $new_coupon_field_wrapper = $new_coupon_field.closest( '.coupon' );
154
155				$new_coupon_field.val( $old_coupon_field_val );
156				// The coupon input with error needs to be focused before adding the live region
157				// with the error message, otherwise the screen reader won't read it.
158				$new_coupon_field.focus();
159				show_coupon_error( $old_coupon_error_msg, $new_coupon_field_wrapper, true );
160			}
161
162			if ( $notices.length > 0 ) {
163				show_notice( $notices );
164			}
165
166			update_cart_totals_div( $new_totals );
167		}
168
169		$( document.body ).trigger( 'updated_wc_div' );
170	};
171
172	/**
173	 * Update the .cart_totals div with a string of html.
174	 *
175	 * @param {String} html_str The HTML string with which to replace the div.
176	 */
177	var update_cart_totals_div = function ( html_str ) {
178		$( '.cart_totals' ).replaceWith( html_str );
179		$( document.body ).trigger( 'updated_cart_totals' );
180	};
181
182	/**
183	 * Shows new notices on the page.
184	 *
185	 * @param {Object} The Notice HTML Element in string or object form.
186	 */
187	var show_notice = function ( html_element, $target ) {
188		if ( ! $target ) {
189			$target =
190				$( '.woocommerce-notices-wrapper:first' ) ||
191				$( '.wc-empty-cart-message' ).closest( '.woocommerce' ) ||
192				$( '.woocommerce-cart-form' );
193		}
194		$target.prepend( html_element );
195	};
196
197	/**
198	 * Shows coupon form errors.
199	 *
200	 * @param {string|object}
200 html_element The HTML string response after applying an invalid coupon or a jQuery element.
201	 * @param {Object} $target Coupon field wrapper jQuery element.
202	 * @param {boolean} is_live_region Whether role="alert" should be added or not.
203	 */
204	var show_coupon_error = function ( html_element, $target, is_live_region ) {
205		if ( $target.length === 0 ) {
206			return;
207		}
208
209		var $coupon_error_el = html_element;
210
211		if ( typeof html_element === 'string' ) {
212			var msg = $( $.parseHTML( html_element ) ).text().trim();
213
214			if ( msg === '' ) {
215				return;
216			}
217
218			$coupon_error_el = $('<p>', {
219				class: 'coupon-error-notice',
220				id: 'coupon-error-notice',
221				text: msg
222			});
223		}
224
225		if ( is_live_region ) {
226			$coupon_error_el.attr( 'role', 'alert' );
227		}
228
229		$target.find( '#coupon_code' )
230			.addClass( 'has-error' )
231			.attr( 'aria-invalid', 'true' )
232			.attr( 'aria-describedby', 'coupon-error-notice' );
233		$target.append( $coupon_error_el );
234	};
235
236	/**
237	 * Object to handle AJAX calls for cart shipping changes.
238	 */
239	var cart_shipping = {
240		/**
241		 * Initialize event handlers and UI state.
242		 */
243		init: function ( cart ) {
244			this.cart = cart;
245			this.toggle_shipping = this.toggle_shipping.bind( this );
246			this.shipping_method_selected =
247				this.shipping_method_selected.bind( this );
248			this.shipping_calculator_submit =
249				this.shipping_calculator_submit.bind( this );
250
251			$( document ).on(
252				'click',
253				'.shipping-calculator-button',
254				this.toggle_shipping
255			);
256			$( document ).on(
257				'change',
258				'select.shipping_method, :input[name^=shipping_method]',
259				this.shipping_method_selected
260			);
261			$( document ).on(
262				'submit',
263				'form.woocommerce-shipping-calculator',
264				this.shipping_calculator_submit
265			);
266
267			$( '.shipping-calculator-form' ).hide();
268		},
269
270		/**
271		 * Toggle Shipping Calculator panel
272		 */
273		toggle_shipping: function ( event ) {
274			var $target = $( event.currentTarget );
275
276			$( '.shipping-calculator-form' ).slideToggle( 'slow', function () {
277				var self = this;
278
279				setTimeout( function () {
280					var $form = $( self );
281
282					$target.attr( 'aria-expanded', $form.is( ':visible' ) ? 'true' : 'false' );
283				}, 0 );
284			} );
285
286			$( 'select.country_to_state, input.country_to_state' ).trigger(
287				'change'
288			);
289			$( document.body ).trigger( 'country_to_state_changed' ); // Trigger select2 to load.
290			return false;
291		},
292
293		/**
294		 * Handles when a shipping method is selected.
295		 */
296		shipping_method_selected: function ( event ) {
297			var shipping_methods = {};
298
299			// eslint-disable-next-line max-len
300			$(
301				'select.shipping_method, :input[name^=shipping_method][type=radio]:checked, :input[name^=shipping_method][type=hidden]'
302			).each( function () {
303				shipping_methods[ $( this ).data( 'index' ) ] = $( this ).val();
304			} );
305
306			block( $( 'div.cart_totals' ) );
307
308			var data = {
309				security: wc_cart_params.update_shipping_method_nonce,
310				shipping_method: shipping_methods,
311			};
312
313			$.ajax( {
314				type: 'post',
315				url: get_url( 'update_shipping_method' ),
316				data: data,
317				dataType: 'html',
318				success: function ( response ) {
319					update_cart_totals_div( response );
320
321					var newCurrentTarget = document.getElementById( event.currentTarget.id );
322
323					if ( newCurrentTarget ) {
324						newCurrentTarget.focus();
325					}
326				},
327				complete: function () {
328					unblock( $( 'div.cart_totals' ) );
329					$( document.body ).trigger( 'updated_shipping_method' );
330				},
331			} );
332		},
333
334		/**
335		 * Handles a shipping calculator form submit.
336		 *
337		 * @param {Object} evt The JQuery event.
338		 */
339		shipping_calculator_submit: function ( evt ) {
340			evt.preventDefault();
341
342			var $form = $( evt.currentTarget );
343
344			block( $( 'div.cart_totals' ) );
345			block( $form );
346
347			// Provide the submit button value because wc-form-handler expects it.
348			$( '<input />' )
349				.attr( 'type', 'hidden' )
350				.attr( 'name', 'calc_shipping' )
351				.attr( 'value', 'x' )
352				.appendTo( $form );
353
354			// Make call to actual form post URL.
355			$.ajax( {
356				type: $form.attr( 'method' ),
357				url: $form.attr( 'action' ),
358				data: $form.serialize(),
359				dataType: 'html',
360				success: function ( response ) {
361					update_wc_div( response );
362				},
363				complete: function () {
364					unblock( $form );
365					unblock( $( 'div.cart_totals' ) );
366				},
367			} );
368		},
369	};
370
371	/**
372	 * Object to handle cart UI.
373	 */
374	var cart = {
375		/**
376		 * Initialize cart UI events.
377		 */
378		init: function () {
379			this.update_cart_totals = this.update_cart_totals.bind( this );
380			this.input_keypress = this.input_keypress.bind( this );
381			this.cart_submit = this.cart_submit.bind( this );
382			this.submit_click = this.submit_click.bind( this );
383			this.apply_coupon = this.apply_coupon.bind( this );
384			this.remove_coupon_clicked =
385				this.remove_coupon_clicked.bind( this );
386			this.remove_coupon_error = this.remove_coupon_error.bind( this );
387			this.quantity_update = this.quantity_update.bind( this );
388			this.item_remove_clicked = this.item_remove_clicked.bind( this );
389			this.item_restore_clicked = this.item_restore_clicked.bind( this );
390			this.update_cart = this.update_cart.bind( this );
391
392			$( document ).on( 'wc_update_cart added_to_cart', function () {
393				cart.update_cart.apply( cart, [].slice.call( arguments, 1 ) );
394			} );
395			$( document ).on(
396				'click',
397				'.woocommerce-cart-form :input[type=submit]',
398				this.submit_click
399			);
400			$( document ).on(
401				'keypress',
402				'.woocommerce-cart-form :input[type=number]',
403				this.input_keypress
404			);
405			$( document ).on(
406				'submit',
407				'.woocommerce-cart-form',
408				this.cart_submit
409			);
410			$( document ).on(
411				'click',
412				'a.woocommerce-remove-coupon',
413				this.remove_coupon_clicked
414			);
415			$( document ).on(
416				'keydown',
417				'a.woocommerce-remove-coupon',
418				this.on_keydown_remove_coupon
419			);
420			$( document ).on(
421				'click',
422				'.woocommerce-cart-form .product-remove > a',
423				this.item_remove_clicked
424			);
425			$( document ).on(
426				'keydown',
427				'.woocommerce-cart-form .product-remove > a',
428				this.on_keydown_remove_item
429			);
430			$( document ).on(
431				'click',
432				'.woocommerce-cart .restore-item',
433				this.item_restore_clicked
434			);
435			$( document ).on(
436				'change input',
437				'.woocommerce-cart-form .cart_item :input',
438				this.input_changed
439			);
440			$( document ).on(
441				'change input',
442				'#coupon_code',
443				this.remove_coupon_error
444			);
445
446			$( '.woocommerce-cart-form :input[name="update_cart"]' ).prop(
447				'disabled',
448				true
449			);
450		},
451
452		/**
453		 * After an input is changed, enable the update cart button.
454		 */
455		input_changed: function () {
456			$( '.woocommerce-cart-form :input[name="update_cart"]' ).prop(
457				'disabled',
458				false
459			);
460		},
461
462		/**
463		 * Update entire cart via ajax.
464		 */
465		update_cart: function ( preserve_notices ) {
466			var $form = $( '.woocommerce-cart-form' );
467
468			block( $form );
469			block( $( 'div.cart_totals' ) );
470
471			// Make call to actual form post URL.
472			$.ajax( {
473				type: $form.attr( 'method' ),
474				url: $form.attr( 'action' ),
475				data: $form.serialize(),
476				dataType: 'html',
477				success: function ( response ) {
478					update_wc_div( response, preserve_notices );
479				},
480				complete: function () {
481					unblock( $form );
482					unblock( $( 'div.cart_totals' ) );
483					$.scroll_to_notices( $( '[role="alert"]' ) );
484				},
485			} );
486		},
487
488		/**
489		 * Update the cart after something has changed.
490		 */
491		update_cart_totals: function () {
492			block( $( 'div.cart_totals' ) );
493
494			$.ajax( {
495				url: get_url( 'get_cart_totals' ),
496				dataType: 'html',
497				success: function ( response ) {
498					update_cart_totals_div( response );
499				},
500				complete: function () {
501					unblock( $( 'div.cart_totals' ) );
502				},
503			} );
504		},
505
506		/**
507		 * Handle the <ENTER> key for quantity fields.
508		 *
509		 * @param {Object} evt The JQuery event
510		 *
511		 * For IE, if you hit enter on a quantity field, it makes the
512		 * document.activeElement the first submit button it finds.
513		 * For us, that is the Apply Coupon button. This is required
514		 * to catch the event before that happens.
515		 */
516		input_keypress: function ( evt ) {
517			// Catch the enter key and don't let it submit the form.
518			if ( 13 === evt.keyCode ) {
519				var $form = $( evt.currentTarget ).parents( 'form' );
520
521				try {
522					// If there are no validation errors, handle the submit.
523					if ( $form[ 0 ].checkValidity() ) {
524						evt.preventDefault();
525						this.cart_submit( evt );
526					}
527				} catch ( err ) {
528					evt.preventDefault();
529					this.cart_submit( evt );
530				}
531			}
532		},
533
534		/**
535		 * Handle cart form submit and route to correct logic.
536		 *
537		 * @param {Object} evt The JQuery event
538		 */
539		cart_submit: function ( evt ) {
540			var $submit = $( document.activeElement ),
541				$clicked = $( ':input[type=submit][clicked=true]' ),
542				$form = $( evt.currentTarget );
543
544			// For submit events, currentTarget is form.
545			// For keypress events, currentTarget is input.
546			if ( ! $form.is( 'form' ) ) {
547				$form = $( evt.currentTarget ).parents( 'form' );
548			}
549
550			if (
551				0 === $form.find( '.woocommerce-cart-form__contents' ).length
552			) {
553				return;
554			}
555
556			if ( is_blocked( $form ) ) {
557				return false;
558			}
559
560			if (
561				$clicked.is( ':input[name="update_cart"]' ) ||
562				$submit.is( 'input.qty' )
563			) {
564				evt.preventDefault();
565				this.quantity_update( $form );
566			} else if (
567				$clicked.is( ':input[name="apply_coupon"]' ) ||
568				$submit.is( '#coupon_code' )
569			) {
570				evt.preventDefault();
571				this.apply_coupon( $form );
572			}
573		},
574
575		/**
576		 * Special handling to identify which submit button was clicked.
577		 *
578		 * @param {Object} evt The JQuery event
579		 */
580		submit_click: function ( evt ) {
581			$(
582				':input[type=submit]',
583				$( evt.target ).parents( 'form' )
584			).removeAttr( 'clicked' );
585			$( evt.target ).attr( 'clicked', 'true' );
586		},
587
588		/**
589		 * Apply Coupon code
590		 *
591		 * @param {JQuery Object} $form The cart form.
592		 */
593		apply_coupon: function ( $form ) {
594			block( $form );
595
596			var cart = this;
597			var $text_field = $( '#coupon_code' );
598			var coupon_code = $text_field.val();
599
600			var data = {
601				security: wc_cart_params.apply_coupon_nonce,
602				coupon_code: coupon_code,
603			};
604
605			$.ajax( {
606				type: 'POST',
607				url: get_url( 'apply_coupon' ),
608				data: data,
609				dataType: 'html',
610				success: function ( response ) {
611					$(
612						'.woocommerce-error, .woocommerce-message, .woocommerce-info, ' +
613						'.is-error, .is-info, .is-success, .coupon-error-notice'
614					).remove();
615
616					// We only want to show coupon notices if they are not errors.
617					// Coupon errors are shown under the input.
618					if ( response.indexOf( 'woocommerce-error' ) === -1 && response.indexOf( 'is-error' ) === -1 ) {
619						show_notice( response );
620					} else {
621						var $coupon_wrapper = $text_field.closest( '.coupon' );
622
623						if ( $coupon_wrapper.length > 0 ) {
624							show_coupon_error( response, $coupon_wrapper, false );
625						}
626					}
627
628					$( document.body ).trigger( 'applied_coupon', [
629						coupon_code,
630					] );
631				},
632				complete: function () {
633					unblock( $form );
634					cart.update_cart( true );
635				},
636			} );
637		},
638
639		/**
640		 * Handle when a remove coupon link is clicked.
641		 *
642		 * @param {Object} evt The JQuery event
643		 */
644		remove_coupon_clicked: function ( evt ) {
645			evt.preventDefault();
646
647			var cart = this;
648			var $wrapper = $( evt.currentTarget ).closest( '.cart_totals' );
649			var coupon = $( evt.currentTarget ).attr( 'data-coupon' );
650
651			block( $wrapper );
652
653			var data = {
654				security: wc_cart_params.remove_coupon_nonce,
655				coupon: coupon,
656			};
657
658			$.ajax( {
659				type: 'POST',
660				url: get_url( 'remove_coupon' ),
661				data: data,
662				dataType: 'html',
663				success: function ( response ) {
664					$(
665						'.woocommerce-error, .woocommerce-message, .woocommerce-info, .is-error, .is-info, .is-success'
666					).remove();
667					show_notice( response );
668					$( document.body ).trigger( 'removed_coupon', [ coupon ] );
669					$( '#coupon_code' )
670						.val('')
671						.removeClass('has-error')
672						.removeAttr('aria-invalid')
673						.removeAttr('aria-describedby')
674						.closest('.coupon')
675						.find('.coupon-error-notice')
676						.remove();
677					unblock( $wrapper );
678				},
679				complete: function () {
680					cart.update_cart( true );
681				},
682			} );
683		},
684
685		/**
686		 * Handle when pressing the Space key on the remove coupon link.
687		 * This is necessary because the link got the role="button" attribute
688		 * and needs to act like a button.
689		 *
690		 * @param {Object} evt The JQuery event
691		 */
692		on_keydown_remove_coupon: function ( evt ) {
693			if ( evt.key === ' ' ) {
694				evt.preventDefault();
695				$( evt.currentTarget ).trigger( 'click' );
696			}
697		},
698
699		/**
700		 * Handle when the coupon input loses focus.
701		 *
702		 * @param {Object} evt The JQuery event
703		 */
704		remove_coupon_error: function ( evt ) {
705			$( evt.currentTarget )
706				.removeClass( 'has-error' )
707				.removeAttr( 'aria-invalid' )
708				.removeAttr( 'aria-describedby' )
709				.closest( '.coupon' )
710				.find( '.coupon-error-notice' )
711				.remove();
712		},
713
714		/**
715		 * Handle a cart Quantity Update
716		 *
717		 * @param {JQuery Object} $form The cart form.
718		 */
719		quantity_update: function ( $form ) {
720			block( $form );
721			block( $( 'div.cart_totals' ) );
722
723			// Provide the submit button value because wc-form-handler expects it.
724			$( '<input />' )
725				.attr( 'type', 'hidden' )
726				.attr( 'name', 'update_cart' )
727				.attr( 'value', 'Update Cart' )
728				.appendTo( $form );
729
730			// Make call to actual form post URL.
731			$.ajax( {
732				type: $form.attr( 'method' ),
733				url: $form.attr( 'action' ),
734				data: $form.serialize(),
735				dataType: 'html',
736				success: function ( response ) {
737					update_wc_div( response );
738				},
739				complete: function () {
740					unblock( $form );
741					unblock( $( 'div.cart_totals' ) );
742					$.scroll_to_notices( $( '[role="alert"]' ) );
743				},
744			} );
745		},
746
747		/**
748		 * Handle when a remove item link is clicked.
749		 *
750		 * @param {Object} evt The JQuery event
751		 */
752		item_remove_clicked: function ( evt ) {
753			evt.preventDefault();
754
755			var $a = $( evt.currentTarget );
756			var $form = $a.parents( 'form' );
757
758			block( $form );
759			block( $( 'div.cart_totals' ) );
760
761			$.ajax( {
762				type: 'GET',
763				url: $a.attr( 'href' ),
764				dataType: 'html',
765				success: function ( response ) {
766					update_wc_div( response );
767				},
768				complete: function () {
769					unblock( $form );
770					unblock( $( 'div.cart_totals' ) );
771					$.scroll_to_notices( $( '[role="alert"]' ) );
772					$( document.body ).trigger( 'item_removed_from_classic_cart');
773				},
774			} );
775		},
776
777		/**
778		 * Handle when pressing the Space key on the remove item link.
779		 * This is necessary because the link got the role="button" attribute
780		 * and needs to act like a button.
781		 *
782		 * @param {Object} evt The JQuery event
783		 */
784		on_keydown_remove_item: function ( event ) {
785			if ( event.key === ' ' ) {
786				event.preventDefault();
787				$( event.currentTarget ).trigger( 'click' );
788			}
789		},
790
791		/**
792		 * Handle when a restore item link is clicked.
793		 *
794		 * @param {Object} evt The JQuery event
795		 */
796		item_restore_clicked: function ( evt ) {
797			evt.preventDefault();
798
799			var $a = $( evt.currentTarget );
800			var $form = $( 'form.woocommerce-cart-form' );
801
802			block( $form );
803			block( $( 'div.cart_totals' ) );
804
805			$.ajax( {
806				type: 'GET',
807				url: $a.attr( 'href' ),
808				dataType: 'html',
809				success: function ( response ) {
810					update_wc_div( response );
811				},
812				complete: function () {
813					unblock( $form );
814					unblock( $( 'div.cart_totals' ) );
815				},
816			} );
817		},
818	};
819
820	cart_shipping.init( cart );
821	cart.init();
822} );

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.