1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131
|
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Class for rendering the results of an auto complete and
* allow the user to select an row.
*
*/
goog.provide('goog.ui.ac.Renderer');
goog.provide('goog.ui.ac.Renderer.CustomRenderer');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.a11y.aria.State');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dispose');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.fx.dom.FadeInAndShow');
goog.require('goog.fx.dom.FadeOutAndHide');
goog.require('goog.positioning');
goog.require('goog.positioning.Corner');
goog.require('goog.positioning.Overflow');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.IdGenerator');
goog.require('goog.ui.ac.AutoComplete');
/**
* Class for rendering the results of an auto-complete in a drop down list.
*
* @constructor
* @param {Element=} opt_parentNode optional reference to the parent element
* that will hold the autocomplete elements. goog.dom.getDocument().body
* will be used if this is null.
* @param {?({renderRow}|{render})=} opt_customRenderer Custom full renderer to
* render each row. Should be something with a renderRow or render method.
* @param {boolean=} opt_rightAlign Determines if the autocomplete will always
* be right aligned. False by default.
* @param {boolean=} opt_useStandardHighlighting Determines if standard
* highlighting should be applied to each row of data. Standard highlighting
* bolds every matching substring for a given token in each row. True by
* default.
* @extends {goog.events.EventTarget}
* @suppress {underscore}
*/
goog.ui.ac.Renderer = function(
opt_parentNode, opt_customRenderer, opt_rightAlign,
opt_useStandardHighlighting) {
goog.ui.ac.Renderer.base(this, 'constructor');
/**
* Reference to the parent element that will hold the autocomplete elements
* @type {Element}
* @private
*/
this.parent_ = opt_parentNode || goog.dom.getDocument().body;
/**
* Dom helper for the parent element's document.
* @type {goog.dom.DomHelper}
* @private
*/
this.dom_ = goog.dom.getDomHelper(this.parent_);
/**
* Whether to reposition the autocomplete UI below the target node
* @type {boolean}
* @private
*/
this.reposition_ = !opt_parentNode;
/**
* Reference to the main element that controls the rendered autocomplete
* @type {Element}
* @private
*/
this.element_ = null;
/**
* The current token that has been entered
* @type {string}
* @private
*/
this.token_ = '';
/**
* Array used to store the current set of rows being displayed
* @type {Array<!Object>}
* @private
*/
this.rows_ = [];
/**
* Array of the node divs that hold each result that is being displayed.
* @type {Array<Element>}
* @protected
* @suppress {underscore|visibility}
*/
this.rowDivs_ = [];
/**
* The index of the currently highlighted row
* @type {number}
* @protected
* @suppress {underscore|visibility}
*/
this.hilitedRow_ = -1;
/**
* The time that the rendering of the menu rows started
* @type {number}
* @protected
* @suppress {underscore|visibility}
*/
this.startRenderingRows_ = -1;
/**
* Store the current state for the renderer
* @type {boolean}
* @private
*/
this.visible_ = false;
/**
* Classname for the main element. This must be a single valid class name.
* @type {string}
*/
this.className = goog.getCssName('ac-renderer');
/**
* Classname for row divs. This must be a single valid class name.
* @type {string}
*/
this.rowClassName = goog.getCssName('ac-row');
// TODO(gboyer): Remove this as soon as we remove references and ensure that
// no groups are pushing javascript using this.
/**
* The old class name for active row. This name is deprecated because its
* name is generic enough that a typical implementation would require a
* descendant selector.
* Active row will have rowClassName & activeClassName &
* legacyActiveClassName.
* @type {string}
* @private
*/
this.legacyActiveClassName_ = goog.getCssName('active');
/**
* Class name for active row div. This must be a single valid class name.
* Active row will have rowClassName & activeClassName &
* legacyActiveClassName.
* @type {string}
*/
this.activeClassName = goog.getCssName('ac-active');
/**
* Class name for the bold tag highlighting the matched part of the text.
* @type {string}
*/
this.highlightedClassName = goog.getCssName('ac-highlighted');
/**
* Custom full renderer
* @type {?({renderRow}|{render})}
* @private
*/
this.customRenderer_ = opt_customRenderer || null;
/**
* Flag to indicate whether standard highlighting should be applied.
* this is set to true if left unspecified to retain existing
* behaviour for autocomplete clients
* @type {boolean}
* @private
*/
this.useStandardHighlighting_ =
opt_useStandardHighlighting != null ? opt_useStandardHighlighting : true;
/**
* Flag to indicate whether matches should be done on whole words instead
* of any string.
* @type {boolean}
* @private
*/
this.matchWordBoundary_ = true;
/**
* Flag to set all tokens as highlighted in the autocomplete row.
* @type {boolean}
* @private
*/
this.highlightAllTokens_ = false;
/**
* Determines if the autocomplete will always be right aligned
* @type {boolean}
* @private
*/
this.rightAlign_ = !!opt_rightAlign;
/**
* Whether to align with top of target field
* @type {boolean}
* @private
*/
this.topAlign_ = false;
/**
* Duration (in msec) of fade animation when menu is shown/hidden.
* Setting to 0 (default) disables animation entirely.
* @type {number}
* @private
*/
this.menuFadeDuration_ = 0;
/**
* Whether we should limit the dropdown from extending past the bottom of the
* screen and instead show a scrollbar on the dropdown.
* @type {boolean}
* @private
*/
this.showScrollbarsIfTooLarge_ = false;
/**
* Animation in progress, if any.
* @type {goog.fx.Animation|undefined}
*/
this.animation_;
};
goog.inherits(goog.ui.ac.Renderer, goog.events.EventTarget);
/**
* The anchor element to position the rendered autocompleter against.
* @type {Element}
* @private
*/
goog.ui.ac.Renderer.prototype.anchorElement_;
/**
* The anchor element to position the rendered autocompleter against.
* @protected {Element|undefined}
*/
goog.ui.ac.Renderer.prototype.target_;
/**
* The element on which to base the width of the autocomplete.
* @protected {Node}
*/
goog.ui.ac.Renderer.prototype.widthProvider_;
/**
* The border width of the autocomplete dropdown, only used in calculating the
* dropdown width.
* @private {number}
*/
goog.ui.ac.Renderer.prototype.borderWidth_ = 0;
/**
* A flag used to make sure we highlight only one match in the rendered row.
* @private {boolean}
*/
goog.ui.ac.Renderer.prototype.wasHighlightedAtLeastOnce_;
/**
* The delay before mouseover events are registered, in milliseconds
* @type {number}
* @const
*/
goog.ui.ac.Renderer.DELAY_BEFORE_MOUSEOVER = 300;
/**
* Gets the renderer's element.
* @return {Element} The main element that controls the rendered autocomplete.
*/
goog.ui.ac.Renderer.prototype.getElement = function() {
return this.element_;
};
/**
* Sets the width provider element. The provider is only used on redraw and as
* such will not automatically update on resize.
* @param {Node} widthProvider The element whose width should be mirrored.
* @param {number=} opt_borderWidth The with of the border of the autocomplete,
* which will be subtracted from the width of the autocomplete dropdown.
*/
goog.ui.ac.Renderer.prototype.setWidthProvider = function(
widthProvider, opt_borderWidth) {
this.widthProvider_ = widthProvider;
if (opt_borderWidth) {
this.borderWidth_ = opt_borderWidth;
}
};
/**
* Set whether to align autocomplete to top of target element
* @param {boolean} align If true, align to top.
*/
goog.ui.ac.Renderer.prototype.setTopAlign = function(align) {
this.topAlign_ = align;
};
/**
* @return {boolean} Whether we should be aligning to the top of
* the target element.
*/
goog.ui.ac.Renderer.prototype.getTopAlign = function() {
return this.topAlign_;
};
/**
* Set whether to align autocomplete to the right of the target element.
* @param {boolean} align If true, align to right.
*/
goog.ui.ac.Renderer.prototype.setRightAlign = function(align) {
this.rightAlign_ = align;
};
/**
* @return {boolean} Whether the autocomplete menu should be right aligned.
*/
goog.ui.ac.Renderer.prototype.getRightAlign = function() {
return this.rightAlign_;
};
/**
* @param {boolean} show Whether we should limit the dropdown from extending
* past the bottom of the screen and instead show a scrollbar on the
* dropdown.
*/
goog.ui.ac.Renderer.prototype.setShowScrollbarsIfTooLarge = function(show) {
this.showScrollbarsIfTooLarge_ = show;
};
/**
* Set whether or not standard highlighting should be used when rendering rows.
* @param {boolean} useStandardHighlighting true if standard highlighting used.
*/
goog.ui.ac.Renderer.prototype.setUseStandardHighlighting = function(
useStandardHighlighting) {
this.useStandardHighlighting_ = useStandardHighlighting;
};
/**
* @param {boolean} matchWordBoundary Determines whether matches should be
* higlighted only when the token matches text at a whole-word boundary.
* True by default.
*/
goog.ui.ac.Renderer.prototype.setMatchWordBoundary = function(
matchWordBoundary) {
this.matchWordBoundary_ = matchWordBoundary;
};
/**
* Set whether or not to highlight all matching tokens rather than just the
* first.
* @param {boolean} highlightAllTokens Whether to highlight all matching tokens
* rather than just the first.
*/
goog.ui.ac.Renderer.prototype.setHighlightAllTokens = function(
highlightAllTokens) {
this.highlightAllTokens_ = highlightAllTokens;
};
/**
* Sets the duration (in msec) of the fade animation when menu is shown/hidden.
* Setting to 0 (default) disables animation entirely.
* @param {number} duration Duration (in msec) of the fade animation (or 0 for
* no animation).
*/
goog.ui.ac.Renderer.prototype.setMenuFadeDuration = function(duration) {
this.menuFadeDuration_ = duration;
};
/**
* Sets the anchor element for the subsequent call to renderRows.
* @param {Element} anchor The anchor element.
*/
goog.ui.ac.Renderer.prototype.setAnchorElement = function(anchor) {
this.anchorElement_ = anchor;
};
/**
* @return {Element} The anchor element.
* @protected
*/
goog.ui.ac.Renderer.prototype.getAnchorElement = function() {
return this.anchorElement_;
};
/**
* Render the autocomplete UI
*
* @param {Array<!Object>} rows Matching UI rows.
* @param {string} token Token we are currently matching against.
* @param {Element=} opt_target Current HTML node, will position popup beneath
* this node.
*/
goog.ui.ac.Renderer.prototype.renderRows = function(rows, token, opt_target) {
this.token_ = token;
this.rows_ = rows;
this.hilitedRow_ = -1;
this.startRenderingRows_ = goog.now();
this.target_ = opt_target;
this.rowDivs_ = [];
this.redraw();
};
/**
* Hide the object.
*/
goog.ui.ac.Renderer.prototype.dismiss = function() {
if (this.visible_) {
this.visible_ = false;
this.toggleAriaMarkup_(false /* isShown */);
if (this.menuFadeDuration_ > 0) {
goog.dispose(this.animation_);
this.animation_ =
new goog.fx.dom.FadeOutAndHide(this.element_, this.menuFadeDuration_);
this.animation_.play();
} else {
goog.style.setElementShown(this.element_, false);
}
}
};
/**
* Show the object.
*/
goog.ui.ac.Renderer.prototype.show = function() {
if (!this.visible_) {
this.visible_ = true;
this.toggleAriaMarkup_(true /* isShown */);
if (this.menuFadeDuration_ > 0) {
goog.dispose(this.animation_);
this.animation_ =
new goog.fx.dom.FadeInAndShow(this.element_, this.menuFadeDuration_);
this.animation_.play();
} else {
goog.style.setElementShown(this.element_, true);
}
}
};
/**
* Toggle the ARIA markup to add popup semantics when the target is shown and
* to remove them when it is hidden.
* @param {boolean} isShown Whether the menu is being shown.
* @private
*/
goog.ui.ac.Renderer.prototype.toggleAriaMarkup_ = function(isShown) {
if (!this.target_) {
return;
}
goog.a11y.aria.setState(this.target_, goog.a11y.aria.State.HASPOPUP, isShown);
goog.a11y.aria.setState(
goog.asserts.assert(this.element_), goog.a11y.aria.State.EXPANDED,
isShown);
goog.a11y.aria.setState(this.target_, goog.a11y.aria.State.EXPANDED, isShown);
if (isShown) {
goog.a11y.aria.setState(
this.target_, goog.a11y.aria.State.OWNS, this.element_.id);
} else {
goog.a11y.aria.removeState(this.target_, goog.a11y.aria.State.OWNS);
goog.a11y.aria.setActiveDescendant(this.target_, null);
}
};
/**
* @return {boolean} True if the object is visible.
*/
goog.ui.ac.Renderer.prototype.isVisible = function() {
return this.visible_;
};
/**
* Sets the 'active' class of the nth item.
* @param {number} index Index of the item to highlight.
*/
goog.ui.ac.Renderer.prototype.hiliteRow = function(index) {
var row =
index >= 0 && index < this.rows_.length ? this.rows_[index] : undefined;
var rowDiv = index >= 0 && index < this.rowDivs_.length ?
this.rowDivs_[index] :
undefined;
var evtObj = /** @lends {goog.events.Event.prototype} */ ({
type: goog.ui.ac.AutoComplete.EventType.ROW_HILITE,
rowNode: rowDiv,
row: row ? row.data : null
});
if (this.dispatchEvent(evtObj)) {
this.hiliteNone();
this.hilitedRow_ = index;
if (rowDiv) {
goog.dom.classlist.addAll(
rowDiv, [this.activeClassName, this.legacyActiveClassName_]);
if (this.target_) {
goog.a11y.aria.setActiveDescendant(this.target_, rowDiv);
}
goog.style.scrollIntoContainerView(rowDiv, this.element_);
}
}
};
/**
* Removes the 'active' class from the currently selected row.
*/
goog.ui.ac.Renderer.prototype.hiliteNone = function() {
if (this.hilitedRow_ >= 0) {
goog.dom.classlist.removeAll(
goog.asserts.assert(this.rowDivs_[this.hilitedRow_]),
[this.activeClassName, this.legacyActiveClassName_]);
}
};
/**
* Sets the 'active' class of the item with a given id.
* @param {number} id Id of the row to hilight. If id is -1 then no rows get
* hilited.
*/
goog.ui.ac.Renderer.prototype.hiliteId = function(id) {
if (id == -1) {
this.hiliteRow(-1);
} else {
for (var i = 0; i < this.rows_.length; i++) {
if (this.rows_[i].id == id) {
this.hiliteRow(i);
return;
}
}
}
};
/**
* Sets CSS classes on autocomplete conatainer element.
*
* @param {Element} elem The container element.
* @private
*/
goog.ui.ac.Renderer.prototype.setMenuClasses_ = function(elem) {
goog.asserts.assert(elem);
// Legacy clients may set the renderer's className to a space-separated list
// or even have a trailing space.
goog.dom.classlist.addAll(elem, goog.string.trim(this.className).split(' '));
};
/**
* If the main HTML element hasn't been made yet, creates it and appends it
* to the parent.
* @private
*/
goog.ui.ac.Renderer.prototype.maybeCreateElement_ = function() {
if (!this.element_) {
// Make element and add it to the parent
var el = this.dom_.createDom(goog.dom.TagName.DIV, {style: 'display:none'});
if (this.showScrollbarsIfTooLarge_) {
// Make sure that the dropdown will get scrollbars if it isn't large
// enough to show all rows.
el.style.overflowY = 'auto';
}
this.element_ = el;
this.setMenuClasses_(el);
goog.a11y.aria.setRole(el, goog.a11y.aria.Role.LISTBOX);
el.id = goog.ui.IdGenerator.getInstance().getNextUniqueId();
this.dom_.appendChild(this.parent_, el);
// Add this object as an event handler
goog.events.listen(
el, goog.events.EventType.CLICK, this.handleClick_, false, this);
goog.events.listen(
el, goog.events.EventType.MOUSEDOWN, this.handleMouseDown_, false,
this);
goog.events.listen(
el, goog.events.EventType.MOUSEOVER, this.handleMouseOver_, false,
this);
}
};
/**
* Redraw (or draw if this is the first call) the rendered auto-complete drop
* down.
*/
goog.ui.ac.Renderer.prototype.redraw = function() {
// Create the element if it doesn't yet exist
this.maybeCreateElement_();
// For top aligned with target (= bottom aligned element),
// we need to hide and then add elements while hidden to prevent
// visible repositioning
if (this.topAlign_) {
this.element_.style.visibility = 'hidden';
}
if (this.widthProvider_) {
var width = this.widthProvider_.clientWidth - this.borderWidth_ + 'px';
this.element_.style.minWidth = width;
}
// Remove the current child nodes
this.rowDivs_.length = 0;
this.dom_.removeChildren(this.element_);
// Generate the new rows (use forEach so we can change rows_ from an
// array to a different datastructure if required)
if (this.customRenderer_ && this.customRenderer_.render) {
this.customRenderer_.render(this, this.element_, this.rows_, this.token_);
} else {
var curRow = null;
goog.array.forEach(this.rows_, function(row) {
row = this.renderRowHtml(row, this.token_);
if (this.topAlign_) {
// Aligned with top of target = best match at bottom
this.element_.insertBefore(row, curRow);
} else {
this.dom_.appendChild(this.element_, row);
}
curRow = row;
}, this);
}
// Don't show empty result sets
if (this.rows_.length == 0) {
this.dismiss();
return;
} else {
this.show();
}
this.reposition();
// Make the autocompleter unselectable, so that it
// doesn't steal focus from the input field when clicked.
goog.style.setUnselectable(this.element_, true);
};
/**
* @return {goog.positioning.Corner} The anchor corner to position the popup at.
* @protected
*/
goog.ui.ac.Renderer.prototype.getAnchorCorner = function() {
var anchorCorner = this.rightAlign_ ? goog.positioning.Corner.BOTTOM_RIGHT :
goog.positioning.Corner.BOTTOM_LEFT;
if (this.topAlign_) {
anchorCorner = goog.positioning.flipCornerVertical(anchorCorner);
}
return anchorCorner;
};
/**
* Repositions the auto complete popup relative to the location node, if it
* exists and the auto position has been set.
*/
goog.ui.ac.Renderer.prototype.reposition = function() {
if (this.target_ && this.reposition_) {
var anchorElement = this.anchorElement_ || this.target_;
var anchorCorner = this.getAnchorCorner();
var overflowMode = goog.positioning.Overflow.ADJUST_X_EXCEPT_OFFSCREEN;
if (this.showScrollbarsIfTooLarge_) {
// positionAtAnchor will set the height of this.element_ when it runs
// (because of RESIZE_HEIGHT), and it will never increase it relative to
// its current value when it runs again. But if the user scrolls their
// page, then we might actually want a bigger height when the dropdown is
// displayed next time. So we clear the height before calling
// positionAtAnchor, so it is free to set the height as large as it
// chooses.
this.element_.style.height = '';
overflowMode |= goog.positioning.Overflow.RESIZE_HEIGHT;
}
goog.positioning.positionAtAnchor(
anchorElement, anchorCorner, this.element_,
goog.positioning.flipCornerVertical(anchorCorner), null, null,
overflowMode);
if (this.topAlign_) {
// This flickers, but is better than the alternative of positioning
// in the wrong place and then moving.
this.element_.style.visibility = 'visible';
}
}
};
/**
* Sets whether the renderer should try to determine where to position the
* drop down.
* @param {boolean} auto Whether to autoposition the drop down.
*/
goog.ui.ac.Renderer.prototype.setAutoPosition = function(auto) {
this.reposition_ = auto;
};
/**
* @return {boolean} Whether the drop down will be autopositioned.
* @protected
*/
goog.ui.ac.Renderer.prototype.getAutoPosition = function() {
return this.reposition_;
};
/**
* @return {Element} The target element.
* @protected
*/
goog.ui.ac.Renderer.prototype.getTarget = function() {
return this.target_ || null;
};
/**
* Disposes of the renderer and its associated HTML.
* @override
* @protected
*/
goog.ui.ac.Renderer.prototype.disposeInternal = function() {
if (this.element_) {
goog.events.unlisten(
this.element_, goog.events.EventType.CLICK, this.handleClick_, false,
this);
goog.events.unlisten(
this.element_, goog.events.EventType.MOUSEDOWN, this.handleMouseDown_,
false, this);
goog.events.unlisten(
this.element_, goog.events.EventType.MOUSEOVER, this.handleMouseOver_,
false, this);
this.dom_.removeNode(this.element_);
this.element_ = null;
this.visible_ = false;
}
goog.dispose(this.animation_);
this.parent_ = null;
goog.ui.ac.Renderer.base(this, 'disposeInternal');
};
/**
* Generic function that takes a row and renders a DOM structure for that row.
*
* Normally this will only be matching a maximum of 20 or so items. Even with
* 40 rows, DOM this building is fine.
*
* @param {Object} row Object representing row.
* @param {string} token Token to highlight.
* @param {Node} node The node to render into.
* @private
*/
goog.ui.ac.Renderer.prototype.renderRowContents_ = function(row, token, node) {
goog.dom.setTextContent(node, row.data.toString());
};
/**
* Goes through a node and all of its child nodes, replacing HTML text that
* matches a token with <b>token</b>.
* The replacement will happen on the first match or all matches depending on
* this.highlightAllTokens_ value.
*
* @param {Node} node Node to match.
* @param {string|Array<string>} tokenOrArray Token to match or array of tokens
* to match. By default, only the first match will be highlighted. If
* highlightAllTokens is set, then all tokens appearing at the start of a
* word, in whatever order and however many times, will be highlighted.
* @private
*/
goog.ui.ac.Renderer.prototype.startHiliteMatchingText_ = function(
node, tokenOrArray) {
this.wasHighlightedAtLeastOnce_ = false;
this.hiliteMatchingText_(node, tokenOrArray);
};
/**
* @param {Node} node Node to match.
* @param {string|Array<string>} tokenOrArray Token to match or array of tokens
* to match.
* @private
*/
goog.ui.ac.Renderer.prototype.hiliteMatchingText_ = function(
node, tokenOrArray) {
if (!this.highlightAllTokens_ && this.wasHighlightedAtLeastOnce_) {
return;
}
if (node.nodeType == goog.dom.NodeType.TEXT) {
var rest = null;
if (goog.isArray(tokenOrArray) && tokenOrArray.length > 1 &&
!this.highlightAllTokens_) {
rest = goog.array.slice(tokenOrArray, 1);
}
var token = this.getTokenRegExp_(tokenOrArray);
if (token.length == 0) return;
var text = node.nodeValue;
// Create a regular expression to match a token at the beginning of a line
// or preceded by non-alpha-numeric characters. Note: token could have |
// operators in it, so we need to parenthesise it before adding \b to it.
// or preceded by non-alpha-numeric characters
//
// NOTE(user): When using word matches, this used to have
// a (^|\\W+) clause where it now has \\b but it caused various
// browsers to hang on really long strings. The (^|\\W+) matcher was also
// unnecessary, because \b already checks that the character before the
// is a non-word character, and ^ matches the start of the line or following
// a line terminator character, which is also \W. The regexp also used to
// have a capturing match before the \\b, which would capture the
// non-highlighted content, but that caused the regexp matching to run much
// slower than the current version.
var re = this.matchWordBoundary_ ?
new RegExp('\\b(?:' + token + ')', 'gi') :
new RegExp(token, 'gi');
var textNodes = [];
var lastIndex = 0;
// Find all matches
// Note: text.split(re) has inconsistencies between IE and FF, so
// manually recreated the logic
var match = re.exec(text);
var numMatches = 0;
while (match) {
numMatches++;
textNodes.push(text.substring(lastIndex, match.index));
textNodes.push(text.substring(match.index, re.lastIndex));
lastIndex = re.lastIndex;
match = re.exec(text);
}
textNodes.push(text.substring(lastIndex));
// Replace the tokens with bolded text. Each pair of textNodes
// (starting at index idx) includes a node of text before the bolded
// token, and a node (at idx + 1) consisting of what should be
// enclosed in bold tags.
if (textNodes.length > 1) {
var maxNumToBold = !this.highlightAllTokens_ ? 1 : numMatches;
for (var i = 0; i < maxNumToBold; i++) {
var idx = 2 * i;
node.nodeValue = textNodes[idx];
var boldTag = this.dom_.createElement(goog.dom.TagName.B);
boldTag.className = this.highlightedClassName;
this.dom_.appendChild(
boldTag, this.dom_.createTextNode(textNodes[idx + 1]));
boldTag = node.parentNode.insertBefore(boldTag, node.nextSibling);
node.parentNode.insertBefore(
this.dom_.createTextNode(''), boldTag.nextSibling);
node = boldTag.nextSibling;
}
// Append the remaining text nodes to the end.
var remainingTextNodes = goog.array.slice(textNodes, maxNumToBold * 2);
node.nodeValue = remainingTextNodes.join('');
this.wasHighlightedAtLeastOnce_ = true;
} else if (rest) {
this.hiliteMatchingText_(node, rest);
}
} else {
var child = node.firstChild;
while (child) {
var nextChild = child.nextSibling;
this.hiliteMatchingText_(child, tokenOrArray);
child = nextChild;
}
}
};
/**
* Transforms a token into a string ready to be put into the regular expression
* in hiliteMatchingText_.
* @param {string|Array<string>} tokenOrArray The token or array to get the
* regex string from.
* @return {string} The regex-ready token.
* @private
*/
goog.ui.ac.Renderer.prototype.getTokenRegExp_ = function(tokenOrArray) {
var token = '';
if (!tokenOrArray) {
return token;
}
if (goog.isArray(tokenOrArray)) {
// Remove invalid tokens from the array, which may leave us with nothing.
tokenOrArray = goog.array.filter(tokenOrArray, function(str) {
return !goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str));
});
}
// If highlighting all tokens, join them with '|' so the regular expression
// will match on any of them.
if (this.highlightAllTokens_) {
if (goog.isArray(tokenOrArray)) {
var tokenArray = goog.array.map(tokenOrArray, goog.string.regExpEscape);
token = tokenArray.join('|');
} else {
// Remove excess whitespace from the string so bars will separate valid
// tokens in the regular expression.
token = goog.string.collapseWhitespace(tokenOrArray);
token = goog.string.regExpEscape(token);
token = token.replace(/ /g, '|');
}
} else {
// Not highlighting all matching tokens. If tokenOrArray is a string, use
// that as the token. If it is an array, use the first element in the
// array.
// TODO(user): why is this this way?. We should match against all
// tokens in the array, but only accept the first match.
if (goog.isArray(tokenOrArray)) {
token = tokenOrArray.length > 0 ?
goog.string.regExpEscape(tokenOrArray[0]) :
'';
} else {
// For the single-match string token, we refuse to match anything if
// the string begins with a non-word character, as matches by definition
// can only occur at the start of a word. (This also handles the
// goog.string.isEmptyOrWhitespace(goog.string.makeSafe(tokenOrArray))
// case.)
if (!/^\W/.test(tokenOrArray)) {
token = goog.string.regExpEscape(tokenOrArray);
}
}
}
return token;
};
/**
* Render a row by creating a div and then calling row rendering callback or
* default row handler
*
* @param {Object} row Object representing row.
* @param {string} token Token to highlight.
* @return {!Element} An element with the rendered HTML.
*/
goog.ui.ac.Renderer.prototype.renderRowHtml = function(row, token) {
// Create and return the element.
var elem = this.dom_.createDom(goog.dom.TagName.DIV, {
className: this.rowClassName,
id: goog.ui.IdGenerator.getInstance().getNextUniqueId()
});
goog.a11y.aria.setRole(elem, goog.a11y.aria.Role.OPTION);
if (this.customRenderer_ && this.customRenderer_.renderRow) {
this.customRenderer_.renderRow(row, token, elem);
} else {
this.renderRowContents_(row, token, elem);
}
if (token && this.useStandardHighlighting_) {
this.startHiliteMatchingText_(elem, token);
}
goog.dom.classlist.add(elem, this.rowClassName);
this.rowDivs_.push(elem);
return elem;
};
/**
* Given an event target looks up through the parents till it finds a div. Once
* found it will then look to see if that is one of the childnodes, if it is
* then the index is returned, otherwise -1 is returned.
* @param {Element} et HtmlElement.
* @return {number} Index corresponding to event target.
* @private
*/
goog.ui.ac.Renderer.prototype.getRowFromEventTarget_ = function(et) {
while (et && et != this.element_ &&
!goog.dom.classlist.contains(et, this.rowClassName)) {
et = /** @type {Element} */ (et.parentNode);
}
return et ? goog.array.indexOf(this.rowDivs_, et) : -1;
};
/**
* Handle the click events. These are redirected to the AutoComplete object
* which then makes a callback to select the correct row.
* @param {goog.events.Event} e Browser event object.
* @private
*/
goog.ui.ac.Renderer.prototype.handleClick_ = function(e) {
var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target));
if (index >= 0) {
this.dispatchEvent(/** @lends {goog.events.Event.prototype} */ ({
type: goog.ui.ac.AutoComplete.EventType.SELECT,
row: this.rows_[index].id
}));
}
e.stopPropagation();
};
/**
* Handle the mousedown event and prevent the AC from losing focus.
* @param {goog.events.Event} e Browser event object.
* @private
*/
goog.ui.ac.Renderer.prototype.handleMouseDown_ = function(e) {
e.stopPropagation();
e.preventDefault();
};
/**
* Handle the mousing events. These are redirected to the AutoComplete object
* which then makes a callback to set the correctly highlighted row. This is
* because the AutoComplete can move the focus as well, and there is no sense
* duplicating the code
* @param {goog.events.Event} e Browser event object.
* @private
*/
goog.ui.ac.Renderer.prototype.handleMouseOver_ = function(e) {
var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target));
if (index >= 0) {
if ((goog.now() - this.startRenderingRows_) <
goog.ui.ac.Renderer.DELAY_BEFORE_MOUSEOVER) {
return;
}
this.dispatchEvent({
type: goog.ui.ac.AutoComplete.EventType.HILITE,
row: this.rows_[index].id
});
}
};
/**
* Class allowing different implementations to custom render the autocomplete.
* Extending classes should override the render function.
* @constructor
*/
goog.ui.ac.Renderer.CustomRenderer = function() {};
/**
* Renders the autocomplete box. May be set to null.
*
* Because of the type, this function cannot be documented with param JSDoc.
*
* The function expects the following parameters:
*
* renderer, goog.ui.ac.Renderer: The autocomplete renderer.
* element, Element: The main element that controls the rendered autocomplete.
* rows, Array: The current set of rows being displayed.
* token, string: The current token that has been entered. *
*
* @type {function(goog.ui.ac.Renderer, Element, Array, string)|
* null|undefined}
*/
goog.ui.ac.Renderer.CustomRenderer.prototype.render = function(
renderer, element, rows, token) {};
/**
* Generic function that takes a row and renders a DOM structure for that row.
* @param {Object} row Object representing row.
* @param {string} token Token to highlight.
* @param {Node} node The node to render into.
*/
goog.ui.ac.Renderer.CustomRenderer.prototype.renderRow = function(
row, token, node) {};
|