1(function($, window, document, undefined) { 2 3 function Feature(element, options) { 4 //Current options set by the caller including defaults. 5 this.options = $.extend({}, Feature.Defaults, options); 6 this.$element = $(element); 7 this.id = $(element)[0].id; 8 this.data = {}; 9 10 this._handlers = {}; 11 this._states = { 12 current: {}, 13 tags: { 14 'initializing': [ 'busy' ], 15 'animating': [ 'busy' ], 16 'dragging': [ 'interacting' ] 17 } 18 }; 19 //not used - was trying a different way of capturing resize event 20 // $.each([ 'onResize', 'onThrottledResize' ], $.proxy(function(i, handler) { 21 // this._handlers[handler] = $.proxy(this[handler], this); 22 // }, this)); 23 } 24 25 26 Feature.Defaults = { 27 scriptPath: "services/eventsservice.asmx" 28 , currentWidgetModuleItemID: 0 29 , detectIpad: false 30 , autoScroll: 0 31 , mode: 'h' 32 , arrowPagination: true 33 , textPagination: false 34 , iconPagination: false 35 , allEventsLink: true 36 , allEventsLinkText: 'ALL EVENTS >' 37 , hasImages: true 38 , imageBackground: false 39 , lazyLoadImages: true 40 , buyNowButton: {visible: false, text: 'BUY NOW'} 41 , moreButton: {visible: false, text: 'MORE >'} 42 , slideSpeed: 600 43 , easing: 'linear' 44 , maxItemsVisible: 0 45 , useCropbox: false 46 , multiFeatureLoaded: function(){} 47 , isResponsive: true 48 , responsiveRefreshRate : 200 49 , isSwipeable: true 50 , hasDescription: true 51 , textContainer: false 52 , link: 'all' 53 , attributeOrder: { 54 1: 'image', 55 2: 'date', 56 3: 'title', 57 4: 'description', 58 5: 'buttons' 59 } 60 // , hasButtonContainer: false 61 }; 62 63 64 // 65 // Feature.Type = { 66 // Event: 'event', 67 // State: 'state' 68 // }; 69 70 // Initiates the Multifeature 71 Feature.prototype.initialize = function() { 72 // this.enter('initializing'); 73 // this.trigger('initialize'); 74 //console.log("initializing " + this.id); 75 if (this.data.length > 0) { 76 77 $('body').addClass(this.id+'Showing'); 78 79 this.setup(); //Was populateWidget() 80 81 this.buildContainer(); 82 83 if (this.options.hasPanels) { 84 this.paginateWidget(); 85 86 this.addControls(); 87 88 if (this.options.autoScroll > 0) { 89 var schedInterval = setInterval(function() { scheduleScroll() }, this.options.autoScroll), 90 target = this; 91 92 function scheduleScroll() { 93 $(target.$element.selector + ' .multiFeatureArrowRight').click(); 94 } 95 96 $(target.$element.selector).mouseenter(function() { 97 clearInterval(schedInterval); 98 }); 99 100 $(target.$element.selector).mouseleave(function() { 101 schedInterval = setInterval(function() { scheduleScroll() }, target.options.autoScroll); 102 }); 103 } 104 } 105 106 } else { 107 $('body').addClass(this.id+'NotShowing'); 108 this.$element.remove(); 109 } 110 111 if(this.options.isSwipeable){ 112 var parentObj = this; 113 $(this.$element.selector + ' .multiFeatureContent').on("swipeleft",function(){ 114 $(parentObj.$element.selector + ' .multiFeatureArrowRight').click(); 115 }); 116 117 $(this.$element.selector + ' .multiFeatureContent').on("swiperight",function(){ 118 $(parentObj.$element.selector + ' .multiFeatureArrowLeft').click(); 119 }); 120 } 121 122 this.options.multiFeatureLoaded(this.data); // callback if needed for the constructor 123 124 var observer = lozad('.lozad', { 125 rootMargin: '200px 0px', // syntax similar to that of CSS Margin 126 threshold: 0.1 // ratio of element convergence 127 }); 128 observer.observe(); 129 130 131 // this.leave('initializing'); 132 // this.trigger('initialized'); 133 }; 134 135 /** 136 * Setups the current settings. 137 * @todo Remove responsive classes. Why should adaptive designs be brought into IE8? 138 * @todo Support for media queries by using `matchMedia` would be nice. 139 * @public 140 */ 141 Feature.prototype.setup = function() { 142 var $widgetContainer = this.$element.find('.multiFeatureContent'), 143 totalItems; 144 145 if (this.options.maxItemsVisible > 0) { 146 (this.options.maxItemsVisible <= this.data.length) ? totalItems = this.options.maxItemsVisible : totalItems = this.data.length; 147 } else { 148 totalItems = this.data.length; 149 } 150 151 for (i = 0; i < totalItems; i++) { 152 var featureAnchor = '', 153 purchaseAnchor = '', 154 purchaseLinkText = '', 155 purchaseClass = '', 156 imageContainer = '', 157 hasLink = this.data[i].URL != null, 158 featureItemDate = '', 159 featureTemplate = '', 160 elements = { 161 'image': '', 162 'date': '', 163 'title': '', 164 'description': '', 165 'buttons': '' 166 }; 167 168 // Build image container if hasImages is true 169 if(this.options.hasImages) { 170 imageContainer = '<div class="featureItemImage"></div>'; 171 } 172 173 if(this.data[i].Date != null && this.data[i].Date != undefined){ 174 featureItemDate = (this.data[i].Date != null) ? this.data[i].Date:''; 175 } 176 177 // Build image HTML 178 if ((this.options.link == 'image' || this.options.link == 'imageAndText') && hasLink) { 179 elements.image = '<a href="' + this.data[i].URL + '" class="image-link">' + imageContainer + '</a>'; 180 } else { 181 elements.image = imageContainer; 182 } 183 184 // Build Date HTML 185 if ((this.options.link == 'text' || this.options.link == 'imageAndText') && hasLink) { 186 elements.date = '<a href="' + this.data[i].URL + '" class="date-link"><span class="featureItemDate">'+ featureItemDate +'</span></a>'; 187 } else { 188 elements.date = '<span class="featureItemDate">'+ featureItemDate +'</span>'; 189 } 190 191 // Build Title HTML 192 if ((this.options.link == 'text' || this.options.link == 'imageAndText') && hasLink) { 193 elements.title = '<a href="' + this.data[i].URL + '" class="date-link"><span class="featureItemTitle">' + this.data[i].Name + '</span></a>'; 194 } else { 195 elements.title = '<span class="featureItemTitle">' + this.data[i].Name + '</span>'; 196 } 197 198 // Build description HTML 199 if (this.data[i].Description != null) { 200 elements.description = '<span class="featureItemDescription">' + this.data[i].Description + '</span>'; 201 } else { 202 elements.description = '<span class="featureItemDescription"></span>'; 203 } 204 205 // Build button HTML 206 if (this.data[i].URL != null && (this.options.moreButton.visible || this.options.buyNowButton.visible)) { 207 var buyNowButton = this.options.buyNowButton.visible ? '<a href="'+ this.data[i].URL +'"
207class="button button1 buy-now-button">'+ this.options.buyNowButton.text +'</a>' : '', 208 moreButton = this.options.moreButton.visible ? '<a href="'+ this.data[i].URL +'" class="button button1 more-button">'+ this.options.moreButton.text +'</a>' : ''; 209 elements.buttons = '<div class="button-container">' + buyNowButton + moreButton +'</div>'; 210 } 211 212 // combine all of the elements in the order called for 213 var contentStr = elements[this.options.attributeOrder[1]] 214 + elements[this.options.attributeOrder[2]] 215 + elements[this.options.attributeOrder[3]] 216 + elements[this.options.attributeOrder[4]] 217 + elements[this.options.attributeOrder[5]]; 218 219 if (this.options.link == 'all') { 220 $widgetContainer.append( 221 '<a href="' + this.data[i].URL + '" class="featureItem '+this.id+'FeatureItem" id="'+this.id+'featureItem'+i+'"></a>' 222 ); 223 $('#'+this.id+'featureItem'+i).append(contentStr); 224 } else { 225 $widgetContainer.append( 226 '<div class="featureItem '+this.id+'FeatureItem" id="'+this.id+'featureItem'+i+'"></div>' 227 ); 228 $('#'+this.id+'featureItem'+i).append(contentStr); 229 } 230 231 if (this.options.hasImages) { 232 var cropbox = '', 233 noImageURL = SITEBASEURL + "local/" + SITETHEME + "/App_Themes/" + SITETHEME + "/images/no_image_available.jpg", 234 imageMaxWidth = parseInt(this.$element.find('.featureItemImage').css('width')), 235 imageMaxHeight = parseInt(this.$element.find('.featureItemImage').css('height')), 236 imageDimensions = [imageMaxWidth, imageMaxHeight], // the dimensions of the image container 237 238 imgWidthConversion = imageDimensions[0]/this.data[i].ImageWidth, 239 imgHeightConversion = imageDimensions[1]/this.data[i].ImageHeight, 240 241 checkHeight = Math.floor(this.data[i].ImageHeight*imgWidthConversion), 242 checkWidth = Math.floor(this.data[i].ImageWidth*imgHeightConversion), 243 sizeSetting = ''; 244 245 if (!this.options.useCropbox ){ 246 if(checkHeight >= imageDimensions[1]){ 247 sizeSetting = '&w='+imageDimensions[0]; 248 } else if(checkWidth >= imageDimensions[0]){ 249 sizeSetting = '&h='+imageDimensions[1]; 250 } 251 } else { 252 sizeSetting = '&w='+imageDimensions[0]+'&h='+imageDimensions[1] 253 } 254 255 if (this.options.useCropbox) { 256 cropbox = '&cropbox=1&cropboxhpos=center&stf=1' 257 } 258 259 260 261 if (this.data[i].ImageURL != '') { 262 263 var imageAlternateText = this.data[i].Name; 264 265 if (this.data[i].ImageAlternateText !== "") { 266 imageAlternateText = this.data[i].ImageAlternateText; 267 } 268 269 var imgPath = this.data[i].ImageURL; 270 var cleanImgPath = imgPath.replace(/(?=[() ])/g, '\\'); 271 272 if (this.options.imageBackground) { 273 if (this.options.lazyLoadImages) { 274 $(this.$element.selector + 'featureItem'+ i +' .featureItemImage').addClass('lozad'); 275 $(this.$element.selector + 'featureItem'+ i +' .featureItemImage').attr('data-background-image', cleanImgPath+sizeSetting+cropbox); 276 } else { 277 $(this.$element.selector + 'featureItem'+ i +' .featureItemImage').css('background-image', 'url("'+ this.data[i].ImageURL+sizeSetting+cropbox +'")'); 278 } 279 } else { 280 if (this.options.lazyLoadImages) { 281 282 $(this.$element.selector + 'featureItem' + i + ' .featureItemImage').append('<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" data-src="' + imgPath + sizeSetting + cropbox + '" class="lozad" alt="' + imageAlternateText + '" />'); 283 } else { 284 $(this.$element.selector + 'featureItem' + i + ' .featureItemImage').append('<img src="' + this.data[i].ImageURL + sizeSetting + cropbox + '" alt="' + imageAlternateText + '" />'); 285 } 286 } 287 } else { 288 // note: will only pull in no-image-available image if it exists in the theme's local images folder
289 if (this.options.imageBackground) { 290 if (this.options.lazyLoadImages) { 291 $(this.$element.selector + 'featureItem'+ i +' .featureItemImage').addClass('lozad'); 292 $(this.$element.selector + 'featureItem'+ i +' .featureItemImage').attr('data-background-image', noImageURL+sizeSetting+cropbox); 293 } else { 294 $(this.$element.selector + 'featureItem'+ i +' .featureItemImage').css('background-image', 'url("'+ noImageURL+sizeSetting+cropbox +'")'); 295 } 296 } else { 297 if (this.options.lazyLoadImages) { 298 $(this.$element.selector + 'featureItem' + i + ' .featureItemImage').append('<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" data-src="' + noImageURL + sizeSetting + cropbox + '" alt="' + imageAlternateText + '" />'); 299 } else { 300 $(this.$element.selector + 'featureItem' + i + ' .featureItemImage').append('<img src="' + noImageURL + sizeSetting + cropbox + '" alt="' + imageAlternateText + '" />'); 301 } 302 } 303 } 304 305 if (this.options.textContainer) { 306 $(this.$element.selector + 'featureItem'+ i +' span').wrapAll('<div class="text-container"></div>'); 307 } 308 309 } 310 311 } 312 313 if (this.options.allEventsLink) { 314 $widgetContainer.append('<a href="' + SITEBASEURL + 'events" class="all-events-btn">' + this.options.allEventsLinkText + '</a>'); 315 } 316 317 }; 318 319 // Positions panels for pagination 320 Feature.prototype.paginateWidget = function() { 321 var setLeft = 0, 322 containerWidth = parseInt($(this.$element.selector + ' .multiFeatureContent').css('width')); 323 $(this.$element.selector + ' .multiFeatureItemPanel').each(function(){ 324 $(this).css('left', setLeft+'px'); 325 setLeft = setLeft+containerWidth; 326 }); 327 }; 328 329 330 /** 331 * Builds the container and determines how many items fit in a panel. 332 * @todo Need to revisit buildContainer, it works but could be much more efficent. 333 * @public 334 */ 335 Feature.prototype.buildContainer = function() { 336 var parentObj = this; 337 338 if(parentObj.options.isResponsive){ 339 this.panelLogic(this); 340 } else { 341 parentObj.options.hasPanels = false; 342 } 343 }; 344 345 // Sets up the panels for pagination 346 Feature.prototype.panelLogic = function(feature) { 347 348 var itemsPerPage, 349 containerWidth = parseInt($(feature.$element.selector + ' .multiFeatureContent').css('width')), 350 containerHeight = parseInt(feature.$element.css('height')), 351 itemWidth = parseInt($('.featureItem', feature.$element).css('width')), 352 itemHeight = parseInt($('.featureItem', feature.$element).css('height')), 353 marginLeftWidth = parseInt($('.featureItem', feature.$element).css('margin-left')), 354 marginRightWidth = parseInt($('.featureItem', feature.$element).css('margin-right')); 355 356 itemWidth = itemWidth + marginLeftWidth + marginRightWidth; 357 358 var columnNumber = Math.floor(containerWidth/itemWidth), 359 rowNumber = Math.floor(containerHeight/itemHeight), 360 itemsPerPage = columnNumber*rowNumber; 361 if ( feature.options.maxItemsVisible < itemsPerPage && feature.options.maxItemsVisible > 0 ) { 362 itemsPerPage = feature.options.maxItemsVisible; 363 } 364 365 if ( feature.data.length > itemsPerPage && feature.options.isResponsive) { 366 feature.options.hasPanels = true; 367 368 $(feature.$element.selector).addClass('multiFeatureWithMultiplePages'); 369 370 var elems = $('.featureItem', feature.$element), 371 panelNumber = 1; 372 373 if ( itemsPerPage > 0 ){ 374 for(var i = 0; i < elems.length; i+=itemsPerPage) { 375 elems.slice(i, i+itemsPerPage).wrapAll('<div class="multiFeatureItemPanel multiFeatureItemPanel'+panelNumber+'"></div>'); 376 panelNumber++; 377 } 378 } else { 379 $('.featureItem', feature.$element).each(function(){ 380 $(this).wrapAll('<div class="multiFeatureItemPanel multiFeatureItemPanel'+panelNumber+'"></div>'); 381 panelNumber++; 382 }); 383 } 384 385 } else { 386 $(feature.$element.selector).removeClass('multiFeatureWithMultiplePages'); 387 feature.options.hasPanels = false; 388 } 389 } 390
391 // Updates the view. 392 Feature.prototype.update = function() { 393 this.$element.html('<div class="multiFeatureContent"></div>'); 394 this.initialize(); 395 }; 396 397 398 399 //NOT USED - Checks window `resize` event. 400 Feature.prototype.onThrottledResize = function() { 401 window.clearTimeout(this.resizeTimer); 402 this.resizeTimer = window.setTimeout(this._handlers.onResize, this.options.responsiveRefreshRate); 403 }; 404 405 406 //NOT USED - Checks window `resize` event. 407 Feature.prototype.onResize = function() { 408 409 if (!this.$element.hasClass('.multiFeatureWithMultiplePages')) { 410 return false; 411 } 412 this.enter('resizing'); 413 414 if (this.trigger('resize').isDefaultPrevented()) { 415 this.leave('resizing'); 416 return false; 417 } 418 419 this.invalidate('width'); 420 421 this.refresh(); 422 423 this.leave('resizing'); 424 this.trigger('resized'); 425 }; 426 427 428 // Registers event handlers. 429 Feature.prototype.registerEventHandlers = function() { 430 if (this.options.isResponsive !== false) { 431 this.on(window, 'resize', this.onThrottledResize); 432 } 433 }; 434 435 // Adding controls to the container 436 Feature.prototype.addControls = function(event) { 437 var parent = this; 438 if(this.options.arrowPagination){ 439 $(this.$element.selector).addClass('multiFeatureWithArrows'); 440 $(this.$element).append('<a href="javascript:void(0)" class="multiFeatureArrow multiFeatureArrowLeft" id="'+this.id+'ArrowLeft"></a><a href="javascript:void(0)" class="multiFeatureArrow multiFeatureArrowRight" id="'+this.id+'ArrowRight"></a>') 441 $(this.$element.selector + 'ArrowLeft').click(function(){ 442 var el = $(this) 443 var arrowContainer = el.attr('id').replace('ArrowLeft',''); 444 parent.multiFeatureLeft(1, arrowContainer); 445 }); 446 $(this.$element.selector + 'ArrowRight').click(function(){ 447 var el = $(this) 448 var arrowContainer = el.attr('id').replace('ArrowRight',''); 449 parent.multiFeatureRight(1, arrowContainer); 450 }); 451 } else { 452 $(this.$element.selector + '.multiFeatureArrow').css('display', 'none'); // for responsive layout 453 } 454 455 if(this.options.textPagination) { 456 $(this.$element.selector).addClass('multiFeatureWithTextPagination'); 457 $(this.$element).append('<div id="'+this.id+'TextPaginationContainer"><a href="javascript:void(0)" class="multiFeatureWithTextPaginationArrowLeft multiFeatureWithTextPaginationArrow" id="'+this.id+'WithTextPaginationArrowLeft"></a><span class="multiFeatureWithTextPaginationPagination" id="'+this.id+'WithTextPaginationPagination"></span><a href="javascript:void(0)" class="multiFeatureWithTextPaginationArrowRight multiFeatureWithTextPaginationArrow" id="'+this.id+'WithTextPaginationArrowRight"></a></div>'); 458 $(this.$element.selector + 'WithTextPaginationArrowLeft').click(function(){ 459 var el = $(this) 460 var arrowContainer = el.attr('id').replace('ArrowLeft',''); 461 parent.multiFeatureLeft(1, parent.id); 462 }); 463 $(this.$element.selector + 'WithTextPaginationArrowRight').click(function(){ 464 var el = $(this) 465 var arrowContainer = el.attr('id').replace('ArrowRight',''); 466 parent.multiFeatureRight(1, parent.id); 467 }); 468 var numPages = $(this.$element.selector + ' .multiFeatureItemPanel').length; 469 $(this.$element.selector + 'WithTextPaginationPagination').text('1 of '+ numPages); 470 } 471 472 if(this.options.iconPagination){ 473 $(this.$element.selector).addClass('multiFeatureWithIconPagination'); 474 } 475 }; 476 477 //Paginate left 478 Feature.prototype.multiFeatureLeft = function(num, parent) { 479 var containerWidth = parseInt($(this.$element.selector + ' .multiFeatureContent').css('width')); 480 var parentObj = this; 481 var checkActive = $('#'+parent).hasClass('multiFeatureActive'); 482 var panelsNumber = $('#'+parent+' .multiFeatureItemPanel').length; 483 if(!checkActive){ 484 var i = 1; 485 $('#'+parent).addClass('multiFeatureActive'); 486 $('#'+parent+' .multiFeatureContent .multiFeatureItemPanel').last().prependTo('#'+parent+' .multiFeatureContent').css('left','-'+containerWidth+'px'); 487 setTimeout(function(){ 488 $('#'+parent+' .multiFeatureItemPanel').each(function(){ 489 var getLeft = parseInt($(this).css('left')); 490 var newLeft = getLeft+containerWidth; 491 $(this).animate({ 492 left: newLeft+'px' 493 }, parentObj.options.slideSpeed, parentObj.options.easing, function(){ 494 if (i === panelsNumber) { 495 setTimeout(function() { 496 $('#'+parent).removeClass('multiFeatureActive'); 497 }, 500); 498 i = 1; 499 } else { 500 i++; 501 } 502 if(parentObj.options.textPagination){ 503 var numPages = $('#'+parent+' .multiFeatureItemPanel').length; 504 var getCur = $('.multiFeatureItemPanel[style="left: 0px;"]').attr('class'); 505 var r = /\d+/; 506 getCur = getCur.match(r) 507 $('#'+parent+'WithTextPaginationPagination').text(getCur+' of '+numPages); 508 } 509 }); 510 511 }); 512 }); 513 } 514 }; 515 516 //Paginate Right 517 Feature.prototype.multiFeatureRight = function(num, parent) { 518 var containerWidth = parseInt($(this.$element.selector + ' .multiFeatureContent').css('width')); 519 var parentObj = this; 520 var checkActive = $('#'+parent).hasClass('multiFeatureActive'); 521 var panelsNumber = $('#'+parent+' .multiFeatureItemPanel').length; 522 if(!checkActive){ 523 var i = 1; 524 $('#'+parent).addClass('multiFeatureActive'); 525 $('#'+parent+' .multiFeatureItemPanel').each(function(){ 526 var getLeft = parseInt($(this).css('left')); 527 var newLeft = getLeft-containerWidth; 528 $(this).animate({ 529 left: newLeft+'px' 530 }, parentObj.options.slideSpeed, parentObj.options.easing, function(){ 531 var setNewLeft = ($('#'+parent+' .multiFeatureItemPanel').length-1)*containerWidth; 532 $('#'+parent+' .multiFeatureContent .multiFeatureItemPanel').first().addClass('newlyPositionedSlide').animate({ 533 left: setNewLeft+'px' 534 },1, 'linear', function(){ 535 $('#'+parent+' .newlyPositionedSlide').appendTo('#'+parent+' .multiFeatureContent'); 536 $('#'+parent+' .multiFeatureContent .multiFeatureItemPanel').removeClass('newlyPositionedSlide'); 537 if (i === panelsNumber) { 538 setTimeout(function() { 539 $('#'+parent).removeClass('multiFeatureActive'); 540 }, 500); 541 i = 1; 542 } else { 543 i++; 544 } 545 if(parentObj.options.textPagination){ 546 var numPages = $('#'+parent+' .multiFeatureItemPanel').length; 547 var getCur = $('#'+parent+' .multiFeatureItemPanel[style="left: 0px;"]').attr('class'); 548 var r = /\d+/; 549 getCur = getCur.match(r) 550 $('#'+parent+'WithTextPaginationPagination').text(getCur+' of '+numPages); 551 } 552 }); 553 554 }); 555 556 557 }); 558 } 559 }; 560 561 562 // Initiate factory for the Multifeature 563 $.fn.MultiScheduleFeature = function(option) { 564 565 var featureObj = new Feature(this, typeof option == 'object' && option); 566 567 $.ajax({
568 type: "POST", 569 dataType: "jsonp", 570 jsonpCallback: "saffireEvents", 571 contentType: "application/jsonp", 572 url: SITEBASEURL + "services/eventsservice.asmx/GetEventsJsonP", 573 error: function(results, error, thrown) { 574 575 576 }, 577 success: function(data) { 578 featureObj.data = data; 579 featureObj.initialize(); 580 }, 581 // error: getWidgetDataError 582 }); 583 584 var windowWidth = $(window).width(); 585 586 $(window).resize(function() { 587 if ($(window).width() != windowWidth) { 588 windowWidth = $(window).width(); 589 if (featureObj.options.isResponsive) { 590 featureObj.update(); 591 } 592 } 593 }); 594 }; 595 596 var getWidgetDataError = function (data) { 597 console.log('failed to load' 598 , '\n' + 'Status: ' + data.status + ' ' + data.statusText 599 , '\n' + data.responseText); 600 601 }; 602 603 $.fn.MultiScheduleFeature.Constructor = Feature; 604 605})(window.jQuery, window, document);
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.