1/* 2 * jQuery Tippy 3 * Version 1.4.1 4 * By Chris Roberts, [email protected] 5 * http://croberts.me/ 6 * 7 */ 8 9/* 10 * The MIT License (MIT) 11 * 12 * Copyright (c) 2013 Chris Roberts 13 * 14 * Permission is hereby granted, free of charge, to any person obtaining a copy of 15 * this software and associated documentation files (the "Software"), to deal in 16 * the Software without restriction, including without limitation the rights to 17 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 18 * the Software, and to permit persons to whom the Software is furnished to do so, 19 * subject to the following conditions: 20 * 21 * The above copyright notice and this permission notice shall be included in all 22 * copies or substantial portions of the Software. 23 * 24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 26 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 27 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 28 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 29 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 * 31 */ 32 33(function($) { 34 $.fn.tippy = function(options) { 35 // Track data for each of our tooltips 36 var tippy_state = {}; 37 38 // Hold global position data 39 var tippy_positions = {}; 40 41 // Store id of last displayed tooltip 42 var tippy_showing = false; 43 44 var opts = $.extend({}, $.fn.tippy.defaults, options); 45 46 // How many tooltips are out there? 47 var countTips = 0; 48 49 // Arbitrary counter for bringing tooltips to the front when clicked 50 var topTipIndex = 100; 51 52 // Loop through tooltips and set them up 53 return this.each(function() { 54 countTips++; 55 var tipId = 'tippy_' + countTips; 56 57 // Initialize the storage object for this tooltip and load all default or global options 58 tippy_state[tipId] = {}; 59 60 // Grab data values 61 tippy_state[tipId].options = $.extend({}, opts, $(this).data()); 62 63 // Make sure the data container is hidden 64 $(this).hide(); 65 66 // Set the id to our data container 67 $(this).attr('id', tipId); 68 69 // Create the link that will trigger the tooltip. 70 var tippyLink; 71 72 // See if we are attaching the tooltip to an already-existing anchor or if we're creating a new link 73 if (typeof tippy_state[tipId].options.anchor != 'undefined') { 74 tippyLink = $(tippy_state[tipId].options.anchor); 75 } else { 76 tippyLink = $('<a></a>'); 77 } 78 79 tippyLink.addClass('tippy_link') 80 .attr('id', tipId + '_link'); 81 82 // Using [''] for class variable since .class breaks in ie8 83 if (typeof tippy_state[tipId].options['class'] != 'undefined') { 84 tippyLink.addClass(tippy_state[tipId].options['class']); 85 } 86 87 if (typeof tippy_state[tipId].options.name != 'undefined') { 88 tippyLink.attr('name', tippy_state[tipId].options.name) 89 } 90 91 if (tippy_state[tipId].options.showtitle) { 92 tippyLink.attr('title', tippy_state[tipId].options.title); 93 } 94 95 if (typeof tippy_state[tipId].options.img != 'undefined') { 96 tippyLink.css('display', 'inline-block').css('position', 'relative'); 97 98 var tippyImg = $('<img />'); 99 tippyImg.attr('src', tippy_state[tipId].options.img); 100 101 if (tippy_state[tipId].options.showtitle) { 102 tippyImg.attr('alt', tippy_state[tipId].options.title); 103 } 104 105 tippyLink.append(tippyImg); 106 tippy_state[tipId].img = tippyImg; 107 tippy_state[tipId].imgsrc = tippy_state[tipId].options.img; 108 109 // See if we have a swap image, go ahead and set it 110 if (typeof tippy_state[tipId].options.swapimg != 'undefined') { 111 var tippySwapImg = $('<img />') 112 .attr('src', tippy_state[tipId].options.swapimg) 113 .addClass('tippy_swap') 114 .css('display', 'none'); 115 116 if (tippy_state[tipId].options.showtitle) { 117 tippySwapImg.attr('alt', tippy_state[tipId].options.title); 118 } 119 120 tippyLink.append(tippySwapImg); 121 tippy_state[tipId].swapimg = tippySwapImg; 122 123 // tippy_state[tipId].swapimgsrc = tippy_state[tipId].options.swapimg; 124 } 125 } else if (typeof tippy_state[tipId].options.title != 'undefined') { 126 tippyLink.html(tippy_state[tipId].options.title); 127 } 128 129 if (typeof tippy_state[tipId].options.title == 'undefined' && typeof tippy_state[tipId].options.headertitle == 'undefined') { 130 tippy_state[tipId].options.showheader = false;
131 } 132 133 // Check for a link 134 if (typeof tippy_state[tipId].options.href != 'undefined') { 135 tippyLink.attr('href', tippy_state[tipId].options.href); 136 137 if (typeof tippy_state[tipId].options.target != 'undefined') { 138 tippyLink.attr('target', tippy_state[tipId].options.target); 139 } 140 } 141 142 if (tippy_state[tipId].options.hoverpopup) { 143 tippyLink.mouseover(function(event) { showTooltip(tipId, event); }); 144 145 if (tippy_state[tipId].options.alttrigger != 'undefined') { 146 $(tippy_state[tipId].options.alttrigger).mouseover(function(event) { showTooltip(tipId, event); }); 147 } 148 } else { 149 tippyLink.click(function(event) { showTooltip(tipId, event); }); 150 151 if (tippy_state[tipId].options.alttrigger != 'undefined') { 152 $(tippy_state[tipId].options.alttrigger).click(function(event) { showTooltip(tipId, event); }); 153 } 154 } 155 156 if (tippy_state[tipId].options.autoclose) { 157 tippyLink.mouseout(function() { 158 hideTooltip(tipId); 159 }); 160 161 if (tippy_state[tipId].options.alttrigger != 'undefined') { 162 $(tippy_state[tipId].options.alttrigger).mouseout(function() { 163 hideTooltip(tipId); 164 }); 165 } 166 } 167 168 if (typeof tippy_state[tipId].options.anchor == 'undefined') { 169 $(this).before(tippyLink); 170 } 171 172 tippy_state[tipId].link = tippyLink; 173 174 // See if we are autoshowing. If so, create the tip and show it. 175 if (tippy_state[tipId].options.autoshow) { 176 createTooltip(tipId); 177 positionTip(tipId); 178 doShowTooltip(tipId, true); 179 } 180 }); 181 182 function createTooltip(tipId) 183 { 184 // Initialize container variables 185 var tipBox, tipHeader, tipClose, tipBody; 186 187 // Create our tooltip box for this tip. Store it so we can reuse it. 188 tipBox = $('<div></div>') 189 .hide() 190 .css('height', 'auto') 191 .css('display', 'none') 192 .addClass('tippy_tip') 193 .addClass('domTip_Tip') 194 .attr('id', tipId + '_box') 195 .mouseover(function() { freezeTooltip(tipId); }) 196 .click(function() { $(this).css('z-index', topTipIndex); topTipIndex++; }); 197 198 tippy_state[tipId].tipBox = tipBox; 199 200 if (!tippy_state[tipId].options.container) { 201 $('#' + tipId).before(tippy_state[tipId].tipBox); 202 } else { 203 $(tippy_state[tipId].options.container).append(tipBox); 204 } 205 206 // Using [''] for class variable since .class breaks in ie8 207 if (typeof tippy_state[tipId].options['class'] != 'undefined') { 208 tipBox.addClass(tippy_state[tipId].options['class'] + '_tip') 209 } 210 211 switch (tippy_state[tipId].options.position) { 212 case 'link': 213 case 'mouse': 214 tipBox.css('position', 'absolute'); 215 break; 216 217 default: 218 tipBox.css('position', tippy_state[tipId].options.position); 219 } 220 221 // Start the close timer when mouse away, if specified 222 if (tippy_state[tipId].options.autoclose) { 223 tipBox.mouseout(function() { hideTooltip(tipId); }); 224 } 225 226 // Set up the header, if used 227 if (tippy_state[tipId].options.showheader) { 228 tipHeader = $('<div></div>') 229 .css('height', 'auto') 230 .addClass('tippy_header') 231 .addClass('domTip_tipHeader'); 232 233 var headerTitle; 234 if (typeof tippy_state[tipId].options.headertitle != 'undefined') { 235 headerTitle = tippy_state[tipId].options.headertitle; 236 } else { 237 headerTitle = tippy_state[tipId].options.title; 238 } 239 240 if (typeof tippy_state[tipId].options.headerhref != 'undefined') { 241 var headerLink = $('<a></a>') 242 .attr('href', tippy_state[tipId].options.headerhref) 243 .attr('title', headerTitle) 244 .html(headerTitle); 245 246 if (typeof tippy_state[tipId].options.target != 'undefined') { 247 headerLink.attr('target', tippy_state[tipId].options.target); 248 } 249 250 tipHeader.append(headerLink); 251 } else { 252 tipHeader.html(headerTitle); 253 } 254 255 tipHeader.appendTo(tipBox); 256 } 257 258 // Set up the tooltip body 259 tipBody = $('<div></div>') 260 .css('height', 'auto') 261 .addClass('tippy_body') 262 .addClass('domTip_tipBody') 263 .appendTo(tipBox); 264 265 if (tippy_state[tipId].options.htmlentities == false) { 266 // Move body content 267 $('#' + tipId).appendTo(tipBody).show(); 268 } 269 270 if (tippy_state[tipId].options.hasnested == true) { 271 tipBody.css('overflow', 'visible'); 272 } 273 274 if (tippy_state[tipId].options.height != false) { 275 tipBody.css("height", tippy_state[tipId].options.height + "px"); 276 tipBody.css("min-height", tippy_state[tipId].options.height + "px"); 277 tipBody.css("max-height", tippy_state[tipId].options.height + "px"); 278 } 279 280 // Set up the close link, if used. Position depends on whether or not the header is displayed 281 if (tippy_state[tipId].options.showclose) {
282 tipClose = $('<div></div>') 283 .addClass('tippy_closelink') 284 .click(function() { doHideTooltip(tipId); }) 285 .html(tippy_state[tipId].options.closetext); 286 287 if (tippy_state[tipId].options.showheader) { 288 tipHeader.append(tipClose); 289 } else { 290 tipBody.prepend(tipClose); 291 } 292 } 293 294 if (tippy_state[tipId].options.width != false) { 295 // Get difference 296 if (tippy_state[tipId].options.showheader) { 297 headerDiff = tipBox.width() - tipHeader.width(); 298 tipHeader.css('width', (tippy_state[tipId].options.width - headerDiff) + 'px'); 299 } 300 301 bodyDiff = tipBox.width() - tipBody.width(); 302 303 tipBox.css('width', tippy_state[tipId].options.width + 'px'); 304 tipBody.css('width', (tippy_state[tipId].options.width - bodyDiff) + 'px'); 305 } 306 307 if (typeof tippy_state[tipId].options.bodystyle != 'undefined') { 308 tipBody.attr('style', tipBody.attr('style') + ' ' + tippy_state[tipId].options.bodystyle); 309 } 310 311 if (tippy_state[tipId].options.draggable) { 312 if (tippy_state[tipId].options.dragheader && tippy_state[tipId].options.showheader) { 313 tipBox.draggable({ handle: '.tippy_header' }); 314 tipHeader.addClass('tippy_draggable'); 315 } else { 316 tipBox.draggable(); 317 tipBox.addClass('tippy_draggable'); 318 } 319 } 320 } 321 322 // Initialize all position data 323 function getPositions(tipId, event) 324 { 325 if (!event) { 326 event = window.event; 327 } 328 329 tippy_positions.scrollPageX = $(window).scrollLeft(); 330 tippy_positions.scrollPageY = $(window).scrollTop(); 331 332 tippy_positions.viewScreenX = $(window).width(); 333 tippy_positions.viewScreenY = $(window).height(); 334 335 tippy_positions.curPageX = event.clientX + tippy_positions.scrollPageX; 336 tippy_positions.curPageY = event.clientY + tippy_positions.scrollPageY; 337 338 tippy_positions.viewPageX = event.clientX; 339 tippy_positions.viewPageY = event.clientY; 340 341 tippy_positions.tipLinkHeight = $('#' + tipId + '_link').height(); 342 343 // document: calculate the position relative to the document. 344 // parent: calculate the position relative to the parent. 345 // Most situations will want parent, but more advanced positioning techniques may want document. 346 if (tippy_state[tipId].options.calcpos == 'document') { 347 tippy_positions.tipLinkX = $('#' + tipId + '_link').offset().left; 348 tippy_positions.tipLinkY = $('#' + tipId + '_link').offset().top; 349 } else { 350 tippy_positions.tipLinkX = $('#' + tipId + '_link').position().left; 351 tippy_positions.tipLinkY = $('#' + tipId + '_link').position().top; 352 } 353 }; 354 355 function positionTip(tipId, event) 356 { 357 // Grab the box with a shortcut name 358 tipBox = tippy_state[tipId].tipBox; 359 360 // Grab position settings 361 getPositions(tipId, event); 362 363 // Get the height and width of the tooltip container. Will use this when 364 // calculating tooltip position. 365 var tipHeight = tipBox.height(); 366 var tipWidth = tipBox.width(); 367 368 // Calculate where the tooltip should be located 369 370 var tipHorSide = "left", tipVertSide = "top"; 371 var tipXloc, tipYloc = 0; 372 373 // tipXloc and tipYloc specify where the tooltip should appear. 374 // By default, it is just below and to the right of the mouse pointer. 375 376 if (tippy_state[tipId].options.position == 'link') { 377 // Position below the link 378 tipXloc = tippy_positions.tipLinkX; 379 tipYloc = tippy_positions.tipLinkY + tippy_positions.tipLinkHeight; 380 } else if (tippy_state[tipId].options.position == 'mouse') { 381 // Position below the mouse cursor 382 tipXloc = tippy_positions.curPageX; 383 tipYloc = tippy_positions.curPageY; 384 } else { 385 // Check our top/bottom and left/right values; use 0 as a default if something is missing from each pair. 386 if (typeof tippy_state[tipId].options.top == 'undefined' && typeof tippy_state[tipId].options.bottom == 'undefined') { 387 tipYloc = 0; 388 } else if (typeof tippy_state[tipId].options.top != 'undefined') { 389 tipYloc = tippy_state[tipId].options.top; 390 } else { 391 tipVertSide = "bottom" 392 tipYloc = tippy_state[tipId].options.bottom; 393 } 394 395 if (typeof tippy_state[tipId].options.left == 'undefined' && typeof tippy_state[tipId].options.right == 'undefined') { 396 tipXloc = 0; 397 } else if (typeof tippy_state[tipId].options.left != 'undefined') { 398 tipXloc = tippy_state[tipId].options.left; 399 } else { 400 tipHorSide = "right" 401 tipXloc = tippy_state[tipId].options.right; 402 } 403 } 404 405 // Check offsets if position is link or mouse 406 if (tippy_state[tipId].options.position == 'link' || tippy_state[tipId].options.position == 'mouse') { 407 tipXloc += tippy_state[tipId].options.offsetx; 408 tipYloc += tippy_state[tipId].options.offsety; 409 } 410 411 // Adjust position of tooltip to place it within window boundaries 412 413 // If the tooltip extends off the right side, pull it over 414 if ((tipXloc - tippy_positions.scrollPageX) + 5 + tipWidth > tippy_positions.viewScreenX) { 415 pageXDiff = ((tipXloc - tippy_positions.scrollPageX) + 5 + tipWidth) - tippy_positions.viewScreenX; 416 tipXloc -= pageXDiff; 417 } 418 419 // If the tooltip will extend off the bottom of the screen, pull it back up. 420 if ((tipYloc - tippy_positions.scrollPageY) + 5 + tipHeight > tippy_positions.viewScreenY) { 421 pageYDiff = ((tipYloc - tippy_positions.scrollPageY) + 5 + tipHeight - tippy_positions.viewScreenY); 422 tipYloc -= pageYDiff; 423 } 424 425 // If the tooltip extends off the bottom and the top, line up the top of 426 // the tooltip with the top of the page 427 if (tipHeight > tippy_positions.viewScreenY) { 428 tipYloc = tippy_positions.scrollPageY + 5; 429 } 430 431 // Set the position in pixels. 432 tipBox.css(tipHorSide, tipXloc + "px"); 433 tipBox.css(tipVertSide, tipYloc + "px"); 434 } 435 436 function showTooltip(tipId, event) 437 { 438 // See if state exists 439 if (typeof tippy_state[tipId].tipBox == 'undefined') { 440 createTooltip(tipId); 441 } 442 443 // Are we already showing the tooltip? Freeze it. 444 if (tippy_state[tipId].state == 'showing' || tippy_state[tipId].state == 'displaying') { 445 // Nothing to freeze if we're not autoclosing 446 if (tippy_state[tipId].options.autoclose) { 447 freezeTooltip(tipId); 448 } 449 } else { 450 tippy_state[tipId].state = 'displaying'; 451 positionTip(tipId, event); 452 453 tippy_state[tipId].timer = setTimeout(function() { 454 doShowTooltip(tipId, false); 455 }, tippy_state[tipId].options.showdelay); 456 } 457 } 458 459 function doShowTooltip(tipId, instashow) 460 { 461 tippy_state[tipId].state = 'showing'; 462 463 // Check on a swapimg/swaptitle to use if img/title is set 464 if (typeof tippy_state[tipId].options.swapimg != 'undefined' && typeof tippy_state[tipId].options.img != 'undefined') { 465 // Fade out the primary image but don't set it display: none; This approach preserves 466 // the image size for the swapimg. 467 tippy_state[tipId].img.animate({ opacity: 0 }, 500); 468 tippy_state[tipId].swapimg.fadeIn(500); 469 } else if (typeof tippy_state[tipId].options.swaptitle != 'undefined' && typeof tippy_state[tipId].options.title != 'undefined') { 470 tippy_state[tipId].link.html(tippy_state[tipId].options.swaptitle); 471 } 472 473 if (typeof tippy_state[tipId].options.swaptitle != 'undefined' && typeof tippy_state[tipId].options.title != 'undefined') { 474 tippy_state[tipId].link.html(tippy_state[tipId].options.swaptitle); 475 } 476 477 if (!tippy_state[tipId].options.multitip && tippy_showing) { 478 doHideTooltip(tippy_showing); 479 } 480 481 // See if we need to decode and pull in content 482 if (tippy_state[tipId].options.htmlentities == true) { 483 var convertMarkup = $("<div/>").html($('#' + tipId).html()).text(); 484 $('.tippy_body', tippy_state[tipId].tipBox).html(convertMarkup).show(); 485 } 486 487 if (instashow) { 488 tippy_state[tipId].tipBox.show(); 489 } else { 490 tippy_state[tipId].tipBox.fadeIn(tippy_state[tipId].options.showspeed); 491 } 492 493 tippy_showing = tipId; 494 495 if (tippy_state[tipId].tipBox.mediaelementplayer) { 496 // Load the MediaElement player on any media elements in the tooltip 497 jQuery('video,audio', tippy_state[tipId].tipBox).mediaelementplayer().load(); 498 } 499 } 500 501 function showSwapImg(tipId, event) 502 { 503 // Check on a swapimg/swaptitle to use if img/title is set 504 if (typeof tippy_state[tipId].options.swapimg != 'undefined' && typeof tippy_state[tipId].options.img != 'undefined') { 505 // Fade out the primary image but don't set it display: none; This approach preserves 506 // the image size for the swapimg. 507 tippy_state[tipId].img.animate({ opacity: 0 }, 500); 508 tippy_state[tipId].swapimg.fadeIn(500); 509 } 510 } 511 512 function hideSwapImg(tipId) 513 { 514 // Check on a swaptitle to use if title is set 515 if (typeof tippy_state[tipId].options.swapimg != 'undefined' && typeof tippy_state[tipId].options.img != 'undefined') { 516 tippy_state[tipId].img.animate({ opacity: 1 }, 500); 517 tippy_state[tipId].swapimg.fadeOut(500); 518 } 519 } 520 521 // When the visitor mouses away from the link or the tooltip, start a timer that 522 // will hide the tooltip after a set period of time. 523 function hideTooltip(tipId) 524 { 525 if (tippy_state[tipId].state == 'displaying') { 526 tippy_state[tipId].state = 'hidden'; 527 clearTimeout(tippy_state[tipId].timer); 528 } else { 529 tippy_state[tipId].timer = setTimeout(function() { 530 doHideTooltip(tipId); 531 }, tippy_state[tipId].options.hidedelay); 532 } 533 } 534 535 function doHideTooltip(tipId) 536 { 537 tippy_showing = false;
538 tippy_state[tipId].state = 'hidden'; 539 clearTimeout(tippy_state[tipId].timer); 540 541 hideSwapImg(tipId); 542 543 if (typeof tippy_state[tipId].options.swaptitle != 'undefined' && typeof tippy_state[tipId].options.title != 'undefined') { 544 tippy_state[tipId].link.html(tippy_state[tipId].options.title); 545 } 546 547 tippy_state[tipId].tipBox.fadeOut(tippy_state[tipId].options.hidespeed, function() { 548 // See if we need to clear content 549 if (tippy_state[tipId].options.htmlentities == true) { 550 $('.tippy_body', tippy_state[tipId].tipBox).html(''); 551 } 552 }); 553 } 554 555 // If the user mouses over the tooltip, make sure we don't hide it. The tooltip 556 // should freeze when the mouse is over it. 557 function freezeTooltip(tipId) 558 { 559 clearTimeout(tippy_state[tipId].timer); 560 tippy_state[tipId].tipBox.stop(); 561 tippy_state[tipId].tipBox.css("opacity", 100); 562 } 563 564 function getTipId(manualId) 565 { 566 console.log("Finding..."); 567 if (tippy_ids[manualId] != "undefined") { 568 return tippy_ids[manualId]; 569 } else { 570 return false; 571 } 572 } 573 574 function triggerTippy(manualId, event) 575 { 576 console.log("Enter..."); 577 tipId = getTipId(manualId); 578 console.log("Got " + tipId); 579 580 if (tipId) { 581 showTooltip(tipId, event); 582 } 583 } 584 }; 585 586 $.fn.tippy.defaults = { 587 showtitle: false, // Should the browser's title attribute be used? Good for accessibility, bad for tooltip visibility. 588 offsetx: 10, // When position is set to mouse or link, how far should the tooltip be offset left or right? Positive for right, negative for left. 589 offsety: 10, // When position is set to mouse or link, how far should the tooltip be offset up or down? Positive for down, negative for up. 590 hoverpopup: true, // Should the tooltip display on hover? If set to false, tooltip shows on click. 591 multitip: false, // Should it be possible to have multiple tooltips onscreen at once? 592 showdelay: 100, // When showing on hover, how long in ms should it delay before showing the tooltip? 593 showspeed: 200, // How long in ms should it take the tooltip to fade in? 594 hidedelay: 1000, // When auto hiding, how long in ms should the tooltip be visible before it starts to fade out? 595 hidespeed: 200, // How long in ms should it take the tooltip to fade out? 596 showheader: true, // Should the tooltip have a header? 597 showclose: true, // Should the tooltip have a close link? Usefor for mobile devices or when autoClose is false. 598 closetext: 'close', // When using a close link, what should it say? 599 autoclose: true, // Should the tooltip auto close after a delay? 600 container: false, // What should be the tooltip's parent element? Useful if you want to move it into another element. 601 position: 'link', // fixed, absolute, relative, mouse, link 602 height: false, // Specify a height for the tooltip 603 width: false, // Specify a width for the tooltip 604 draggable: false, // Should visitors be able to drag the tooltip around? (requires jQuery UI) 605 dragheader: true, // If dragging is enabled should the visitor only be able to drag from the header? If false, user can move the tooltip from any part. 606 autoshow: false, // Should tooltips automatically be displayed when the page is loaded? 607 calcpos: 'parent', // Should the tooltip position be calculated relative to the parent or to the document? 608 htmlentities: false, // If false, Tippy assumes the tooltip content is straight html. If true, assumes it is encoded as entities and needs to be decoded. 609 hasnested: false // Whether or not this tooltip has a nested tooltip 610 } 611}(jQuery));
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.