1(function(angular) { 2 'use strict'; 3 4 var directiveModule = angular.module('angularjs-dropdown-multiselect', []); 5 6 directiveModule.directive('dmDropdownStaticInclude', ['$compile', function($compile) { 7 return function(scope, element, attrs) { 8 var template = attrs.dmDropdownStaticInclude; 9 var contents = element.html(template).contents(); 10 $compile(contents)(scope); 11 }; 12 }]); 13 14 directiveModule.directive('ngDropdownMultiselect', ['$filter', '$document', '$compile', '$parse', function($filter, $document, $compile, $parse) { 15 return { 16 restrict: 'AE', 17 scope: { 18 selectedModel: '=', 19 options: '=', 20 extraSettings: '=', 21 events: '=', 22 searchFilter: '=?', 23 translationTexts: '=', 24 groupBy: '@', 25 disabled: "=", 26 buttonDefaultText : '@' 27 }, 28 template: function(element, attrs) { 29 var checkboxes = attrs.checkboxes ? true : false; 30 var groups = attrs.groupBy ? true : false; 31 32 var template = '<div class="multiselect-parent btn-group dropdown-multiselect" ng-class="{open: open}">'; 33 template += '<button ng-disabled="disabled" type="button" class="dropdown-toggle" ng-class="settings.buttonClasses" ng-click="toggleDropdown()">{{getButtonText()}} <span class="caret"></span></button>'; 34 template += '<ul class="dropdown-menu dropdown-menu-form" ng-if="open" ng-style="{display: open ? \'block\' : \'none\', height : settings.scrollable ? settings.scrollableHeight : \'auto\', overflow: \'auto\' }" >'; 35 template += '<li ng-if="settings.showCheckAll && settings.selectionLimit !== 1"><a ng-keydown="keyDownLink($event)" data-ng-click="selectAll()" tabindex="-1" id="selectAll"><span class="glyphicon glyphicon-ok"></span> {{texts.checkAll}}</a>'; 36 template += '<li ng-if="settings.showUncheckAll"><a ng-keydown="keyDownLink($event)" data-ng-click="deselectAll();" tabindex="-1" id="deselectAll"><span class="glyphicon glyphicon-remove"></span> {{texts.uncheckAll}}</a></li>'; 37 template += '<li ng-if="settings.selectByGroups && ((settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll)" class="divider"></li>'; 38 template += '<li ng-if="settings.selectByGroups && ((settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll)" class="divider"></li>'; 39 template += '<li ng-repeat="currentGroup in settings.selectByGroups track by $index" ng-click="selectCurrentGroup(currentGroup)"><a ng-class="{\'dropdown-selected-group\': selectedGroup === currentGroup}" tabindex="-1">{{::texts.selectGroup}} {{::getGroupLabel(currentGroup)}}</a></li>'; 40 template += '<li ng-if="settings.selectByGroups && settings.showEnableSearchButton" class="divider"></li>'; 41 template += '<li ng-if="settings.showEnableSearchButton && settings.enableSearch"><a ng-keydown="keyDownLink($event); keyDownToggleSearch();" ng-click="toggleSearch($event);" tabindex="-1">{{texts.disableSearch}}</a></li>'; 42 template += '<li ng-if="settings.showEnableSearchButton && !settings.enableSearch"><a ng-keydown="keyDownLink($event); keyDownToggleSearch();" ng-click="toggleSearch($event);" tabindex="-1">{{texts.enableSearch}}</a></li>'; 43 template += '<li ng-if="(settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll || settings.showEnableSearchButton" class="divider"></li>'; 44 template += '<li ng-if="settings.enableSearch"><div class="dropdown-header"><input type="text" class="form-control searchField" ng-keydown="keyDownSearchDefault($event); keyDownSearch($event, input.searchFilter);" ng-style="{width: \'100%\'}" ng-model="input.searchFilter" placeholder="{{texts.searchPlaceholder}}" /></li>'; 45 template += '<li ng-if="settings.enableSearch" class="divider"></li>'; 46 47 if (groups) { 48 template += '<li ng-repeat-start="option in orderedItems | filter:getFilter(input.searchFilter)" ng-show="getPropertyForObject(option, settings.groupBy) !== getPropertyForObject(orderedItems[$index - 1], settings.groupBy)" role="presentation" class="dropdown-header">{{ getGroupLabel(getPropertyForObject(option, settings.groupBy)) }}</li>'; 49 template += '<li ng-class="{\'active\': isChecked(getPropertyForObject(option,settings.idProp)) && settings.styleActive}" ng-repeat-end role="presentation">'; 50 } else { 51 template += '<li ng-class="{\'active\': isChecked(getPropertyForObject(option,settings.idProp)) && settings.styleActive}" role="presentation" ng-repeat="option in options | filter:getFilter(input.searchFilter)">'; 52 } 53 54 template += '<a ng-keydown="option.disabled || keyDownLink($event)" role="menuitem" class="option" tabindex="-1" ng-click="option.disabled || setSelectedItem(getPropertyForObject(option,settings.idProp), false, true)" ng-disabled="option.disabled">'; 55 56 if (checkboxes) { 57 template += '<div class="checkbox"><label><input class="checkboxInput" type="checkbox" ng-click="checkboxClick($event, getPropertyForObject(option,settings.idProp))" ng-checked="isChecked(getPropertyForObject(option,settings.idProp))" /> <span dm-dropdown-static-include="{{settings.template}}"></div></label></span></a>'; 58 } else { 59 template += '<span data-ng-class="{\'glyphicon glyphicon-ok\': isChecked(getPropertyForObject(option,settings.idProp))}"> </span> <span dm-dropdown-static-include="{{settings.template}}"></span></a>'; 60 } 61 62 template += '</li>'; 63 64 template += '<li class="divider" ng-show="settings.selectionLimit > 1"></li>'; 65 template += '<li role="presentation" ng-show="settings.selectionLimit > 1"><a role="menuitem">{{selectedModel.length}} {{texts.selectionOf}} {{settings.selectionLimit}} {{texts.selectionCount}}</a></li>'; 66 67 template += '</ul>'; 68 template += '</div>'; 69 70 return template; 71 }, 72 link: function($scope, $element, $attrs) { 73 var $dropdownTrigger = $element.children()[0]; 74 75 $scope.toggleDropdown = function() { 76 if ($scope.open) { 77 $scope.close() 78 } else { $scope.open = true } 79 if ($scope.settings.keyboardControls) { 80 if ($scope.open) { 81 if ($scope.settings.selectionLimit === 1 && $scope.settings.enableSearch) { 82 setTimeout(function() { 83 angular.element($element)[0].querySelector('.searchField').focus(); 84 }, 0); 85 } else { 86 focusFirstOption(); 87 } 88 } 89 } 90 if ($scope.settings.enableSearch) { 91 if ($scope.open) { 92 setTimeout(function () { 93 angular.element($element)[0].querySelector('.searchField').focus(); 94 }, 0); 95 } 96 } 97 }; 98 99 $scope.checkboxClick = function($event, id) { 100 $scope.setSelectedItem(id, false, true); 101 $event.stopImmediatePropagation(); 102 }; 103 104 $scope.externalEvents = { 105 onItemSelect: angular.noop, 106 onItemDeselect: angular.noop, 107 onSelectAll: angular.noop, 108 onDeselectAll: angular.noop, 109 onInitDone: angular.noop, 110 onMaxSelectionReached: angular.noop, 111 onSelectionChanged: angular.noop, 112 onClose: angular.noop 113 }; 114 115 $scope.settings = { 116 dynamicTitle: true, 117 scrollable: false, 118 scrollableHeight: '300px', 119 closeOnBlur: true, 120 displayProp: 'label', 121 idProp: 'id', 122 externalIdProp: 'id', 123 enableSearch: false, 124 selectionLimit: 0, 125 showCheckAll: true, 126 showUncheckAll: true,
127 showEnableSearchButton: false, 128 closeOnSelect: false, 129 buttonClasses: 'btn btn-default', 130 closeOnDeselect: false, 131 groupBy: $attrs.groupBy || undefined, 132 groupByTextProvider: null, 133 smartButtonMaxItems: 0, 134 smartButtonTextConverter: angular.noop, 135 styleActive: false, 136 keyboardControls: false, 137 template: '{{getPropertyForObject(option, settings.displayProp)}}', 138 searchField: '$', 139 showAllSelectedText: false 140 }; 141 142 $scope.texts = { 143 checkAll: 'Check All', 144 uncheckAll: 'Uncheck All', 145 selectionCount: 'checked', 146 selectionOf: '/', 147 searchPlaceholder: 'Search...', 148 buttonDefaultText: $scope.buttonDefaultText ? $scope.buttonDefaultText : 'Select', 149 dynamicButtonTextSuffix: 'checked', 150 disableSearch: 'Disable search', 151 enableSearch: 'Enable search', 152 selectGroup: 'Select all:', 153 allSelectedText: 'All' 154 }; 155 156 $scope.input = { 157 searchFilter: $scope.searchFilter || '' 158 }; 159 160 if (angular.isDefined($scope.settings.groupBy)) { 161 $scope.$watch('options', function(newValue) { 162 if (angular.isDefined(newValue)) { 163 $scope.orderedItems = $filter('orderBy')(newValue, $scope.settings.groupBy); 164 } 165 }); 166 } 167 168 $scope.$watch('selectedModel', function(newValue) { 169 if (!Array.isArray(newValue)) { 170 $scope.singleSelection = true; 171 } else { 172 $scope.singleSelection = false; 173 } 174 }); 175 176 $scope.close = function() { 177 $scope.open = false; 178 $scope.externalEvents.onClose(); 179 } 180 181 $scope.selectCurrentGroup = function(currentGroup) { 182 $scope.selectedModel.splice(0, $scope.selectedModel.length); 183 if ($scope.orderedItems) { 184 $scope.orderedItems.forEach(function(item) { 185 if (item[$scope.groupBy] === currentGroup) { 186 $scope.setSelectedItem($scope.getPropertyForObject(item, $scope.settings.idProp), false, false) 187 } 188 }); 189 } 190 $scope.externalEvents.onSelectionChanged(); 191 }; 192 193 angular.extend($scope.settings, $scope.extraSettings || []); 194 angular.extend($scope.externalEvents, $scope.events || []); 195 angular.extend($scope.texts, $scope.translationTexts); 196 197 $scope.singleSelection = $scope.settings.selectionLimit === 1; 198 199 function getFindObj(id) { 200 var findObj = {}; 201 202 if ($scope.settings.externalIdProp === '') { 203 findObj[$scope.settings.idProp] = id; 204 } else { 205 findObj[$scope.settings.externalIdProp] = id; 206 } 207 208 return findObj; 209 } 210 211 function clearObject(object) { 212 for (var prop in object) { 213 delete object[prop]; 214 } 215 } 216 217 if ($scope.singleSelection) { 218 if (angular.isArray($scope.selectedModel) && $scope.selectedModel.length === 0) { 219 clearObject($scope.selectedModel); 220 } 221 } 222 223 if ($scope.settings.closeOnBlur) { 224 $document.on('click', function(e) { 225 if ($scope.open) { 226 var target = e.target.parentElement; 227 var parentFound = false; 228 229 while (angular.isDefined(target) && target !== null && !parentFound) { 230 if (!!target.className.split && contains(target.className.split(' '), 'multiselect-parent') && !parentFound) { 231 if (target === $dropdownTrigger) { 232 parentFound = true; 233 } 234 } 235 target = target.parentElement; 236 } 237 238 if (!parentFound) { 239 $scope.$apply(function() { 240 $scope.close(); 241 }); 242 } 243 } 244 }); 245 } 246 247 $scope.getGroupLabel = function(groupValue) { 248 if ($scope.settings.groupByTextProvider !== null) { 249 return $scope.settings.groupByTextProvider(groupValue); 250 } 251 252 return groupValue; 253 }; 254 255 function textWidth(text) { 256 var $btn = $element.find('button'); 257 var canvas = document.createElement('canvas'); 258 var ctx = canvas.getContext('2d'); 259 ctx.font = $btn.css('font-size') + $btn.css('font-family'); 260 // http://stackoverflow.com/questions/38823353/chrome-canvas-2d-context-measuretext-giving-me-weird-results 261 ctx.originalFont = $btn.css('font-size') + $btn.css('font-family'); 262 ctx.fillStyle = '#000000'; 263 return ctx.measureText(text).width; 264 } 265 266 $scope.getButtonText = function() { 267 if ($scope.settings.dynamicTitle && $scope.selectedModel && ($scope.selectedModel.length > 0 || (angular.isObject($scope.selectedModel) && Object.keys($scope.selectedModel).length > 0))) { 268 if ($scope.settings.smartButtonMaxItems > 0) { 269 270 var paddingWidth = 12 * 2, 271 borderWidth = 1 * 2, 272 dropdownIconWidth = 8; 273 var widthLimit = $element[0].offsetWidth - paddingWidth - borderWidth - dropdownIconWidth; 274 275 var itemsText = []; 276 277 angular.forEach($scope.options, function(optionItem) { 278 if ($scope.isChecked($scope.getPropertyForObject(optionItem, $scope.settings.idProp))) { 279 var displayText = $scope.getPropertyForObject(optionItem, $scope.settings.displayProp); 280 var converterResponse = $scope.settings.smartButtonTextConverter(displayText, optionItem); 281 282 itemsText.push(converterResponse ? converterResponse : displayText); 283 } 284 }); 285 286 if ($scope.selectedModel.length > $scope.settings.smartButtonMaxItems) { 287 itemsText = itemsText.slice(0, $scope.settings.smartButtonMaxItems); 288 itemsText.push('...'); 289 } 290 291 var result = itemsText.join(', '); 292 var index = result.length - 4; 293 if ($element[0].offsetWidth === 0) 294 return result; 295 while (textWidth(result) > widthLimit && index > 0) { 296 if (itemsText[itemsText.length - 1] !== "...") { 297 itemsText.push('...'); 298 result = result + "..."; 299 } 300 result = result.slice(0, index) + result.slice(index + 1); 301 index--; 302 } 303 304 return result; 305 } else { 306 var totalSelected; 307 308 if ($scope.options.length === $scope.selectedModel.length){ 309 itemsText = "All Selected"; 310 return itemsText; 311 } 312 313 if ($scope.singleSelection) { 314 totalSelected = ($scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.idProp])) ? 1 : 0; 315 } else { 316 totalSelected = angular.isDefined($scope.selectedModel) ? $scope.selectedModel.length : 0; 317 } 318 319 if (totalSelected === 0) { 320 return $scope.texts.buttonDefaultText; 321 } 322 323 if ($scope.settings.showAllSelectedText && totalSelected === $scope.options.length) { 324 return $scope.texts.allSelectedText; 325 } 326 327 return totalSelected + ' ' + $scope.texts.dynamicButtonTextSuffix; 328 } 329 } else { 330 return $scope.texts.buttonDefaultText; 331 } 332 }; 333 334 $scope.getPropertyForObject = function(object, property) { 335 if (angular.isDefined(object) && object.hasOwnProperty(property)) { 336 return object[property]; 337 } 338 339 return undefined; 340 }; 341 342 $scope.selectAll = function() { 343 var searchResult; 344 $scope.deselectAll(true); 345 $scope.externalEvents.onSelectAll(); 346 347 searchResult = $filter('filter')($scope.options, $scope.getFilter($scope.input.searchFilter)); 348 angular.forEach(searchResult, function(value) { 349 $scope.setSelectedItem(value[$scope.settings.idProp], true, false); 350 }); 351 $scope.externalEvents.onSelectionChanged(); 352 $scope.selectedGroup = null; 353 }; 354 355 $scope.deselectAll = function(dontSendEvent) { 356 dontSendEvent = dontSendEvent || false; 357 358 if (!dontSendEvent) { 359 $scope.externalEvents.onDeselectAll(); 360 } 361 362 if ($scope.singleSelection) { 363 clearObject($scope.selectedModel); 364 } else { 365 $scope.selectedModel.splice(0, $scope.selectedModel.length); 366 } 367 if (!dontSendEvent) { 368 $scope.externalEvents.onSelectionChanged(); 369 } 370 $scope.selectedGroup = null; 371 }; 372 373 $scope.setSelectedItem = function(id, dontRemove, fireSelectionChange) { 374 var findObj = getFindObj(id); 375 var finalObj = null; 376 377 if ($scope.settings.externalIdProp === '') { 378 finalObj = find($scope.options, findObj); 379 } else { 380 finalObj = findObj; 381 } 382 383 if ($scope.singleSelection) { 384 clearObject($scope.selectedModel); 385 angular.extend($scope.selectedModel, finalObj); 386 if (fireSelectionChange) { 387 $scope.externalEvents.onItemSelect(finalObj); 388 } 389 if ($scope.settings.closeOnSelect || $scope.settings.closeOnDeselect) $scope.close(); 390 } else { 391 dontRemove = dontRemove || false; 392 393 var exists = findIndex($scope.selectedModel, findObj) !== -1; 394 395 if (!dontRemove && exists) { 396 $scope.selectedModel.splice(findIndex($scope.selectedModel, findObj), 1); 397 $scope.externalEvents.onItemDeselect(findObj); 398 if ($scope.settings.closeOnDeselect) $scope.close(); 399 } else if (!exists && ($scope.settings.selectionLimit === 0 || $scope.selectedModel.length < $scope.settings.selectionLimit)) { 400 $scope.selectedModel.push(finalObj); 401 if (fireSelectionChange) { 402 $scope.externalEvents.onItemSelect(finalObj); 403 } 404 if ($scope.settings.closeOnSelect) $scope.close(); 405 if ($scope.settings.selectionLimit > 0 && $scope.selectedModel.length === $scope.settings.selectionLimit) { 406 $scope.externalEvents.onMaxSelectionReached(); 407 } 408 } 409 } 410 if (fireSelectionChange) { 411 $scope.externalEvents.onSelectionChanged(); 412 } 413 $scope.selectedGroup = null; 414 }; 415 416 $scope.isChecked = function(id) { 417 if ($scope.singleSelection) { 418 if ($scope.settings.externalIdProp === '') { 419 return $scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.idProp]) && $scope.selectedModel[$scope.settings.idProp] === getFindObj(id)[$scope.settings.idProp]; 420 } 421 return $scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.externalIdProp]) && $scope.selectedModel[$scope.settings.externalIdProp] === getFindObj(id)[$scope.settings.externalIdProp]; 422 } 423 424 return findIndex($scope.selectedModel, getFindObj(id)) !== -1; 425 }; 426 427 $scope.externalEvents.onInitDone(); 428 429 $scope.keyDownLink = function(event) { 430 var sourceScope = angular.element(event.target).scope(); 431 var nextOption; 432 var parent = event.target.parentNode; 433 if (!$scope.settings.keyboardControls) { 434 return; 435 } 436 if (event.keyCode === 13 || event.keyCode === 32) { // enter 437 event.preventDefault(); 438 if (!!sourceScope.option) { 439 $scope.setSelectedItem($scope.getPropertyForObject(sourceScope.option, $scope.settings.idProp), false, true); 440 } else if (event.target.id === 'deselectAll') { 441 $scope.deselectAll(); 442 } else if (event.target.id === 'selectAll') { 443 $scope.selectAll(); 444 } 445 } else if (event.keyCode === 38) { // up arrow 446 event.preventDefault(); 447 if (!!parent.previousElementSibling) { 448 nextOption = parent.previousElementSibling.querySelector('a') || parent.previousElementSibling.querySelector('input'); 449 } 450 while (!nextOption && !!parent) { 451 parent = parent.previousElementSibling; 452 if (!!parent) { 453 nextOption = parent.querySelector('a') || parent.querySelector('input'); 454 } 455 } 456 if (!!nextOption) { 457 nextOption.focus(); 458 } 459 } else if (event.keyCode === 40) { // down arrow 460 event.preventDefault(); 461 if (!!parent.nextElementSibling) { 462 nextOption = parent.nextElementSibling.querySelector('a') || parent.nextElementSibling.querySelector('input'); 463 } 464 while (!nextOption && !!parent) { 465 parent = parent.nextElementSibling; 466 if (!!parent) { 467 nextOption = parent.querySelector('a') || parent.querySelector('input'); 468 } 469 } 470 if (!!nextOption) { 471 nextOption.focus(); 472 } 473 } else if (event.keyCode === 27) { 474 event.preventDefault(); 475 476 $scope.toggleDropdown(); 477 } 478 }; 479 480 $scope.keyDownSearchDefault = function(event) { 481 var parent = event.target.parentNode.parentNode; 482 var nextOption; 483 if (!$scope.settings.keyboardControls) { 484 return; 485 } 486 if (event.keyCode === 9 || event.keyCode === 40) { //tab 487 event.preventDefault(); 488 focusFirstOption(); 489 } else if (event.keyCode === 38) { 490 event.preventDefault(); 491 if (!!parent.previousElementSibling) { 492 nextOption = parent.previousElementSibling.querySelector('a') || parent.previousElementSibling.querySelector('input'); 493 } 494 while (!nextOption && !!parent) { 495 parent = parent.previousElementSibling; 496 if (!!parent) { 497 nextOption = parent.querySelector('a') || parent.querySelector('input'); 498 } 499 } 500 if (!!nextOption) { 501 nextOption.focus(); 502 } 503 } else if (event.keyCode === 27) { 504 event.preventDefault(); 505 506 $scope.toggleDropdown(); 507 } 508 }; 509 510 $scope.keyDownSearch = function(event, searchFilter) { 511 var searchResult; 512 if (!$scope.settings.keyboardControls) { 513 return; 514 } 515 if (event.keyCode === 13) { 516 if ($scope.settings.selectionLimit === 1 && $scope.settings.enableSearch) { 517 searchResult = $filter('filter')($scope.options, $scope.getFilter(searchFilter)); 518 if (searchResult.length === 1) { 519 $scope.setSelectedItem($scope.getPropertyForObject(searchResult[0], $scope.settings.idProp), false, true); 520 } 521 } else if ($scope.settings.enableSearch) { 522 $scope.selectAll(); 523 } 524 } 525 }; 526 527 $scope.getFilter = function(searchFilter) { 528 var filter = {}; 529 filter[$scope.settings.searchField] = searchFilter; 530 return filter; 531 }; 532 533 $scope.toggleSearch = function($event) { 534 if ($event) { 535 $event.stopPropagation(); 536 } 537 $scope.settings.enableSearch = !$scope.settings.enableSearch; 538 if (!$scope.settings.enableSearch) { 539 $scope.input.searchFilter = ''; 540 } 541 }; 542 543 $scope.keyDownToggleSearch = function() { 544 if (!$scope.settings.keyboardControls) { 545 return; 546 } 547 if (event.keyCode === 13) { 548 $scope.toggleSearch(); 549 if ($scope.settings.enableSearch) { 550 setTimeout( 551 function() { 552 angular.element($element)[0].querySelector('.searchField').focus(); 553 }, 0 554 ); 555 } else { 556 focusFirstOption(); 557 } 558 } 559 }; 560 561 function focusFirstOption() { 562 setTimeout(function() { 563 var elementToFocus = angular.element($element)[0].querySelector('.option'); 564 if (angular.isDefined(elementToFocus) && elementToFocus != null) { 565 elementToFocus.focus(); 566 } 567 }, 0); 568 } 569 } 570 }; 571 }]); 572 573 function contains(collection, target) { 574 var containsTarget = false;
575 collection.some(function(object) { 576 if (object === target) { 577 containsTarget = true; 578 return true; 579 } 580 }); 581 return containsTarget; 582 } 583 584 function find(collection, properties) { 585 var target; 586 587 collection.some(function(object) { 588 var hasAllSameProperties = true; 589 Object.keys(properties).forEach(function(key) { 590 if (object[key] !== properties[key]) { 591 hasAllSameProperties = false; 592 } 593 }); 594 if (hasAllSameProperties) { 595 target = object; 596 return true 597 } 598 }); 599 600 return target; 601 } 602 603 function findIndex(collection, properties) { 604 var index = -1; 605 var counter = -1; 606 607 collection.some(function(object) { 608 var hasAllSameProperties = true; 609 counter += 1; 610 Object.keys(properties).forEach(function(key) { 611 if (object[key] !== properties[key]) { 612 hasAllSameProperties = false; 613 } 614 }); 615 if (hasAllSameProperties) { 616 index = counter; 617 return true 618 } 619 }); 620 621 return index; 622 } 623})(angular);
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.