From 8bdce22ca02a4722bc0e018548c7b85913a4c6e9 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Mon, 27 May 2019 00:43:30 +0300 Subject: [PATCH 01/23] wip: initial virtual scroll rework --- src/ng-select/ng-dropdown-panel.component.ts | 267 +++++++++---------- src/ng-select/ng-select.component.ts | 3 +- src/ng-select/virtual-scroll.service.spec.ts | 6 +- src/ng-select/virtual-scroll.service.ts | 46 ++-- 4 files changed, 162 insertions(+), 160 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 41b501ec7..207363707 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -1,35 +1,36 @@ +import { DOCUMENT } from '@angular/common'; import { + AfterContentInit, + ChangeDetectionStrategy, Component, - OnDestroy, - Renderer2, ElementRef, - Input, EventEmitter, - Output, - ViewChild, - SimpleChanges, + HostListener, + Inject, + Input, NgZone, - TemplateRef, - ViewEncapsulation, - ChangeDetectionStrategy, - AfterContentInit, - OnInit, OnChanges, - HostListener, + OnDestroy, + OnInit, Optional, - Inject + Output, + Renderer2, + SimpleChanges, + TemplateRef, + ViewChild, + ViewEncapsulation } from '@angular/core'; -import { DOCUMENT } from '@angular/common'; +import { animationFrameScheduler, asapScheduler, fromEvent, merge, Subject } from 'rxjs'; +import { auditTime, takeUntil } from 'rxjs/operators'; -import { NgOption } from './ng-select.types'; import { DropdownPosition } from './ng-select.component'; -import { WindowService } from './window.service'; +import { NgOption } from './ng-select.types'; import { VirtualScrollService } from './virtual-scroll.service'; -import { takeUntil } from 'rxjs/operators'; -import { Subject, fromEvent, merge } from 'rxjs'; +import { WindowService } from './window.service'; const TOP_CSS_CLASS = 'ng-select-top'; const BOTTOM_CSS_CLASS = 'ng-select-bottom'; +const SCROLL_SCHEDULER = typeof requestAnimationFrame !== 'undefined' ? animationFrameScheduler : asapScheduler; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -56,7 +57,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A @Input() markedItem: NgOption; @Input() position: DropdownPosition = 'auto'; @Input() appendTo: string; - @Input() bufferAmount = 4; + @Input() bufferAmount; @Input() virtualScroll = false; @Input() headerTemplate: TemplateRef; @Input() footerTemplate: TemplateRef; @@ -73,15 +74,12 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A private readonly _destroy$ = new Subject(); private readonly _dropdown: HTMLElement; + private _virtualPadding: HTMLElement; + private _scrollablePanel: HTMLElement; + private _contentPanel: HTMLElement; private _select: HTMLElement; - private _previousStart: number; - private _previousEnd: number; - private _startupLoop = true; - private _isScrolledToMarked = false; + // private _isScrolledToMarked = false; private _scrollToEndFired = false; - private _currentPosition: DropdownPosition; - private _disposeScrollListener = () => { }; - private _disposeDocumentResizeListener = () => { }; constructor( private _renderer: Renderer2, @@ -94,6 +92,8 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A this._dropdown = _elementRef.nativeElement; } + private _currentPosition: DropdownPosition; + get currentPosition(): DropdownPosition { return this._currentPosition; } @@ -109,27 +109,21 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A ngOnInit() { this._select = this._dropdown.parentElement; + this._virtualPadding = this.paddingElementRef.nativeElement; + this._scrollablePanel = this.scrollElementRef.nativeElement; + this._contentPanel = this.contentElementRef.nativeElement; this._handleScroll(); - if (this._document) { - merge( - fromEvent(this._document, 'touchstart', { capture: true }), - fromEvent(this._document, 'mousedown', { capture: true }) - ) - .pipe(takeUntil(this._destroy$)) - .subscribe(($event) => this._handleOutsideClick($event)); - } + this._handleOutsideClick(); } ngOnChanges(changes: SimpleChanges) { if (changes.items) { - this._isScrolledToMarked = false; - this._handleItemsChange(changes.items); + this._handleItemsChange(changes.items.currentValue); } } ngOnDestroy() { this._disposeDocumentResizeListener(); - this._disposeScrollListener(); this._destroy$.next(); this._destroy$.complete(); this._destroy$.unsubscribe(); @@ -151,16 +145,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A }); } - refresh(): Promise { - return new Promise(resolve => { - this._zone.runOutsideAngular(() => { - this._window.requestAnimationFrame(() => { - this._updateItems().then(resolve); - }); - }); - }) - } - scrollInto(item: NgOption) { if (!item) { return; @@ -170,53 +154,77 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A return; } - const d = this._calculateDimensions(this.virtualScroll ? 0 : index); + const d = this._virtualScrollService.dimensions; const scrollEl: Element = this.scrollElementRef.nativeElement; - const buffer = Math.floor(d.viewHeight / d.childHeight) - 1; + const buffer = Math.floor(d.panelHeight / d.itemHeight) - 1; if (this.virtualScroll) { - scrollEl.scrollTop = (index * d.childHeight) - (d.childHeight * Math.min(index, buffer)); + scrollEl.scrollTop = (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); } else { const contentEl: HTMLElement = this.contentElementRef.nativeElement; const childrenHeight = Array.from(contentEl.children).slice(0, index).reduce((c, n) => c + n.clientHeight, 0); - scrollEl.scrollTop = childrenHeight - (d.childHeight * Math.min(index, buffer)); + scrollEl.scrollTop = childrenHeight - (d.itemHeight * Math.min(index, buffer)); } } scrollIntoTag() { const el: Element = this.scrollElementRef.nativeElement; - const d = this._calculateDimensions(); - el.scrollTop = d.childHeight * (d.itemsLength + 1); + const d = this._virtualScrollService.dimensions; + el.scrollTop = d.itemHeight * (this.items.length + 1); } updateDropdownPosition() { - this._window.setTimeout(() => { - this._currentPosition = this._calculateCurrentPosition(this._dropdown); - if (this._currentPosition === 'top') { - this._renderer.addClass(this._dropdown, TOP_CSS_CLASS); - this._renderer.removeClass(this._dropdown, BOTTOM_CSS_CLASS); - this._renderer.addClass(this._select, TOP_CSS_CLASS); - this._renderer.removeClass(this._select, BOTTOM_CSS_CLASS) - } else { - this._renderer.addClass(this._dropdown, BOTTOM_CSS_CLASS); - this._renderer.removeClass(this._dropdown, TOP_CSS_CLASS); - this._renderer.addClass(this._select, BOTTOM_CSS_CLASS); - this._renderer.removeClass(this._select, TOP_CSS_CLASS); - } + this._currentPosition = this._calculateCurrentPosition(this._dropdown); + if (this._currentPosition === 'top') { + this._renderer.addClass(this._dropdown, TOP_CSS_CLASS); + this._renderer.removeClass(this._dropdown, BOTTOM_CSS_CLASS); + this._renderer.addClass(this._select, TOP_CSS_CLASS); + this._renderer.removeClass(this._select, BOTTOM_CSS_CLASS) + } else { + this._renderer.addClass(this._dropdown, BOTTOM_CSS_CLASS); + this._renderer.removeClass(this._dropdown, TOP_CSS_CLASS); + this._renderer.addClass(this._select, BOTTOM_CSS_CLASS); + this._renderer.removeClass(this._select, TOP_CSS_CLASS); + } - if (this.appendTo) { - this._updateAppendedDropdownPosition(); - } + if (this.appendTo) { + this._updateAppendedDropdownPosition(); + } + + this._dropdown.style.opacity = '1'; + } + + private _disposeDocumentResizeListener = () => { }; - this._dropdown.style.opacity = '1'; - }, 0); + private _handleScroll() { + this._zone.runOutsideAngular(() => { + fromEvent(this.scrollElementRef.nativeElement, 'scroll') + .pipe( + takeUntil(this._destroy$), + auditTime(0, SCROLL_SCHEDULER) + ) + .subscribe(() => { + this._calculateItemsRange(); + this._fireScrollToEnd(); + }); + }); } - private _handleOutsideClick($event: any) { - if (this._select.contains($event.target)) { + private _handleOutsideClick() { + if (!this._document) { return; } - if (this._dropdown.contains($event.target)) { + this._zone.runOutsideAngular(() => { + merge( + fromEvent(this._document, 'touchstart', { capture: true }), + fromEvent(this._document, 'mousedown', { capture: true }) + ).pipe(takeUntil(this._destroy$)) + .subscribe($event => this._checkToClose($event)); + }); + } + + private _checkToClose($event: any) { + if (this._select.contains($event.target) || this._dropdown.contains($event.target)) { return; } @@ -228,66 +236,64 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A this.outsideClick.emit(); } - private _handleScroll() { - this._disposeScrollListener = this._renderer.listen(this.scrollElementRef.nativeElement, 'scroll', () => { - this.refresh(); - this._fireScrollToEnd(); - }); - } - - private _handleItemsChange(items: { previousValue: NgOption[], currentValue: NgOption[] }) { - this._scrollToEndFired = false; - this._previousStart = undefined; - this._previousEnd = undefined; - if (items !== undefined && items.previousValue === undefined || - (items.previousValue !== undefined && items.previousValue.length === 0)) { - this._startupLoop = true; + private _handleItemsChange(items: NgOption[]) { + this.items = items || []; + if (this.virtualScroll && this.items.length > 0) { + this._zone.runOutsideAngular(() => { + console.log('items change'); + // TODO: if marked exists, calculate padding and render items based on index + this._calculateItemsRange(); + }); + } else { + this.update.emit(this.items); + // TODO: fix scroll to marked } - this.items = items.currentValue || []; - this.refresh().then(() => { - if (this.appendTo && this._currentPosition === 'top') { - this._updateAppendedDropdownPosition(); - } - }); } - private _updateItems(): Promise { - NgZone.assertNotInAngularZone(); - + private _calculateItemsRange() { + // TODO: separate initial render with scrolling if (!this.virtualScroll) { - this._zone.run(() => { - this.update.emit(this.items.slice()); - this._scrollToMarked(); - }); - return Promise.resolve(); + return; } - const loop = (resolve) => { - const d = this._calculateDimensions(); - const res = this._virtualScrollService.calculateItems(d, this.scrollElementRef.nativeElement, this.bufferAmount || 0); + this._calculateInitialDimensions().then(() => { + this._window.requestAnimationFrame(() => { + NgZone.assertNotInAngularZone(); - (this.paddingElementRef.nativeElement).style.height = `${res.scrollHeight}px`; - (this.contentElementRef.nativeElement).style.transform = 'translateY(' + res.topPadding + 'px)'; - if (res.start !== this._previousStart || res.end !== this._previousEnd) { + const d = this._virtualScrollService.calculateDimensions(this.items.length); + const res = this._virtualScrollService.calculateItems(d, this.scrollElementRef.nativeElement, this.bufferAmount); + this._virtualPadding.style.height = `${res.scrollHeight}px`; + this._contentPanel.style.transform = 'translateY(' + res.topPadding + 'px)'; + this._zone.run(() => { this.update.emit(this.items.slice(res.start, res.end)); this.scroll.emit({ start: res.start, end: res.end }); }); - this._previousStart = res.start; - this._previousEnd = res.end; + }); + }); + } - if (this._startupLoop === true) { - loop(resolve) - } + private _calculateInitialDimensions(): Promise { + if (this._virtualScrollService.dimensions) { + return Promise.resolve(); + } + + return new Promise(resolve => { + console.log('_calculateInitialDimensions'); + const [first] = this.items; + this.update.emit([first]); + Promise.resolve().then(() => { + const option = this._dropdown.querySelector(`#${first.htmlId}`); + const optionRect = option.getBoundingClientRect(); + const virtualHeight = optionRect.height * this.items.length; + this._virtualPadding.style.height = `${virtualHeight}px`; + const panelRect = this._scrollablePanel.getBoundingClientRect(); + this._virtualScrollService.setInitialDimensions(optionRect.height, panelRect.height); - } else if (this._startupLoop === true) { - this._startupLoop = false; - this._scrollToMarked(); resolve(); - } - }; - return new Promise((resolve) => loop(resolve)) + }); + }); } private _fireScrollToEnd() { @@ -305,15 +311,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A } } - private _calculateDimensions(index = 0) { - return this._virtualScrollService.calculateDimensions( - this.items.length, - index, - this.scrollElementRef.nativeElement, - this.contentElementRef.nativeElement - ) - } - private _handleDocumentResize() { if (!this.appendTo) { return; @@ -323,13 +320,13 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A }); } - private _scrollToMarked() { - if (this._isScrolledToMarked || !this.markedItem) { - return; - } - this._isScrolledToMarked = true; - this.scrollInto(this.markedItem); - } + // private _scrollToMarked() { + // if (this._isScrolledToMarked || !this.markedItem) { + // return; + // } + // this._isScrolledToMarked = true; + // this.scrollInto(this.markedItem); + // } private _calculateCurrentPosition(dropdownEl: HTMLElement) { if (this.position !== 'auto') { diff --git a/src/ng-select/ng-select.component.ts b/src/ng-select/ng-select.component.ts index 9b7299cc1..0717c4ded 100644 --- a/src/ng-select/ng-select.component.ts +++ b/src/ng-select/ng-select.component.ts @@ -50,6 +50,7 @@ import { NgDropdownPanelComponent } from './ng-dropdown-panel.component'; import { NgOptionComponent } from './ng-option.component'; import { SelectionModelFactory } from './selection-model'; import { NgSelectConfig } from './config.service'; +import { VirtualScrollService } from './virtual-scroll.service'; export const SELECTION_MODEL_FACTORY = new InjectionToken('ng-select-selection-model'); export type DropdownPosition = 'bottom' | 'top' | 'auto'; @@ -67,7 +68,7 @@ export type GroupValueFn = (key: string | object, children: any[]) => string | o provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgSelectComponent), multi: true - }], + }, VirtualScrollService], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: { diff --git a/src/ng-select/virtual-scroll.service.spec.ts b/src/ng-select/virtual-scroll.service.spec.ts index 5f0770577..b42ceee6f 100644 --- a/src/ng-select/virtual-scroll.service.spec.ts +++ b/src/ng-select/virtual-scroll.service.spec.ts @@ -20,7 +20,7 @@ describe('VirtualScrollService', () => { const itemsLength = 100; const buffer = 4; - const d = service.calculateDimensions(itemsLength, 0, dropdown, content); + const d = service.calculateDimensions(itemsLength); const res = service.calculateItems(d, dropdown, buffer); expect(res).toEqual({ @@ -42,13 +42,11 @@ describe('VirtualScrollService', () => { document.body.appendChild(content); const itemsLength = 100; - const res = service.calculateDimensions(itemsLength, 0, dropdown, content); + const res = service.calculateDimensions(itemsLength); expect(res).toEqual({ itemsLength: itemsLength, - viewWidth: 120, viewHeight: 100, - childWidth: 120, childHeight: 25, itemsPerCol: 4 }) diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/virtual-scroll.service.ts index ffbd9d77e..8cd19d8c1 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/virtual-scroll.service.ts @@ -1,10 +1,6 @@ -import { Injectable } from '@angular/core'; - export interface ItemsDimensions { itemsLength: number; - viewWidth: number; viewHeight: number; - childWidth: number; childHeight: number; itemsPerCol: number; } @@ -16,16 +12,27 @@ export interface ItemsRangeResult { end: number; } -@Injectable({ providedIn: 'root' }) +export interface PanelDimensions { + itemHeight: number; + panelHeight: number; +} + +// @Injectable({ providedIn: 'root' }) export class VirtualScrollService { - calculateItems(d: ItemsDimensions, dropdownEl: HTMLElement, bufferAmount: number): ItemsRangeResult { + private _dimensions: PanelDimensions; + + get dimensions() { + return this._dimensions; + } + + calculateItems(d: ItemsDimensions, dropdown: HTMLElement, bufferAmount: number): ItemsRangeResult { const scrollHeight = d.childHeight * d.itemsLength; - if (dropdownEl.scrollTop > scrollHeight) { - dropdownEl.scrollTop = scrollHeight; + if (dropdown.scrollTop > scrollHeight) { + dropdown.scrollTop = scrollHeight; } - const scrollTop = Math.max(0, dropdownEl.scrollTop); + const scrollTop = Math.max(0, dropdown.scrollTop); const indexByScrollTop = scrollTop / scrollHeight * d.itemsLength; let end = Math.min(d.itemsLength, Math.ceil(indexByScrollTop) + (d.itemsPerCol + 1)); @@ -50,21 +57,20 @@ export class VirtualScrollService { } } - calculateDimensions(itemsLength: number, index: number, panelEl: HTMLElement, contentEl: HTMLElement): ItemsDimensions { - const panelRect = panelEl.getBoundingClientRect(); - const itemRect = contentEl.children[index] ? contentEl.children[index].getBoundingClientRect() : { - width: panelRect.width, - height: panelRect.height, - top: 0, + setInitialDimensions(itemHeight: number, panelHeight: number) { + this._dimensions = { + itemHeight, + panelHeight }; - const itemsPerCol = Math.max(1, Math.floor(panelRect.height / itemRect.height)); + } + + calculateDimensions(itemsLength: number): ItemsDimensions { + const itemsPerCol = Math.max(1, Math.floor(this.dimensions.panelHeight / this.dimensions.itemHeight)); return { itemsLength: itemsLength, - viewWidth: panelRect.width, - viewHeight: panelRect.height, - childWidth: itemRect.width, - childHeight: itemRect.height, + viewHeight: this.dimensions.panelHeight, + childHeight: this.dimensions.itemHeight, itemsPerCol: itemsPerCol, }; } From 71b751184b16dddf78d177ebe88a15507bb87cb2 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Fri, 31 May 2019 22:09:50 +0300 Subject: [PATCH 02/23] scroll to marked after rendering viewport items --- src/ng-select/ng-dropdown-panel.component.ts | 86 +++++++++----------- src/ng-select/virtual-scroll.service.ts | 50 +++++------- 2 files changed, 59 insertions(+), 77 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 207363707..a25a087b8 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -1,6 +1,5 @@ import { DOCUMENT } from '@angular/common'; import { - AfterContentInit, ChangeDetectionStrategy, Component, ElementRef, @@ -51,7 +50,7 @@ const SCROLL_SCHEDULER = typeof requestAnimationFrame !== 'undefined' ? animatio ` }) -export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, AfterContentInit { +export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { @Input() items: NgOption[] = []; @Input() markedItem: NgOption; @@ -78,7 +77,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A private _scrollablePanel: HTMLElement; private _contentPanel: HTMLElement; private _select: HTMLElement; - // private _isScrolledToMarked = false; private _scrollToEndFired = false; constructor( @@ -118,7 +116,8 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A ngOnChanges(changes: SimpleChanges) { if (changes.items) { - this._handleItemsChange(changes.items.currentValue); + const change = changes.items; + this._handleItemsChange(change.currentValue, change.firstChange); } } @@ -132,19 +131,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A } } - ngAfterContentInit() { - this._whenContentReady().then(() => { - if (this._destroy$.closed) { - return; - } - if (this.appendTo) { - this._appendDropdown(); - this._handleDocumentResize(); - } - this.updateDropdownPosition(); - }); - } - scrollInto(item: NgOption) { if (!item) { return; @@ -236,33 +222,39 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A this.outsideClick.emit(); } - private _handleItemsChange(items: NgOption[]) { + private _handleItemsChange(items: NgOption[], firstChange: boolean) { this.items = items || []; - if (this.virtualScroll && this.items.length > 0) { - this._zone.runOutsideAngular(() => { - console.log('items change'); - // TODO: if marked exists, calculate padding and render items based on index + if (this.virtualScroll && this.items.length) { + if (firstChange) { + this._calculateInitialRange(); + } else { this._calculateItemsRange(); - }); + } } else { - this.update.emit(this.items); - // TODO: fix scroll to marked + this.update.emit(this.items); // TODO: fix scroll to marked } } - private _calculateItemsRange() { - // TODO: separate initial render with scrolling + private _calculateInitialRange() { + this._zone.runOutsideAngular(() => { + this._calculateDimensions().then(() => { + const index = this.markedItem ? this.markedItem.index : 0; + this._calculateItemsRange(index); + this._handleDropdownPosition(); + }) + }); + } + + private _calculateItemsRange(startingIndex = null) { if (!this.virtualScroll) { return; } - this._calculateInitialDimensions().then(() => { + this._zone.runOutsideAngular(() => { this._window.requestAnimationFrame(() => { NgZone.assertNotInAngularZone(); - - - const d = this._virtualScrollService.calculateDimensions(this.items.length); - const res = this._virtualScrollService.calculateItems(d, this.scrollElementRef.nativeElement, this.bufferAmount); + const scrollPos = this._virtualScrollService.getScrollPosition(startingIndex, this.bufferAmount, this._scrollablePanel); + const res = this._virtualScrollService.calculateItems(scrollPos, this.items.length, this.bufferAmount); this._virtualPadding.style.height = `${res.scrollHeight}px`; this._contentPanel.style.transform = 'translateY(' + res.topPadding + 'px)'; @@ -270,17 +262,21 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A this.update.emit(this.items.slice(res.start, res.end)); this.scroll.emit({ start: res.start, end: res.end }); }); + + if (scrollPos && startingIndex) { + this._scrollablePanel.scrollTop = scrollPos; + } }); }); } - private _calculateInitialDimensions(): Promise { + private _calculateDimensions(): Promise { if (this._virtualScrollService.dimensions) { return Promise.resolve(); } return new Promise(resolve => { - console.log('_calculateInitialDimensions'); + console.log('_calculateDimensions'); const [first] = this.items; this.update.emit([first]); Promise.resolve().then(() => { @@ -347,7 +343,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A private _appendDropdown() { const parent = document.querySelector(this.appendTo); if (!parent) { - throw new Error(`appendTo selector ${this.appendTo} did not found any parent element`) + throw new Error(`appendTo selector ${this.appendTo} did not found any parent element`); } parent.appendChild(this._dropdown); } @@ -368,20 +364,12 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy, A this._dropdown.style.minWidth = selectRect.width + 'px'; } - private _whenContentReady(): Promise { - if (this.items.length === 0) { - return Promise.resolve(); + private _handleDropdownPosition() { + // TODO: maybe use Promise.resolve() to be sure dropdown already exist + if (this.appendTo) { + this._appendDropdown(); + this._handleDocumentResize(); } - const ready = (resolve) => { - const ngOption = this._dropdown.querySelector('.ng-option'); - if (ngOption) { - resolve(); - return; - } - this._zone.runOutsideAngular(() => { - setTimeout(() => ready(resolve), 5); - }); - }; - return new Promise((resolve) => ready(resolve)) + this.updateDropdownPosition(); } } diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/virtual-scroll.service.ts index 8cd19d8c1..4e0eb321a 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/virtual-scroll.service.ts @@ -1,9 +1,4 @@ -export interface ItemsDimensions { - itemsLength: number; - viewHeight: number; - childHeight: number; - itemsPerCol: number; -} +import { isDefined } from './value-utils'; export interface ItemsRangeResult { scrollHeight: number; @@ -15,6 +10,7 @@ export interface ItemsRangeResult { export interface PanelDimensions { itemHeight: number; panelHeight: number; + itemsPerViewport: number; } // @Injectable({ providedIn: 'root' }) @@ -26,28 +22,26 @@ export class VirtualScrollService { return this._dimensions; } - calculateItems(d: ItemsDimensions, dropdown: HTMLElement, bufferAmount: number): ItemsRangeResult { - const scrollHeight = d.childHeight * d.itemsLength; - if (dropdown.scrollTop > scrollHeight) { - dropdown.scrollTop = scrollHeight; - } + calculateItems(scrollPos: number, itemsLength: number, buffer: number): ItemsRangeResult { + const d = this._dimensions; + const scrollHeight = d.itemHeight * itemsLength; - const scrollTop = Math.max(0, dropdown.scrollTop); - const indexByScrollTop = scrollTop / scrollHeight * d.itemsLength; - let end = Math.min(d.itemsLength, Math.ceil(indexByScrollTop) + (d.itemsPerCol + 1)); + const scrollTop = Math.max(0, scrollPos); + const indexByScrollTop = scrollTop / scrollHeight * itemsLength; + let end = Math.min(itemsLength, Math.ceil(indexByScrollTop) + (d.itemsPerViewport + 1)); const maxStartEnd = end; - const maxStart = Math.max(0, maxStartEnd - d.itemsPerCol - 1); + const maxStart = Math.max(0, maxStartEnd - d.itemsPerViewport - 1); let start = Math.min(maxStart, Math.floor(indexByScrollTop)); - let topPadding = d.childHeight * Math.ceil(start) - (d.childHeight * Math.min(start, bufferAmount)); + let topPadding = d.itemHeight * Math.ceil(start) - (d.itemHeight * Math.min(start, buffer)); topPadding = !isNaN(topPadding) ? topPadding : 0; start = !isNaN(start) ? start : -1; end = !isNaN(end) ? end : -1; - start -= bufferAmount; + start -= buffer; start = Math.max(0, start); - end += bufferAmount; - end = Math.min(d.itemsLength, end); + end += buffer; + end = Math.min(itemsLength, end); return { topPadding: topPadding, @@ -58,20 +52,20 @@ export class VirtualScrollService { } setInitialDimensions(itemHeight: number, panelHeight: number) { + const itemsPerViewport = Math.max(1, Math.floor(panelHeight / itemHeight)); this._dimensions = { itemHeight, - panelHeight + panelHeight, + itemsPerViewport }; } - calculateDimensions(itemsLength: number): ItemsDimensions { - const itemsPerCol = Math.max(1, Math.floor(this.dimensions.panelHeight / this.dimensions.itemHeight)); + getScrollPosition(index: number, buffer: number, dropdown: HTMLElement) { + if (isDefined(index)) { + const d = this.dimensions; + return (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); + } - return { - itemsLength: itemsLength, - viewHeight: this.dimensions.panelHeight, - childHeight: this.dimensions.itemHeight, - itemsPerCol: itemsPerCol, - }; + return dropdown.scrollTop; } } From 71d986483c6944ace69b5b176a1582fa785d92bc Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sat, 1 Jun 2019 17:50:27 +0300 Subject: [PATCH 03/23] fix scroll to marked --- src/ng-select/items-list.ts | 10 +- src/ng-select/ng-dropdown-panel.component.ts | 96 +++++++++++--------- src/ng-select/ng-select.component.spec.ts | 4 +- src/ng-select/ng-select.component.ts | 5 +- src/ng-select/virtual-scroll.service.ts | 13 ++- 5 files changed, 71 insertions(+), 57 deletions(-) diff --git a/src/ng-select/items-list.ts b/src/ng-select/items-list.ts index 8d0303292..2e0e4d2f2 100644 --- a/src/ng-select/items-list.ts +++ b/src/ng-select/items-list.ts @@ -201,9 +201,9 @@ export class ItemsList { if (this._filteredItems.length === 0) { return; } - const indexOfLastSelected = this._ngSelect.hideSelected ? -1 : this._filteredItems.indexOf(this.lastSelectedItem); - if (this.lastSelectedItem && indexOfLastSelected > -1) { - this._markedIndex = indexOfLastSelected; + const lastMarkedIndex = this._ngSelect.hideSelected ? -1 : this._lastMarkedIndex; + if (lastMarkedIndex > -1) { + this._markedIndex = lastMarkedIndex; } else { if (this._ngSelect.excludeGroupsFromDefaultSelection) { this._markedIndex = markDefault ? this.filteredItems.findIndex(x => !x.disabled && !x.children) : -1; @@ -310,6 +310,10 @@ export class ItemsList { } } + private get _lastMarkedIndex() { + return Math.max(this.markedIndex, this._filteredItems.indexOf(this.lastSelectedItem)); + } + private _groupBy(items: NgOption[], prop: string | Function): OptionGroups { const groups = new Map(); if (items.length === 0) { diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index a25a087b8..8353d7f60 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -41,7 +41,7 @@ const SCROLL_SCHEDULER = typeof requestAnimationFrame !== 'undefined' ? animatio
-
+
@@ -131,28 +131,29 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } } - scrollInto(item: NgOption) { - if (!item) { + scrollTo(option: NgOption) { + // TODO: fix with search term + if (!option) { return; } - const index = this.items.indexOf(item); + + const index = option.index; if (index < 0 || index >= this.items.length) { return; } const d = this._virtualScrollService.dimensions; - const scrollEl: Element = this.scrollElementRef.nativeElement; - const buffer = Math.floor(d.panelHeight / d.itemHeight) - 1; if (this.virtualScroll) { - scrollEl.scrollTop = (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); + const buffer = Math.floor(d.panelHeight / d.itemHeight) - 1; + this._scrollablePanel.scrollTop = (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); } else { - const contentEl: HTMLElement = this.contentElementRef.nativeElement; - const childrenHeight = Array.from(contentEl.children).slice(0, index).reduce((c, n) => c + n.clientHeight, 0); - scrollEl.scrollTop = childrenHeight - (d.itemHeight * Math.min(index, buffer)); + const item: HTMLElement = this._dropdown.querySelector(`#${option.htmlId}`); + this._scrollablePanel.scrollTop = item.offsetTop + item.clientHeight - d.panelHeight; } } - scrollIntoTag() { + scrollToTag() { + // TODO: needs fix ? const el: Element = this.scrollElementRef.nativeElement; const d = this._virtualScrollService.dimensions; el.scrollTop = d.itemHeight * (this.items.length + 1); @@ -189,7 +190,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { auditTime(0, SCROLL_SCHEDULER) ) .subscribe(() => { - this._calculateItemsRange(); + this._updateItemsRange(); this._fireScrollToEnd(); }); }); @@ -224,28 +225,47 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _handleItemsChange(items: NgOption[], firstChange: boolean) { this.items = items || []; + this._scrollToEndFired = false; + if (this.virtualScroll && this.items.length) { + // TODO: check with appendBody if (firstChange) { this._calculateInitialRange(); } else { - this._calculateItemsRange(); + this._updateItemsRange(); } } else { - this.update.emit(this.items); // TODO: fix scroll to marked + this._updateItems(); } } + private _updateItems() { + // TODO: check with appendBody + + this.update.emit(this.items); + Promise.resolve().then(() => { + const panelHeight = this._scrollablePanel.clientHeight; + this._virtualScrollService.setInitialDimensions(panelHeight / this.items.length, panelHeight); + if (this.markedItem) { + const item: HTMLElement = this._dropdown.querySelector(`#${this.markedItem.htmlId}`); + this._scrollablePanel.scrollTop = item.offsetTop; + } + + this._handleDropdownPosition(); + }); + } + private _calculateInitialRange() { this._zone.runOutsideAngular(() => { this._calculateDimensions().then(() => { const index = this.markedItem ? this.markedItem.index : 0; - this._calculateItemsRange(index); + this._updateItemsRange(index); this._handleDropdownPosition(); - }) + }); }); } - private _calculateItemsRange(startingIndex = null) { + private _updateItemsRange(index = null) { if (!this.virtualScroll) { return; } @@ -253,17 +273,17 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._zone.runOutsideAngular(() => { this._window.requestAnimationFrame(() => { NgZone.assertNotInAngularZone(); - const scrollPos = this._virtualScrollService.getScrollPosition(startingIndex, this.bufferAmount, this._scrollablePanel); - const res = this._virtualScrollService.calculateItems(scrollPos, this.items.length, this.bufferAmount); - this._virtualPadding.style.height = `${res.scrollHeight}px`; - this._contentPanel.style.transform = 'translateY(' + res.topPadding + 'px)'; + const scrollPos = this._virtualScrollService.getScrollPosition(index, this._scrollablePanel); + const range = this._virtualScrollService.calculateItems(scrollPos, this.items.length, this.bufferAmount); + this._virtualPadding.style.height = `${range.scrollHeight}px`; + this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; this._zone.run(() => { - this.update.emit(this.items.slice(res.start, res.end)); - this.scroll.emit({ start: res.start, end: res.end }); + this.update.emit(this.items.slice(range.start, range.end)); + this.scroll.emit({ start: range.start, end: range.end }); }); - if (scrollPos && startingIndex) { + if (scrollPos && index) { this._scrollablePanel.scrollTop = scrollPos; } }); @@ -276,16 +296,14 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } return new Promise(resolve => { - console.log('_calculateDimensions'); const [first] = this.items; this.update.emit([first]); Promise.resolve().then(() => { const option = this._dropdown.querySelector(`#${first.htmlId}`); - const optionRect = option.getBoundingClientRect(); - const virtualHeight = optionRect.height * this.items.length; - this._virtualPadding.style.height = `${virtualHeight}px`; - const panelRect = this._scrollablePanel.getBoundingClientRect(); - this._virtualScrollService.setInitialDimensions(optionRect.height, panelRect.height); + const optionHeight = option.clientHeight; + this._virtualPadding.style.height = `${optionHeight * this.items.length}px`; + const panelHeight = this._scrollablePanel.clientHeight; + this._virtualScrollService.setInitialDimensions(option.clientHeight, panelHeight); resolve(); }); @@ -296,12 +314,12 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { if (this._scrollToEndFired) { return; } - const scroll: HTMLElement = this.scrollElementRef.nativeElement; - const padding: HTMLElement = this.virtualScroll ? - this.paddingElementRef.nativeElement : - this.contentElementRef.nativeElement; - if (scroll.scrollTop + this._dropdown.clientHeight >= padding.clientHeight) { + const padding = this.virtualScroll ? + this._virtualPadding : + this._contentPanel; + + if (this._scrollablePanel.scrollTop + this._dropdown.clientHeight >= padding.clientHeight) { this.scrollToEnd.emit(); this._scrollToEndFired = true; } @@ -316,14 +334,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { }); } - // private _scrollToMarked() { - // if (this._isScrolledToMarked || !this.markedItem) { - // return; - // } - // this._isScrolledToMarked = true; - // this.scrollInto(this.markedItem); - // } - private _calculateCurrentPosition(dropdownEl: HTMLElement) { if (this.position !== 'auto') { return this.position; diff --git a/src/ng-select/ng-select.component.spec.ts b/src/ng-select/ng-select.component.spec.ts index a9d3ae4a3..e1925f540 100644 --- a/src/ng-select/ng-select.component.spec.ts +++ b/src/ng-select/ng-select.component.spec.ts @@ -1001,7 +1001,7 @@ describe('NgSelectComponent', function () { cmp.cities = Array.from(Array(30).keys()).map((_, i) => ({ id: i, name: String.fromCharCode(97 + i) })); tickAndDetectChanges(fixture); - cmp.select.dropdownPanel.scrollInto(cmp.select.itemsList.items[1]); + cmp.select.dropdownPanel.scrollTo(cmp.select.itemsList.items[1]); tickAndDetectChanges(fixture); const panelItems = el.querySelector('.ng-dropdown-panel-items'); @@ -1024,7 +1024,7 @@ describe('NgSelectComponent', function () { cmp.cities = Array.from(Array(30).keys()).map((_, i) => ({ id: i, name: String.fromCharCode(97 + i) })); tickAndDetectChanges(fixture); - cmp.select.dropdownPanel.scrollInto(cmp.select.itemsList.items[15]); + cmp.select.dropdownPanel.scrollTo(cmp.select.itemsList.items[15]); tickAndDetectChanges(fixture); const panelItems = el.querySelector('.ng-dropdown-panel-items'); diff --git a/src/ng-select/ng-select.component.ts b/src/ng-select/ng-select.component.ts index 0717c4ded..aaaae6e43 100644 --- a/src/ng-select/ng-select.component.ts +++ b/src/ng-select/ng-select.component.ts @@ -405,6 +405,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } this.isOpen = false; this._clearSearch(); + this.itemsList.unmarkItem(); this._onTouched(); this.closeEvent.emit(); this._cd.markForCheck(); @@ -734,14 +735,14 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C if (!this.isOpen || !this.dropdownPanel) { return; } - this.dropdownPanel.scrollInto(this.itemsList.markedItem); + this.dropdownPanel.scrollTo(this.itemsList.markedItem); } private _scrollToTag() { if (!this.isOpen || !this.dropdownPanel) { return; } - this.dropdownPanel.scrollIntoTag(); + this.dropdownPanel.scrollToTag(); } private _handleTab($event: KeyboardEvent) { diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/virtual-scroll.service.ts index 4e0eb321a..0a55d694f 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/virtual-scroll.service.ts @@ -44,10 +44,10 @@ export class VirtualScrollService { end = Math.min(itemsLength, end); return { - topPadding: topPadding, - scrollHeight: scrollHeight, - start: start, - end: end + topPadding, + scrollHeight, + start, + end } } @@ -60,10 +60,9 @@ export class VirtualScrollService { }; } - getScrollPosition(index: number, buffer: number, dropdown: HTMLElement) { + getScrollPosition(index: number, dropdown: HTMLElement) { if (isDefined(index)) { - const d = this.dimensions; - return (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); + return index * this.dimensions.itemHeight; } return dropdown.scrollTop; From ab2528959384415e2130e78356292f6abf32b1ce Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sat, 1 Jun 2019 22:08:53 +0300 Subject: [PATCH 04/23] fix navigation with search term --- src/ng-select/ng-dropdown-panel.component.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 8353d7f60..1a21be6ee 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -132,12 +132,11 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } scrollTo(option: NgOption) { - // TODO: fix with search term if (!option) { return; } - const index = option.index; + const index = this.items.indexOf(option); if (index < 0 || index >= this.items.length) { return; } @@ -227,7 +226,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this.items = items || []; this._scrollToEndFired = false; - if (this.virtualScroll && this.items.length) { + if (this.virtualScroll) { // TODO: check with appendBody if (firstChange) { this._calculateInitialRange(); @@ -303,7 +302,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { const optionHeight = option.clientHeight; this._virtualPadding.style.height = `${optionHeight * this.items.length}px`; const panelHeight = this._scrollablePanel.clientHeight; - this._virtualScrollService.setInitialDimensions(option.clientHeight, panelHeight); + this._virtualScrollService.setInitialDimensions(optionHeight, panelHeight); resolve(); }); From 1bb565172aa5e53f1c70c210a07d679356608f46 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 4 Jun 2019 21:59:54 +0300 Subject: [PATCH 05/23] wip --- src/ng-select/ng-dropdown-panel.component.ts | 86 ++++++++++---------- src/ng-select/virtual-scroll.service.ts | 2 +- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 1a21be6ee..c7dedd862 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -25,7 +25,6 @@ import { auditTime, takeUntil } from 'rxjs/operators'; import { DropdownPosition } from './ng-select.component'; import { NgOption } from './ng-select.types'; import { VirtualScrollService } from './virtual-scroll.service'; -import { WindowService } from './window.service'; const TOP_CSS_CLASS = 'ng-select-top'; const BOTTOM_CSS_CLASS = 'ng-select-bottom'; @@ -83,7 +82,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _renderer: Renderer2, private _zone: NgZone, private _virtualScrollService: VirtualScrollService, - private _window: WindowService, _elementRef: ElementRef, @Optional() @Inject(DOCUMENT) private _document: any ) { @@ -117,7 +115,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { ngOnChanges(changes: SimpleChanges) { if (changes.items) { const change = changes.items; - this._handleItemsChange(change.currentValue, change.firstChange); + this._onItemsChange(change.currentValue, change.firstChange); } } @@ -143,6 +141,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { const d = this._virtualScrollService.dimensions; if (this.virtualScroll) { + // TODO: make smoother stepping const buffer = Math.floor(d.panelHeight / d.itemHeight) - 1; this._scrollablePanel.scrollTop = (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); } else { @@ -159,6 +158,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } updateDropdownPosition() { + // TODO: make private ? this._currentPosition = this._calculateCurrentPosition(this._dropdown); if (this._currentPosition === 'top') { this._renderer.addClass(this._dropdown, TOP_CSS_CLASS); @@ -184,13 +184,9 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _handleScroll() { this._zone.runOutsideAngular(() => { fromEvent(this.scrollElementRef.nativeElement, 'scroll') - .pipe( - takeUntil(this._destroy$), - auditTime(0, SCROLL_SCHEDULER) - ) + .pipe(takeUntil(this._destroy$), auditTime(0, SCROLL_SCHEDULER)) .subscribe(() => { - this._updateItemsRange(); - this._fireScrollToEnd(); + this._onContentScrolled(); }); }); } @@ -222,17 +218,12 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this.outsideClick.emit(); } - private _handleItemsChange(items: NgOption[], firstChange: boolean) { + private _onItemsChange(items: NgOption[], firstChange: boolean) { this.items = items || []; this._scrollToEndFired = false; if (this.virtualScroll) { - // TODO: check with appendBody - if (firstChange) { - this._calculateInitialRange(); - } else { - this._updateItemsRange(); - } + this._updateItemsRange(firstChange); } else { this._updateItems(); } @@ -240,11 +231,12 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _updateItems() { // TODO: check with appendBody + // TODO: run outside angular this.update.emit(this.items); Promise.resolve().then(() => { const panelHeight = this._scrollablePanel.clientHeight; - this._virtualScrollService.setInitialDimensions(panelHeight / this.items.length, panelHeight); + this._virtualScrollService.setDimensions(panelHeight / this.items.length, panelHeight); if (this.markedItem) { const item: HTMLElement = this._dropdown.querySelector(`#${this.markedItem.htmlId}`); this._scrollablePanel.scrollTop = item.offsetTop; @@ -254,42 +246,48 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { }); } - private _calculateInitialRange() { + private _updateItemsRange(firstChange: boolean) { this._zone.runOutsideAngular(() => { - this._calculateDimensions().then(() => { - const index = this.markedItem ? this.markedItem.index : 0; - this._updateItemsRange(index); - this._handleDropdownPosition(); - }); + if (firstChange) { + this._measureDimensions().then(() => { + const index = this.markedItem ? this.markedItem.index : 0; + this._renderItemsRange(index); + this._handleDropdownPosition(); // TODO: check with appendBody + }); + } else { + this._renderItemsRange(); + } }); } - private _updateItemsRange(index = null) { - if (!this.virtualScroll) { - return; + private _onContentScrolled() { + if (this.virtualScroll) { + this._renderItemsRange(); } - this._zone.runOutsideAngular(() => { - this._window.requestAnimationFrame(() => { - NgZone.assertNotInAngularZone(); - const scrollPos = this._virtualScrollService.getScrollPosition(index, this._scrollablePanel); - const range = this._virtualScrollService.calculateItems(scrollPos, this.items.length, this.bufferAmount); - this._virtualPadding.style.height = `${range.scrollHeight}px`; - this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; - - this._zone.run(() => { - this.update.emit(this.items.slice(range.start, range.end)); - this.scroll.emit({ start: range.start, end: range.end }); - }); + this._fireScrollToEnd(); + } - if (scrollPos && index) { - this._scrollablePanel.scrollTop = scrollPos; - } - }); + private _renderItemsRange(startIndex = null) { + NgZone.assertNotInAngularZone(); + + const scrollPos = this._virtualScrollService.getScrollPosition(startIndex, this._scrollablePanel); + const range = this._virtualScrollService.calculateItems(scrollPos, this.items.length, this.bufferAmount); + + this._virtualPadding.style.height = `${range.scrollHeight}px`; + this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; + + this._zone.run(() => { + this.update.emit(this.items.slice(range.start, range.end)); + this.scroll.emit({ start: range.start, end: range.end }); }); + + if (scrollPos && startIndex) { + this._scrollablePanel.scrollTop = scrollPos; + } } - private _calculateDimensions(): Promise { + private _measureDimensions(): Promise { if (this._virtualScrollService.dimensions) { return Promise.resolve(); } @@ -302,7 +300,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { const optionHeight = option.clientHeight; this._virtualPadding.style.height = `${optionHeight * this.items.length}px`; const panelHeight = this._scrollablePanel.clientHeight; - this._virtualScrollService.setInitialDimensions(optionHeight, panelHeight); + this._virtualScrollService.setDimensions(optionHeight, panelHeight); resolve(); }); diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/virtual-scroll.service.ts index 0a55d694f..4f1a32a53 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/virtual-scroll.service.ts @@ -51,7 +51,7 @@ export class VirtualScrollService { } } - setInitialDimensions(itemHeight: number, panelHeight: number) { + setDimensions(itemHeight: number, panelHeight: number) { const itemsPerViewport = Math.max(1, Math.floor(panelHeight / itemHeight)); this._dimensions = { itemHeight, From ae03fc53ab3f900a6b6943966d2dc9a3c7fc17b2 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Wed, 5 Jun 2019 18:52:20 +0300 Subject: [PATCH 06/23] remove document resize listener --- src/ng-select/ng-dropdown-panel.component.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index c7dedd862..9d63521bf 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -120,7 +120,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } ngOnDestroy() { - this._disposeDocumentResizeListener(); this._destroy$.next(); this._destroy$.complete(); this._destroy$.unsubscribe(); @@ -179,8 +178,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._dropdown.style.opacity = '1'; } - private _disposeDocumentResizeListener = () => { }; - private _handleScroll() { this._zone.runOutsideAngular(() => { fromEvent(this.scrollElementRef.nativeElement, 'scroll') @@ -322,15 +319,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } } - private _handleDocumentResize() { - if (!this.appendTo) { - return; - } - this._disposeDocumentResizeListener = this._renderer.listen('window', 'resize', () => { - this._updateAppendedDropdownPosition(); - }); - } - private _calculateCurrentPosition(dropdownEl: HTMLElement) { if (this.position !== 'auto') { return this.position; @@ -375,7 +363,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { // TODO: maybe use Promise.resolve() to be sure dropdown already exist if (this.appendTo) { this._appendDropdown(); - this._handleDocumentResize(); } this.updateDropdownPosition(); } From c8f9775f5beb968951ced348dc654f7fcc079b9e Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Wed, 5 Jun 2019 20:18:39 +0300 Subject: [PATCH 07/23] fix appendTo with virtual scroll --- demo/app/examples/virtual-scroll.component.ts | 1 + src/ng-select/ng-dropdown-panel.component.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/demo/app/examples/virtual-scroll.component.ts b/demo/app/examples/virtual-scroll.component.ts index a74b426c1..55a0221f2 100644 --- a/demo/app/examples/virtual-scroll.component.ts +++ b/demo/app/examples/virtual-scroll.component.ts @@ -16,6 +16,7 @@ import { HttpClient } from '@angular/common/http'; bindLabel="title" bindValue="thumbnailUrl" placeholder="Select photo" + appendTo="body" (scroll)="onScroll($event)" (scrollToEnd)="onScrollToEnd()"> diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 9d63521bf..ee5342713 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -247,9 +247,9 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._zone.runOutsideAngular(() => { if (firstChange) { this._measureDimensions().then(() => { + this._handleDropdownPosition(); const index = this.markedItem ? this.markedItem.index : 0; this._renderItemsRange(index); - this._handleDropdownPosition(); // TODO: check with appendBody }); } else { this._renderItemsRange(); @@ -271,6 +271,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { const scrollPos = this._virtualScrollService.getScrollPosition(startIndex, this._scrollablePanel); const range = this._virtualScrollService.calculateItems(scrollPos, this.items.length, this.bufferAmount); + // TODO: height should change only when items.length has changed this._virtualPadding.style.height = `${range.scrollHeight}px`; this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; @@ -305,6 +306,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _fireScrollToEnd() { + // TODO: with virtual scroll fire it inside _renderItemsRange if (this._scrollToEndFired) { return; } @@ -345,10 +347,8 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _updateAppendedDropdownPosition() { const parent = document.querySelector(this.appendTo) || document.body; - this._dropdown.style.display = 'none'; const selectRect: ClientRect = this._select.getBoundingClientRect(); const boundingRect = parent.getBoundingClientRect(); - this._dropdown.style.display = ''; const offsetTop = selectRect.top - boundingRect.top; const offsetLeft = selectRect.left - boundingRect.left; const topDelta = this._currentPosition === 'bottom' ? selectRect.height : -this._dropdown.clientHeight; @@ -360,7 +360,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _handleDropdownPosition() { - // TODO: maybe use Promise.resolve() to be sure dropdown already exist + NgZone.assertNotInAngularZone(); if (this.appendTo) { this._appendDropdown(); } From da53ac2af189cb4d3540f576d05b8957a729884f Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sun, 9 Jun 2019 18:50:28 +0300 Subject: [PATCH 08/23] use scrollTop from scroll event, return if scrollPosition didn't change --- src/ng-select/ng-dropdown-panel.component.ts | 57 +++++++++++++------- src/ng-select/virtual-scroll.service.ts | 13 +---- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index ee5342713..072eecf01 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -24,7 +24,8 @@ import { auditTime, takeUntil } from 'rxjs/operators'; import { DropdownPosition } from './ng-select.component'; import { NgOption } from './ng-select.types'; -import { VirtualScrollService } from './virtual-scroll.service'; +import { isDefined } from './value-utils'; +import { PanelDimensions, VirtualScrollService } from './virtual-scroll.service'; const TOP_CSS_CLASS = 'ng-select-top'; const BOTTOM_CSS_CLASS = 'ng-select-bottom'; @@ -77,6 +78,8 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _contentPanel: HTMLElement; private _select: HTMLElement; private _scrollToEndFired = false; + private _itemsChanged = false; + private _lastScrollPosition: number; constructor( private _renderer: Renderer2, @@ -182,9 +185,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._zone.runOutsideAngular(() => { fromEvent(this.scrollElementRef.nativeElement, 'scroll') .pipe(takeUntil(this._destroy$), auditTime(0, SCROLL_SCHEDULER)) - .subscribe(() => { - this._onContentScrolled(); - }); + .subscribe((e: Event) => this._onContentScrolled(e.srcElement.scrollTop)); }); } @@ -218,6 +219,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _onItemsChange(items: NgOption[], firstChange: boolean) { this.items = items || []; this._scrollToEndFired = false; + this._itemsChanged = true; if (this.virtualScroll) { this._updateItemsRange(firstChange); @@ -246,10 +248,10 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _updateItemsRange(firstChange: boolean) { this._zone.runOutsideAngular(() => { if (firstChange) { - this._measureDimensions().then(() => { + this._measureDimensions().then((d: PanelDimensions) => { this._handleDropdownPosition(); const index = this.markedItem ? this.markedItem.index : 0; - this._renderItemsRange(index); + this._renderItemsRange(index * d.itemHeight); }); } else { this._renderItemsRange(); @@ -257,22 +259,31 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { }); } - private _onContentScrolled() { + private _onContentScrolled(scrollTop: number) { if (this.virtualScroll) { - this._renderItemsRange(); + this._renderItemsRange(scrollTop); } this._fireScrollToEnd(); } - private _renderItemsRange(startIndex = null) { + private _updateVirtualHeight(height: number) { + if (this._itemsChanged) { + this._virtualPadding.style.height = `${height}px`; + this._itemsChanged = false; + } + } + + private _renderItemsRange(scrollTop = null) { NgZone.assertNotInAngularZone(); - const scrollPos = this._virtualScrollService.getScrollPosition(startIndex, this._scrollablePanel); - const range = this._virtualScrollService.calculateItems(scrollPos, this.items.length, this.bufferAmount); + if (scrollTop && this._lastScrollPosition === scrollTop) { + return; + } - // TODO: height should change only when items.length has changed - this._virtualPadding.style.height = `${range.scrollHeight}px`; + scrollTop = scrollTop || this._scrollablePanel.scrollTop; + const range = this._virtualScrollService.calculateItems(scrollTop, this.items.length, this.bufferAmount); + this._updateVirtualHeight(range.scrollHeight); this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; this._zone.run(() => { @@ -280,14 +291,16 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this.scroll.emit({ start: range.start, end: range.end }); }); - if (scrollPos && startIndex) { - this._scrollablePanel.scrollTop = scrollPos; + if (!isDefined(this._lastScrollPosition) && scrollTop) { + this._scrollablePanel.scrollTop = scrollTop; } + + this._lastScrollPosition = scrollTop; } - private _measureDimensions(): Promise { + private _measureDimensions(): Promise { if (this._virtualScrollService.dimensions) { - return Promise.resolve(); + return Promise.resolve(this._virtualScrollService.dimensions); } return new Promise(resolve => { @@ -300,7 +313,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { const panelHeight = this._scrollablePanel.clientHeight; this._virtualScrollService.setDimensions(optionHeight, panelHeight); - resolve(); + resolve(this._virtualScrollService.dimensions); }); }); } @@ -316,11 +329,15 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._contentPanel; if (this._scrollablePanel.scrollTop + this._dropdown.clientHeight >= padding.clientHeight) { - this.scrollToEnd.emit(); - this._scrollToEndFired = true; + this._emitScrollToEnd(); } } + private _emitScrollToEnd() { + this.scrollToEnd.emit(); + this._scrollToEndFired = true; + } + private _calculateCurrentPosition(dropdownEl: HTMLElement) { if (this.position !== 'auto') { return this.position; diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/virtual-scroll.service.ts index 4f1a32a53..99329b20b 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/virtual-scroll.service.ts @@ -1,5 +1,3 @@ -import { isDefined } from './value-utils'; - export interface ItemsRangeResult { scrollHeight: number; topPadding: number; @@ -13,7 +11,6 @@ export interface PanelDimensions { itemsPerViewport: number; } -// @Injectable({ providedIn: 'root' }) export class VirtualScrollService { private _dimensions: PanelDimensions; @@ -31,7 +28,7 @@ export class VirtualScrollService { let end = Math.min(itemsLength, Math.ceil(indexByScrollTop) + (d.itemsPerViewport + 1)); const maxStartEnd = end; - const maxStart = Math.max(0, maxStartEnd - d.itemsPerViewport - 1); + const maxStart = Math.max(0, maxStartEnd - d.itemsPerViewport); let start = Math.min(maxStart, Math.floor(indexByScrollTop)); let topPadding = d.itemHeight * Math.ceil(start) - (d.itemHeight * Math.min(start, buffer)); @@ -59,12 +56,4 @@ export class VirtualScrollService { itemsPerViewport }; } - - getScrollPosition(index: number, dropdown: HTMLElement) { - if (isDefined(index)) { - return index * this.dimensions.itemHeight; - } - - return dropdown.scrollTop; - } } From 89679c09c0abc6f101157234aba11ffef8ef8b1e Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sun, 9 Jun 2019 19:00:17 +0300 Subject: [PATCH 09/23] use scrollTop while emitting scrollEnd --- src/ng-select/ng-dropdown-panel.component.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 072eecf01..a257cc03d 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -264,7 +264,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._renderItemsRange(scrollTop); } - this._fireScrollToEnd(); + this._fireScrollToEnd(scrollTop); } private _updateVirtualHeight(height: number) { @@ -318,8 +318,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { }); } - private _fireScrollToEnd() { - // TODO: with virtual scroll fire it inside _renderItemsRange + private _fireScrollToEnd(scrollTop: number) { if (this._scrollToEndFired) { return; } @@ -328,16 +327,12 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._virtualPadding : this._contentPanel; - if (this._scrollablePanel.scrollTop + this._dropdown.clientHeight >= padding.clientHeight) { - this._emitScrollToEnd(); + if (scrollTop + this._dropdown.clientHeight >= padding.clientHeight) { + this._zone.run(() => this.scrollToEnd.emit()); + this._scrollToEndFired = true; } } - private _emitScrollToEnd() { - this.scrollToEnd.emit(); - this._scrollToEndFired = true; - } - private _calculateCurrentPosition(dropdownEl: HTMLElement) { if (this.position !== 'auto') { return this.position; From e1747f7b5c9700d70fcd0c36c96ff981f1b7936f Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sun, 9 Jun 2019 21:37:44 +0300 Subject: [PATCH 10/23] make smoother navigation --- src/ng-select/ng-dropdown-panel.component.ts | 35 +++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index a257cc03d..02ba7d64a 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -143,12 +143,21 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { const d = this._virtualScrollService.dimensions; if (this.virtualScroll) { - // TODO: make smoother stepping + // TODO: use same logic as in else const buffer = Math.floor(d.panelHeight / d.itemHeight) - 1; this._scrollablePanel.scrollTop = (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); } else { const item: HTMLElement = this._dropdown.querySelector(`#${option.htmlId}`); - this._scrollablePanel.scrollTop = item.offsetTop + item.clientHeight - d.panelHeight; + const itemTop = item.offsetTop; + const itemBottom = itemTop + item.clientHeight; + const top = isDefined(this._lastScrollPosition) ? this._lastScrollPosition : itemTop; + const bottom = top + d.panelHeight; + + if (itemBottom > bottom) { + this._scrollablePanel.scrollTop = top + itemBottom - bottom; + } else if (itemTop <= top) { + this._scrollablePanel.scrollTop = itemTop; + } } } @@ -229,19 +238,15 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _updateItems() { - // TODO: check with appendBody - // TODO: run outside angular - this.update.emit(this.items); - Promise.resolve().then(() => { - const panelHeight = this._scrollablePanel.clientHeight; - this._virtualScrollService.setDimensions(panelHeight / this.items.length, panelHeight); - if (this.markedItem) { - const item: HTMLElement = this._dropdown.querySelector(`#${this.markedItem.htmlId}`); - this._scrollablePanel.scrollTop = item.offsetTop; - } - this._handleDropdownPosition(); + this._zone.runOutsideAngular(() => { + Promise.resolve().then(() => { + const panelHeight = this._scrollablePanel.clientHeight; + this._virtualScrollService.setDimensions(0, panelHeight); + this._handleDropdownPosition(); + this.scrollTo(this.markedItem); + }); }); } @@ -263,7 +268,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { if (this.virtualScroll) { this._renderItemsRange(scrollTop); } - + this._lastScrollPosition = scrollTop; this._fireScrollToEnd(scrollTop); } @@ -294,8 +299,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { if (!isDefined(this._lastScrollPosition) && scrollTop) { this._scrollablePanel.scrollTop = scrollTop; } - - this._lastScrollPosition = scrollTop; } private _measureDimensions(): Promise { From c59b0efbdc44b9d453225dc4bcf0565ee85250b1 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sun, 9 Jun 2019 21:54:14 +0300 Subject: [PATCH 11/23] adjust groups demo --- demo/app/examples/groups.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo/app/examples/groups.component.ts b/demo/app/examples/groups.component.ts index c2e366ff2..a87c77fdc 100644 --- a/demo/app/examples/groups.component.ts +++ b/demo/app/examples/groups.component.ts @@ -14,7 +14,6 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; bindLabel="name" bindValue="name" groupBy="country" - [multiple]="true" [(ngModel)]="selectedAccount"> {{item.country || 'Unnamed group'}} @@ -149,7 +148,7 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; ` }) export class SelectGroupsComponent { - selectedAccount = ['Samantha']; + selectedAccount = 'Michael'; accounts = [ { name: 'Jill', email: 'jill@email.com', age: 15, country: undefined, child: { state: 'Active' } }, { name: 'Henry', email: 'henry@email.com', age: 10, country: undefined, child: { state: 'Active' } }, From 3294a664d0dc544d4134e0029bbb2b09597be70b Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 11 Jun 2019 07:57:19 +0300 Subject: [PATCH 12/23] use same scrollTo logic --- src/ng-select/ng-dropdown-panel.component.ts | 22 +++++++------------- src/ng-select/virtual-scroll.service.ts | 14 +++++++++++++ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 02ba7d64a..b82715149 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -141,23 +141,17 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { return; } - const d = this._virtualScrollService.dimensions; + let scrollTo; if (this.virtualScroll) { - // TODO: use same logic as in else - const buffer = Math.floor(d.panelHeight / d.itemHeight) - 1; - this._scrollablePanel.scrollTop = (index * d.itemHeight) - (d.itemHeight * Math.min(index, buffer)); + const itemHeight = this._virtualScrollService.dimensions.itemHeight; + scrollTo = this._virtualScrollService.getScrollTo(index * itemHeight, itemHeight, this._lastScrollPosition); } else { const item: HTMLElement = this._dropdown.querySelector(`#${option.htmlId}`); - const itemTop = item.offsetTop; - const itemBottom = itemTop + item.clientHeight; - const top = isDefined(this._lastScrollPosition) ? this._lastScrollPosition : itemTop; - const bottom = top + d.panelHeight; - - if (itemBottom > bottom) { - this._scrollablePanel.scrollTop = top + itemBottom - bottom; - } else if (itemTop <= top) { - this._scrollablePanel.scrollTop = itemTop; - } + scrollTo = this._virtualScrollService.getScrollTo(item.offsetTop, item.clientHeight, this._lastScrollPosition); + } + + if (isDefined(scrollTo)) { + this._scrollablePanel.scrollTop = scrollTo; } } diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/virtual-scroll.service.ts index 99329b20b..c154b3e05 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/virtual-scroll.service.ts @@ -1,3 +1,5 @@ +import { isDefined } from './value-utils'; + export interface ItemsRangeResult { scrollHeight: number; topPadding: number; @@ -56,4 +58,16 @@ export class VirtualScrollService { itemsPerViewport }; } + + getScrollTo(itemTop: number, itemHeight: number, lastScroll: number) { + const itemBottom = itemTop + itemHeight; + const top = isDefined(lastScroll) ? lastScroll : itemTop; + const bottom = top + this.dimensions.panelHeight; + + if (itemBottom > bottom) { + return top + itemBottom - bottom; + } else if (itemTop <= top) { + return itemTop; + } + } } From cf45a076e3ca1152f5326a0e4238cf578426f793 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 11 Jun 2019 08:16:41 +0300 Subject: [PATCH 13/23] move appendTo to ngOnInit --- src/ng-select/ng-dropdown-panel.component.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index b82715149..a8724bf54 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -113,6 +113,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._contentPanel = this.contentElementRef.nativeElement; this._handleScroll(); this._handleOutsideClick(); + this._appendDropdown(); } ngOnChanges(changes: SimpleChanges) { @@ -238,7 +239,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { Promise.resolve().then(() => { const panelHeight = this._scrollablePanel.clientHeight; this._virtualScrollService.setDimensions(0, panelHeight); - this._handleDropdownPosition(); + this.updateDropdownPosition(); // TODO: only on first change? this.scrollTo(this.markedItem); }); }); @@ -248,9 +249,9 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._zone.runOutsideAngular(() => { if (firstChange) { this._measureDimensions().then((d: PanelDimensions) => { - this._handleDropdownPosition(); const index = this.markedItem ? this.markedItem.index : 0; this._renderItemsRange(index * d.itemHeight); + this.updateDropdownPosition(); }); } else { this._renderItemsRange(); @@ -347,6 +348,10 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _appendDropdown() { + if (!this.appendTo) { + return; + } + const parent = document.querySelector(this.appendTo); if (!parent) { throw new Error(`appendTo selector ${this.appendTo} did not found any parent element`); @@ -367,12 +372,4 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._dropdown.style.width = selectRect.width + 'px'; this._dropdown.style.minWidth = selectRect.width + 'px'; } - - private _handleDropdownPosition() { - NgZone.assertNotInAngularZone(); - if (this.appendTo) { - this._appendDropdown(); - } - this.updateDropdownPosition(); - } } From af73f7c59dde6fbb3e65bf64677315d438f8c8c6 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 11 Jun 2019 21:09:21 +0300 Subject: [PATCH 14/23] fix: don't fire scroll to end when scrollTop = 0 fixes #1018 --- src/ng-select/ng-dropdown-panel.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index a8724bf54..2ebffb46f 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -226,6 +226,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._itemsChanged = true; if (this.virtualScroll) { + // TODO: if top and item length changed need to update dropdown position ? this._updateItemsRange(firstChange); } else { this._updateItems(); @@ -317,7 +318,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _fireScrollToEnd(scrollTop: number) { - if (this._scrollToEndFired) { + if (this._scrollToEndFired || scrollTop === 0) { return; } From 754cc73c10f1cd1dab77697778477932f68717c1 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 11 Jun 2019 21:09:41 +0300 Subject: [PATCH 15/23] remove unnecessary margin --- src/themes/default.theme.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/themes/default.theme.scss b/src/themes/default.theme.scss index 415d9c9b9..ea1d40947 100644 --- a/src/themes/default.theme.scss +++ b/src/themes/default.theme.scss @@ -255,7 +255,6 @@ $ng-select-value-font-size: 0.9em !default; padding: 5px 7px; } .ng-dropdown-panel-items { - margin-bottom: 1px; .ng-optgroup { user-select: none; padding: 8px 10px; From 67d24ecd16845139624a3f0c6317aef4ff14024f Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 11 Jun 2019 21:54:10 +0300 Subject: [PATCH 16/23] use offsetBottom when dropdown position is top --- src/ng-select/ng-dropdown-panel.component.ts | 52 ++++++++++++++------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 2ebffb46f..10d1ee939 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -78,7 +78,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _contentPanel: HTMLElement; private _select: HTMLElement; private _scrollToEndFired = false; - private _itemsChanged = false; + private _updateScrollHeight = false; private _lastScrollPosition: number; constructor( @@ -97,6 +97,19 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { return this._currentPosition; } + private _itemsLength: number; + + private get itemsLength() { + return this._itemsLength; + } + + private set itemsLength(value: number) { + if (value !== this._itemsLength) { + this._itemsLength = value; + this._onItemsLengthChanged(); + } + } + @HostListener('mousedown', ['$event']) handleMousedown($event: MouseEvent) { const target = $event.target as HTMLElement; @@ -138,7 +151,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } const index = this.items.indexOf(option); - if (index < 0 || index >= this.items.length) { + if (index < 0 || index >= this.itemsLength) { return; } @@ -160,7 +173,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { // TODO: needs fix ? const el: Element = this.scrollElementRef.nativeElement; const d = this._virtualScrollService.dimensions; - el.scrollTop = d.itemHeight * (this.items.length + 1); + el.scrollTop = d.itemHeight * (this.itemsLength + 1); } updateDropdownPosition() { @@ -223,10 +236,9 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _onItemsChange(items: NgOption[], firstChange: boolean) { this.items = items || []; this._scrollToEndFired = false; - this._itemsChanged = true; + this.itemsLength = items.length; if (this.virtualScroll) { - // TODO: if top and item length changed need to update dropdown position ? this._updateItemsRange(firstChange); } else { this._updateItems(); @@ -269,12 +281,16 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _updateVirtualHeight(height: number) { - if (this._itemsChanged) { + if (this._updateScrollHeight) { this._virtualPadding.style.height = `${height}px`; - this._itemsChanged = false; + this._updateScrollHeight = false; } } + private _onItemsLengthChanged() { + this._updateScrollHeight = true; + } + private _renderItemsRange(scrollTop = null) { NgZone.assertNotInAngularZone(); @@ -283,7 +299,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } scrollTop = scrollTop || this._scrollablePanel.scrollTop; - const range = this._virtualScrollService.calculateItems(scrollTop, this.items.length, this.bufferAmount); + const range = this._virtualScrollService.calculateItems(scrollTop, this.itemsLength, this.bufferAmount); this._updateVirtualHeight(range.scrollHeight); this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; @@ -308,7 +324,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { Promise.resolve().then(() => { const option = this._dropdown.querySelector(`#${first.htmlId}`); const optionHeight = option.clientHeight; - this._virtualPadding.style.height = `${optionHeight * this.items.length}px`; + this._virtualPadding.style.height = `${optionHeight * this.itemsLength}px`; const panelHeight = this._scrollablePanel.clientHeight; this._virtualScrollService.setDimensions(optionHeight, panelHeight); @@ -362,13 +378,21 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _updateAppendedDropdownPosition() { const parent = document.querySelector(this.appendTo) || document.body; - const selectRect: ClientRect = this._select.getBoundingClientRect(); + const selectRect = this._select.getBoundingClientRect(); const boundingRect = parent.getBoundingClientRect(); - const offsetTop = selectRect.top - boundingRect.top; const offsetLeft = selectRect.left - boundingRect.left; - const topDelta = this._currentPosition === 'bottom' ? selectRect.height : -this._dropdown.clientHeight; - this._dropdown.style.top = offsetTop + topDelta + 'px'; - this._dropdown.style.bottom = 'auto'; + const delta = selectRect.height; + + if (this._currentPosition === 'top') { + const offsetBottom = boundingRect.bottom - selectRect.bottom; + this._dropdown.style.bottom = offsetBottom + delta + 'px'; + this._dropdown.style.top = 'auto'; + } else if (this._currentPosition === 'bottom') { + const offsetTop = selectRect.top - boundingRect.top; + this._dropdown.style.top = offsetTop + delta + 'px'; + this._dropdown.style.bottom = 'auto'; + } + this._dropdown.style.left = offsetLeft + 'px'; this._dropdown.style.width = selectRect.width + 'px'; this._dropdown.style.minWidth = selectRect.width + 'px'; From 1583bd22e0fa74e62f5fe4824d7991f1a2762fd3 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Wed, 12 Jun 2019 07:59:05 +0300 Subject: [PATCH 17/23] feat: adjust dropdown position on selection when multiple and appendTo closes #984 --- src/ng-select/ng-dropdown-panel.component.ts | 52 +++++++++++++------- src/ng-select/ng-select.component.ts | 17 ++++--- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 10d1ee939..b76697ba3 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -77,6 +77,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _scrollablePanel: HTMLElement; private _contentPanel: HTMLElement; private _select: HTMLElement; + private _parent: HTMLElement; private _scrollToEndFired = false; private _updateScrollHeight = false; private _lastScrollPosition: number; @@ -176,8 +177,17 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { el.scrollTop = d.itemHeight * (this.itemsLength + 1); } - updateDropdownPosition() { - // TODO: make private ? + adjustDropdownPosition() { + if (this._currentPosition === 'top') { + return; + } + + const parent = this._parent.getBoundingClientRect(); + const select = this._select.getBoundingClientRect(); + this._setDropdownOffset(parent, select); + } + + private _handleDropdownPosition() { this._currentPosition = this._calculateCurrentPosition(this._dropdown); if (this._currentPosition === 'top') { this._renderer.addClass(this._dropdown, TOP_CSS_CLASS); @@ -192,7 +202,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } if (this.appendTo) { - this._updateAppendedDropdownPosition(); + this._updateDropdownPosition(); } this._dropdown.style.opacity = '1'; @@ -252,7 +262,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { Promise.resolve().then(() => { const panelHeight = this._scrollablePanel.clientHeight; this._virtualScrollService.setDimensions(0, panelHeight); - this.updateDropdownPosition(); // TODO: only on first change? + this._handleDropdownPosition(); // TODO: only on first change? this.scrollTo(this.markedItem); }); }); @@ -264,7 +274,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._measureDimensions().then((d: PanelDimensions) => { const index = this.markedItem ? this.markedItem.index : 0; this._renderItemsRange(index * d.itemHeight); - this.updateDropdownPosition(); + this._handleDropdownPosition(); }); } else { this._renderItemsRange(); @@ -369,32 +379,36 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { return; } - const parent = document.querySelector(this.appendTo); + this._parent = document.querySelector(this.appendTo); if (!parent) { throw new Error(`appendTo selector ${this.appendTo} did not found any parent element`); } - parent.appendChild(this._dropdown); + this._parent.appendChild(this._dropdown); } - private _updateAppendedDropdownPosition() { - const parent = document.querySelector(this.appendTo) || document.body; - const selectRect = this._select.getBoundingClientRect(); - const boundingRect = parent.getBoundingClientRect(); - const offsetLeft = selectRect.left - boundingRect.left; - const delta = selectRect.height; + private _updateDropdownPosition() { + const select = this._select.getBoundingClientRect(); + const parent = this._parent.getBoundingClientRect(); + const offsetLeft = select.left - parent.left; + + this._setDropdownOffset(parent, select); + + this._dropdown.style.left = offsetLeft + 'px'; + this._dropdown.style.width = select.width + 'px'; + this._dropdown.style.minWidth = select.width + 'px'; + } + + private _setDropdownOffset(parent: ClientRect, select: ClientRect) { + const delta = select.height; if (this._currentPosition === 'top') { - const offsetBottom = boundingRect.bottom - selectRect.bottom; + const offsetBottom = parent.bottom - select.bottom; this._dropdown.style.bottom = offsetBottom + delta + 'px'; this._dropdown.style.top = 'auto'; } else if (this._currentPosition === 'bottom') { - const offsetTop = selectRect.top - boundingRect.top; + const offsetTop = select.top - parent.top; this._dropdown.style.top = offsetTop + delta + 'px'; this._dropdown.style.bottom = 'auto'; } - - this._dropdown.style.left = offsetLeft + 'px'; - this._dropdown.style.width = selectRect.width + 'px'; - this._dropdown.style.minWidth = selectRect.width + 'px'; } } diff --git a/src/ng-select/ng-select.component.ts b/src/ng-select/ng-select.component.ts index aaaae6e43..8afe20a95 100644 --- a/src/ng-select/ng-select.component.ts +++ b/src/ng-select/ng-select.component.ts @@ -345,6 +345,8 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C this.typeahead.next(null); } this.clearEvent.emit(); + + this._onSelectionChanged(); } clearModel() { @@ -421,6 +423,8 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } else { this.select(item); } + + this._onSelectionChanged(); } select(item: NgOption) { @@ -557,12 +561,6 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C } } - updateDropdownPosition() { - if (this.dropdownPanel) { - this.dropdownPanel.updateDropdownPosition(); - } - } - private _setItems(items: any[]) { const firstItem = items[0]; this.bindLabel = this.bindLabel || this._defaultLabel; @@ -745,6 +743,13 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C this.dropdownPanel.scrollToTag(); } + private _onSelectionChanged() { + if (this.isOpen && this.multiple && this.appendTo) { + this._cd.detectChanges(); // make sure items are rendered + this.dropdownPanel.adjustDropdownPosition(); + } + } + private _handleTab($event: KeyboardEvent) { if (this.isOpen === false && !isDefined(this.addTag)) { return; From 5d5344a071ce09993f603d6fb7aefb41d1b99100 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Thu, 13 Jun 2019 21:18:39 +0300 Subject: [PATCH 18/23] fix some tests --- src/ng-select/items-list.ts | 7 +- src/ng-select/ng-dropdown-panel.component.ts | 5 +- src/ng-select/ng-select.component.spec.ts | 27 ++++--- src/ng-select/virtual-scroll.service.spec.ts | 84 +++++++++----------- src/ng-select/virtual-scroll.service.ts | 2 +- src/testing/helpers.ts | 11 +-- 6 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/ng-select/items-list.ts b/src/ng-select/items-list.ts index 2e0e4d2f2..64c2074af 100644 --- a/src/ng-select/items-list.ts +++ b/src/ng-select/items-list.ts @@ -311,7 +311,12 @@ export class ItemsList { } private get _lastMarkedIndex() { - return Math.max(this.markedIndex, this._filteredItems.indexOf(this.lastSelectedItem)); + const selectedIndex = this._filteredItems.indexOf(this.lastSelectedItem); + if (selectedIndex === -1) { + this._markedIndex = selectedIndex; + } + + return Math.max(this.markedIndex, selectedIndex); } private _groupBy(items: NgOption[], prop: string | Function): OptionGroups { diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index b76697ba3..4f6f3d8fd 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -80,7 +80,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { private _parent: HTMLElement; private _scrollToEndFired = false; private _updateScrollHeight = false; - private _lastScrollPosition: number; + private _lastScrollPosition = 0; constructor( private _renderer: Renderer2, @@ -318,8 +318,9 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this.scroll.emit({ start: range.start, end: range.end }); }); - if (!isDefined(this._lastScrollPosition) && scrollTop) { + if (isDefined(scrollTop) && this._lastScrollPosition === 0) { this._scrollablePanel.scrollTop = scrollTop; + this._lastScrollPosition = scrollTop; } } diff --git a/src/ng-select/ng-select.component.spec.ts b/src/ng-select/ng-select.component.spec.ts index e1925f540..e732ecf73 100644 --- a/src/ng-select/ng-select.component.spec.ts +++ b/src/ng-select/ng-select.component.spec.ts @@ -11,7 +11,7 @@ import { NgSelectModule } from './ng-select.module'; import { Subject } from 'rxjs'; import { WindowService } from './window.service'; -describe('NgSelectComponent', function () { +describe('NgSelectComponent', () => { describe('Data source', () => { it('should set items from primitive numbers array', fakeAsync(() => { @@ -556,7 +556,7 @@ describe('NgSelectComponent', function () { })]); })); - it('should bind bind to nested value property', fakeAsync(() => { + it('should bind to nested value property', fakeAsync(() => { const fixture = createTestingModule( NgSelectTestCmp, ` { + it('should scroll to item and change scroll position when scrolled to not visible item', fakeAsync(() => { const fixture = createTestingModule( NgSelectTestCmp, ` { + it('should close on option select by default', fakeAsync(() => { const fixture = createTestingModule( NgSelectTestCmp, ` { + it('should not close when isOpen is true', fakeAsync(() => { const fixture = createTestingModule( NgSelectTestCmp, ` { - it('should select next value on arrow down', () => { + it('should select next value on arrow down', fakeAsync(() => { selectOption(fixture, KeyCode.ArrowDown, 1); const result = [jasmine.objectContaining({ value: fixture.componentInstance.cities[1] })]; expect(select.selectedItems).toEqual(result); - }); + })); it('should stop marked loop if all items disabled', fakeAsync(() => { fixture.componentInstance.cities[0].disabled = true; @@ -1262,13 +1265,13 @@ describe('NgSelectComponent', function () { expect(select.selectedItems).toEqual(result); })); - it('should select last value on arrow up', () => { + it('should select last value on arrow up', fakeAsync(() => { selectOption(fixture, KeyCode.ArrowUp, 1); const result = [jasmine.objectContaining({ value: fixture.componentInstance.cities[2] })]; expect(select.selectedItems).toEqual(result); - }); + })); }); describe('esc', () => { @@ -1856,13 +1859,13 @@ describe('NgSelectComponent', function () { })); })); - it('should not toggle item on enter when dropdown is closed', () => { + it('should not toggle item on enter when dropdown is closed', fakeAsync(() => { selectOption(fixture, KeyCode.ArrowDown, 0); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Esc); expect((fixture.componentInstance.select.selectedItems).length).toBe(1); triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter); expect((fixture.componentInstance.select.selectedItems).length).toBe(1); - }); + })); describe('max selected items', () => { let arrowIcon: DebugElement = null; diff --git a/src/ng-select/virtual-scroll.service.spec.ts b/src/ng-select/virtual-scroll.service.spec.ts index b42ceee6f..7ce3fd3a3 100644 --- a/src/ng-select/virtual-scroll.service.spec.ts +++ b/src/ng-select/virtual-scroll.service.spec.ts @@ -1,54 +1,48 @@ -import { TestBed, inject } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { VirtualScrollService } from './virtual-scroll.service'; describe('VirtualScrollService', () => { + + let service: VirtualScrollService; + beforeEach(() => { TestBed.configureTestingModule({ providers: [VirtualScrollService] }); + + service = TestBed.get(VirtualScrollService); }); - it('should calculate items', inject([VirtualScrollService], (service: VirtualScrollService) => { - const dropdown = document.createElement('div'); - dropdown.style.width = '120px'; - dropdown.style.height = '100px'; - document.body.appendChild(dropdown); - - const content = document.createElement('div'); - content.innerHTML = `
`; - document.body.appendChild(content); - - const itemsLength = 100; - const buffer = 4; - const d = service.calculateDimensions(itemsLength); - const res = service.calculateItems(d, dropdown, buffer); - - expect(res).toEqual({ - start: 0, - end: 9, - topPadding: 0, - scrollHeight: 2500 - }) - })) - - it('should calculate dimensions', inject([VirtualScrollService], (service: VirtualScrollService) => { - const dropdown = document.createElement('div'); - dropdown.style.width = '120px'; - dropdown.style.height = '100px'; - document.body.appendChild(dropdown); - - const content = document.createElement('div'); - content.innerHTML = `
`; - document.body.appendChild(content); - - const itemsLength = 100; - const res = service.calculateDimensions(itemsLength); - - expect(res).toEqual({ - itemsLength: itemsLength, - viewHeight: 100, - childHeight: 25, - itemsPerCol: 4 - }) - })); -}); + describe('calculate items', () => { + it('should calculate items from beginning', () => { + const itemsLength = 100; + const buffer = 4; + + service.setDimensions(25, 100); + const res = service.calculateItems(0, itemsLength, buffer); + + expect(res).toEqual({ + start: 0, + end: 9, + topPadding: 0, + scrollHeight: 2500 + }) + }); + + it('should calculate items when scrolled', () => { + const itemsLength = 100; + const buffer = 4; + + service.setDimensions(25, 100); + const res = service.calculateItems(1250, itemsLength, buffer); + + expect(res).toEqual({ + start: 46, + end: 59, + topPadding: 1150, + scrollHeight: 2500 + }) + }); + }); +}) +; diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/virtual-scroll.service.ts index c154b3e05..b781c26f1 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/virtual-scroll.service.ts @@ -61,7 +61,7 @@ export class VirtualScrollService { getScrollTo(itemTop: number, itemHeight: number, lastScroll: number) { const itemBottom = itemTop + itemHeight; - const top = isDefined(lastScroll) ? lastScroll : itemTop; + const top = isDefined(lastScroll) ? lastScroll : itemTop; // TODO: needs fix, lastScroll always defined const bottom = top + this.dimensions.panelHeight; if (itemBottom > bottom) { diff --git a/src/testing/helpers.ts b/src/testing/helpers.ts index a8097c22c..439a36b11 100644 --- a/src/testing/helpers.ts +++ b/src/testing/helpers.ts @@ -1,13 +1,9 @@ -import { ComponentFixture, tick } from '@angular/core/testing'; -import { KeyCode } from '../ng-select/ng-select.types'; import { DebugElement } from '@angular/core'; +import { ComponentFixture, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { KeyCode } from '../ng-select/ng-select.types'; -export class TestsErrorHandler { - handleError(error: any) { - throw error; - } -} +export class TestsErrorHandler {} export function tickAndDetectChanges(fixture: ComponentFixture) { fixture.detectChanges(); @@ -16,6 +12,7 @@ export function tickAndDetectChanges(fixture: ComponentFixture) { export function selectOption(fixture, key: KeyCode, index: number) { triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space); // open + tickAndDetectChanges(fixture); // need to tick and detect changes, since dropdown fully inits after promise is resolved for (let i = 0; i < index; i++) { triggerKeyDownEvent(getNgSelectElement(fixture), key); } From 7298106784e9e9309bcacfb52c25345c1b6d2d5a Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sat, 15 Jun 2019 13:18:52 +0300 Subject: [PATCH 19/23] fix tests --- src/ng-select/ng-dropdown-panel.component.ts | 39 +++++----- .../ng-dropdown-panel.service.spec.ts | 71 +++++++++++++++++++ ...ervice.ts => ng-dropdown-panel.service.ts} | 8 +-- src/ng-select/ng-select.component.spec.ts | 16 ++--- src/ng-select/ng-select.component.ts | 4 +- src/ng-select/virtual-scroll.service.spec.ts | 48 ------------- src/ng-select/window.service.ts | 12 ---- src/testing/mocks.ts | 13 ---- 8 files changed, 104 insertions(+), 107 deletions(-) create mode 100644 src/ng-select/ng-dropdown-panel.service.spec.ts rename src/ng-select/{virtual-scroll.service.ts => ng-dropdown-panel.service.ts} (91%) delete mode 100644 src/ng-select/virtual-scroll.service.spec.ts delete mode 100644 src/ng-select/window.service.ts diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 4f6f3d8fd..071136e4b 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -25,7 +25,7 @@ import { auditTime, takeUntil } from 'rxjs/operators'; import { DropdownPosition } from './ng-select.component'; import { NgOption } from './ng-select.types'; import { isDefined } from './value-utils'; -import { PanelDimensions, VirtualScrollService } from './virtual-scroll.service'; +import { PanelDimensions, NgDropdownPanelService } from './ng-dropdown-panel.service'; const TOP_CSS_CLASS = 'ng-select-top'; const BOTTOM_CSS_CLASS = 'ng-select-bottom'; @@ -85,7 +85,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { constructor( private _renderer: Renderer2, private _zone: NgZone, - private _virtualScrollService: VirtualScrollService, + private _panelService: NgDropdownPanelService, _elementRef: ElementRef, @Optional() @Inject(DOCUMENT) private _document: any ) { @@ -146,7 +146,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } } - scrollTo(option: NgOption) { + scrollTo(option: NgOption, startFromOption = false) { if (!option) { return; } @@ -158,11 +158,12 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { let scrollTo; if (this.virtualScroll) { - const itemHeight = this._virtualScrollService.dimensions.itemHeight; - scrollTo = this._virtualScrollService.getScrollTo(index * itemHeight, itemHeight, this._lastScrollPosition); + const itemHeight = this._panelService.dimensions.itemHeight; + scrollTo = this._panelService.getScrollTo(index * itemHeight, itemHeight, this._lastScrollPosition); } else { const item: HTMLElement = this._dropdown.querySelector(`#${option.htmlId}`); - scrollTo = this._virtualScrollService.getScrollTo(item.offsetTop, item.clientHeight, this._lastScrollPosition); + const lastScroll = startFromOption ? item.offsetTop : this._lastScrollPosition; + scrollTo = this._panelService.getScrollTo(item.offsetTop, item.clientHeight, lastScroll); } if (isDefined(scrollTo)) { @@ -173,7 +174,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { scrollToTag() { // TODO: needs fix ? const el: Element = this.scrollElementRef.nativeElement; - const d = this._virtualScrollService.dimensions; + const d = this._panelService.dimensions; el.scrollTop = d.itemHeight * (this.itemsLength + 1); } @@ -251,19 +252,23 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { if (this.virtualScroll) { this._updateItemsRange(firstChange); } else { - this._updateItems(); + this._updateItems(firstChange); } } - private _updateItems() { + private _updateItems(firstChange: boolean) { this.update.emit(this.items); + if (firstChange === false) { + return; + } + this._zone.runOutsideAngular(() => { Promise.resolve().then(() => { const panelHeight = this._scrollablePanel.clientHeight; - this._virtualScrollService.setDimensions(0, panelHeight); - this._handleDropdownPosition(); // TODO: only on first change? - this.scrollTo(this.markedItem); + this._panelService.setDimensions(0, panelHeight); + this._handleDropdownPosition(); + this.scrollTo(this.markedItem, firstChange); }); }); } @@ -309,7 +314,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } scrollTop = scrollTop || this._scrollablePanel.scrollTop; - const range = this._virtualScrollService.calculateItems(scrollTop, this.itemsLength, this.bufferAmount); + const range = this._panelService.calculateItems(scrollTop, this.itemsLength, this.bufferAmount); this._updateVirtualHeight(range.scrollHeight); this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; @@ -325,8 +330,8 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _measureDimensions(): Promise { - if (this._virtualScrollService.dimensions) { - return Promise.resolve(this._virtualScrollService.dimensions); + if (this._panelService.dimensions) { + return Promise.resolve(this._panelService.dimensions); } return new Promise(resolve => { @@ -337,9 +342,9 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { const optionHeight = option.clientHeight; this._virtualPadding.style.height = `${optionHeight * this.itemsLength}px`; const panelHeight = this._scrollablePanel.clientHeight; - this._virtualScrollService.setDimensions(optionHeight, panelHeight); + this._panelService.setDimensions(optionHeight, panelHeight); - resolve(this._virtualScrollService.dimensions); + resolve(this._panelService.dimensions); }); }); } diff --git a/src/ng-select/ng-dropdown-panel.service.spec.ts b/src/ng-select/ng-dropdown-panel.service.spec.ts new file mode 100644 index 000000000..6792aa64f --- /dev/null +++ b/src/ng-select/ng-dropdown-panel.service.spec.ts @@ -0,0 +1,71 @@ +import { TestBed } from '@angular/core/testing'; +import { NgDropdownPanelService } from './ng-dropdown-panel.service'; + +describe('NgDropdownPanelService', () => { + + let service: NgDropdownPanelService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [NgDropdownPanelService] + }); + + service = TestBed.get(NgDropdownPanelService); + }); + + describe('calculate items', () => { + it('should calculate items from start', () => { + const itemsLength = 100; + const buffer = 4; + + service.setDimensions(25, 100); + const res = service.calculateItems(0, itemsLength, buffer); + + expect(res).toEqual({ + start: 0, + end: 9, + topPadding: 0, + scrollHeight: 2500 + }) + }); + + it('should calculate items when scrolled', () => { + const itemsLength = 100; + const buffer = 4; + + service.setDimensions(25, 100); + const res = service.calculateItems(1250, itemsLength, buffer); + + expect(res).toEqual({ + start: 46, + end: 59, + topPadding: 1150, + scrollHeight: 2500 + }) + }); + }); + + describe('scroll to', () => { + beforeEach(() => { + service.setDimensions(40, 240); + }); + + it('should not scroll if item is in visible area', () => { + expect(service.getScrollTo(0, 40, 0)).toBe(0); + expect(service.getScrollTo(200, 40, 0)).toBeNull(); + }); + + it('should scroll by item height', () => { + expect(service.getScrollTo(240, 40, 0)).toBe(40); + }); + + it('should start from top when reached bottom', () => { + expect(service.getScrollTo(0, 40, 400)).toBe(0); + }); + + it('should move to bottom when reached top', () => { + expect(service.getScrollTo(600, 40, 0)).toBe(400); + }); + }); +}) +; diff --git a/src/ng-select/virtual-scroll.service.ts b/src/ng-select/ng-dropdown-panel.service.ts similarity index 91% rename from src/ng-select/virtual-scroll.service.ts rename to src/ng-select/ng-dropdown-panel.service.ts index b781c26f1..6cb0498b0 100644 --- a/src/ng-select/virtual-scroll.service.ts +++ b/src/ng-select/ng-dropdown-panel.service.ts @@ -1,5 +1,3 @@ -import { isDefined } from './value-utils'; - export interface ItemsRangeResult { scrollHeight: number; topPadding: number; @@ -13,7 +11,7 @@ export interface PanelDimensions { itemsPerViewport: number; } -export class VirtualScrollService { +export class NgDropdownPanelService { private _dimensions: PanelDimensions; @@ -61,7 +59,7 @@ export class VirtualScrollService { getScrollTo(itemTop: number, itemHeight: number, lastScroll: number) { const itemBottom = itemTop + itemHeight; - const top = isDefined(lastScroll) ? lastScroll : itemTop; // TODO: needs fix, lastScroll always defined + const top = lastScroll; const bottom = top + this.dimensions.panelHeight; if (itemBottom > bottom) { @@ -69,5 +67,7 @@ export class VirtualScrollService { } else if (itemTop <= top) { return itemTop; } + + return null; } } diff --git a/src/ng-select/ng-select.component.spec.ts b/src/ng-select/ng-select.component.spec.ts index e732ecf73..7a874b39d 100644 --- a/src/ng-select/ng-select.component.spec.ts +++ b/src/ng-select/ng-select.component.spec.ts @@ -5,11 +5,10 @@ import { ConsoleService } from './console.service'; import { FormsModule } from '@angular/forms'; import { getNgSelectElement, selectOption, TestsErrorHandler, tickAndDetectChanges, triggerKeyDownEvent } from '../testing/helpers'; import { KeyCode, NgOption } from './ng-select.types'; -import { MockConsole, MockNgWindow, MockNgZone } from '../testing/mocks'; +import { MockConsole, MockNgZone } from '../testing/mocks'; import { NgSelectComponent } from './ng-select.component'; import { NgSelectModule } from './ng-select.module'; import { Subject } from 'rxjs'; -import { WindowService } from './window.service'; describe('NgSelectComponent', () => { @@ -984,7 +983,7 @@ describe('NgSelectComponent', () => { fixture.componentInstance.cities = Array.from(Array(30).keys()).map((_, i) => ({ id: i, name: String.fromCharCode(97 + i) })); tickAndDetectChanges(fixture); options = fixture.debugElement.nativeElement.querySelectorAll('.ng-option'); - expect(options.length).toBe(6); + expect(options.length).toBe(8); expect(options[0].innerText).toBe('a'); })); @@ -1021,17 +1020,15 @@ describe('NgSelectComponent', () => { const cmp = fixture.componentInstance; const el: HTMLElement = fixture.debugElement.nativeElement; - cmp.select.open(); - tickAndDetectChanges(fixture); - cmp.cities = Array.from(Array(30).keys()).map((_, i) => ({ id: i, name: String.fromCharCode(97 + i) })); + cmp.select.open(); tickAndDetectChanges(fixture); cmp.select.dropdownPanel.scrollTo(cmp.select.itemsList.items[15]); tickAndDetectChanges(fixture); const panelItems = el.querySelector('.ng-dropdown-panel-items'); - expect(panelItems.scrollTop).toBe(270); + expect(panelItems.scrollTop).toBe(48); })); it('should close on option select by default', fakeAsync(() => { @@ -1117,7 +1114,7 @@ describe('NgSelectComponent', () => { expect(fixture.componentInstance.select.isOpen).toBeTruthy(); })); - it('should not append dropdown, nor update its position when it is destroyed', async(() => { + it('should remove appended dropdown when it is destroyed', async(() => { const fixture = createTestingModule( NgSelectTestCmp, ` @@ -1132,8 +1129,6 @@ describe('NgSelectComponent', () => { fixture.detectChanges(); fixture.whenStable().then(() => { - const selectClasses = (fixture.nativeElement).querySelector('.ng-select').classList; - expect(selectClasses.contains('ng-select-bottom')).toBeFalsy(); const dropdown = document.querySelector('.ng-dropdown-panel'); expect(dropdown).toBeNull(); }); @@ -3373,7 +3368,6 @@ function createTestingModule(cmp: Type, template: string): ComponentFixtur providers: [ { provide: ErrorHandler, useClass: TestsErrorHandler }, { provide: NgZone, useFactory: () => new MockNgZone() }, - { provide: WindowService, useFactory: () => new MockNgWindow() }, { provide: ConsoleService, useFactory: () => new MockConsole() } ] }) diff --git a/src/ng-select/ng-select.component.ts b/src/ng-select/ng-select.component.ts index 8afe20a95..d293c64df 100644 --- a/src/ng-select/ng-select.component.ts +++ b/src/ng-select/ng-select.component.ts @@ -50,7 +50,7 @@ import { NgDropdownPanelComponent } from './ng-dropdown-panel.component'; import { NgOptionComponent } from './ng-option.component'; import { SelectionModelFactory } from './selection-model'; import { NgSelectConfig } from './config.service'; -import { VirtualScrollService } from './virtual-scroll.service'; +import { NgDropdownPanelService } from './ng-dropdown-panel.service'; export const SELECTION_MODEL_FACTORY = new InjectionToken('ng-select-selection-model'); export type DropdownPosition = 'bottom' | 'top' | 'auto'; @@ -68,7 +68,7 @@ export type GroupValueFn = (key: string | object, children: any[]) => string | o provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgSelectComponent), multi: true - }, VirtualScrollService], + }, NgDropdownPanelService], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: { diff --git a/src/ng-select/virtual-scroll.service.spec.ts b/src/ng-select/virtual-scroll.service.spec.ts deleted file mode 100644 index 7ce3fd3a3..000000000 --- a/src/ng-select/virtual-scroll.service.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { VirtualScrollService } from './virtual-scroll.service'; - -describe('VirtualScrollService', () => { - - let service: VirtualScrollService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [VirtualScrollService] - }); - - service = TestBed.get(VirtualScrollService); - }); - - describe('calculate items', () => { - it('should calculate items from beginning', () => { - const itemsLength = 100; - const buffer = 4; - - service.setDimensions(25, 100); - const res = service.calculateItems(0, itemsLength, buffer); - - expect(res).toEqual({ - start: 0, - end: 9, - topPadding: 0, - scrollHeight: 2500 - }) - }); - - it('should calculate items when scrolled', () => { - const itemsLength = 100; - const buffer = 4; - - service.setDimensions(25, 100); - const res = service.calculateItems(1250, itemsLength, buffer); - - expect(res).toEqual({ - start: 46, - end: 59, - topPadding: 1150, - scrollHeight: 2500 - }) - }); - }); -}) -; diff --git a/src/ng-select/window.service.ts b/src/ng-select/window.service.ts deleted file mode 100644 index 6bbc3ecb0..000000000 --- a/src/ng-select/window.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ providedIn: 'root' }) -export class WindowService { - requestAnimationFrame(fn) { - return window.requestAnimationFrame(fn); - } - - setTimeout(handler: (...args: any[]) => void, timeout: number): number { - return window.setTimeout(handler, timeout); - } -} diff --git a/src/testing/mocks.ts b/src/testing/mocks.ts index 64dcf2b1a..c712a0d01 100644 --- a/src/testing/mocks.ts +++ b/src/testing/mocks.ts @@ -1,5 +1,4 @@ import { Injectable, NgZone } from '@angular/core'; -import { WindowService } from '../ng-select/window.service'; @Injectable() export class MockNgZone extends NgZone { @@ -16,18 +15,6 @@ export class MockNgZone extends NgZone { } } -@Injectable() -export class MockNgWindow extends WindowService { - requestAnimationFrame(fn: Function): any { - return fn(); - } - - setTimeout(handler: (...args: any[]) => void, _: number): number { - handler(); - return 0; - } -} - @Injectable() export class MockConsole { warn() { From 456d9f72f6d2b762f1fef8c403f64f64deac6307 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sat, 15 Jun 2019 15:58:10 +0300 Subject: [PATCH 20/23] add more tests --- src/ng-select/items-list.spec.ts | 33 ++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/ng-select/items-list.spec.ts b/src/ng-select/items-list.spec.ts index 823907c08..465998f54 100644 --- a/src/ng-select/items-list.spec.ts +++ b/src/ng-select/items-list.spec.ts @@ -1,7 +1,7 @@ -import { NgSelectComponent } from './ng-select.component'; +import { NgSelectConfig } from './config.service'; import { ItemsList } from './items-list'; +import { NgSelectComponent } from './ng-select.component'; import { DefaultSelectionModel } from './selection-model'; -import { NgSelectConfig } from './config.service'; describe('ItemsList', () => { describe('select', () => { @@ -427,6 +427,35 @@ describe('ItemsList', () => { }); }); + describe('markSelectedOrDefault', () => { + let list: ItemsList; + let cmp: NgSelectComponent; + + beforeEach(() => { + cmp = ngSelectFactory(); + list = itemsListFactory(cmp); + const items = Array.from(Array(30)).map((_, index) => (`item-${index}`)); + list.setItems(items); + }); + + it('should mark first item', () => { + list.markSelectedOrDefault(true); + expect(list.markedIndex).toBe(0); + }); + + it('should keep marked item if it is above last selected item', () => { + list.select(list.items[10]); + list.markSelectedOrDefault(); + expect(list.markedIndex).toBe(10); + + list.markNextItem(); + list.markNextItem(); + list.markNextItem(); + list.markSelectedOrDefault(); + expect(list.markedIndex).toBe(13); + }); + }); + function itemsListFactory(cmp: NgSelectComponent): ItemsList { return new ItemsList(cmp, new DefaultSelectionModel()); } From ca04bfad133ab38b5cd909a3eaa5633202dcf3bf Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Sat, 15 Jun 2019 16:25:42 +0300 Subject: [PATCH 21/23] add more tests --- src/ng-select/ng-select.component.spec.ts | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/ng-select/ng-select.component.spec.ts b/src/ng-select/ng-select.component.spec.ts index 7a874b39d..e3e0fdf59 100644 --- a/src/ng-select/ng-select.component.spec.ts +++ b/src/ng-select/ng-select.component.spec.ts @@ -987,6 +987,36 @@ describe('NgSelectComponent', () => { expect(options[0].innerText).toBe('a'); })); + it('should scroll to selected item on first open when virtual scroll is enabled', fakeAsync(() => { + const fixture = createTestingModule( + NgSelectTestCmp, + ` + `); + + const select = fixture.componentInstance.select; + const cmp = fixture.componentInstance; + cmp.cities = Array.from(Array(30).keys()).map((_, i) => ({ id: i, name: String.fromCharCode(97 + i) })); + cmp['city'] = cmp.cities[10]; + tickAndDetectChanges(fixture); + + select.open(); + tickAndDetectChanges(fixture); + fixture.detectChanges(); + + const buffer = 4; + const itemHeight = 18; + const options = fixture.debugElement.nativeElement.querySelectorAll('.ng-option'); + const marked = fixture.debugElement.nativeElement.querySelector('.ng-option-marked'); + + expect(options.length).toBe(22); + expect(marked.innerText).toBe('k'); + expect(marked.offsetTop).toBe(buffer * itemHeight); + })); + it('should scroll to item and do not change scroll position when scrolled to visible item', fakeAsync(() => { const fixture = createTestingModule( NgSelectTestCmp, From 2e81257498fa9335a4e7121d5c9b1c6502db3dd6 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 25 Jun 2019 08:27:36 +0300 Subject: [PATCH 22/23] fixes --- src/ng-select/ng-dropdown-panel.component.ts | 33 +++++++++----------- src/ng-select/ng-select.component.ts | 3 +- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 071136e4b..04e1689d6 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -21,11 +21,11 @@ import { } from '@angular/core'; import { animationFrameScheduler, asapScheduler, fromEvent, merge, Subject } from 'rxjs'; import { auditTime, takeUntil } from 'rxjs/operators'; +import { NgDropdownPanelService, PanelDimensions } from './ng-dropdown-panel.service'; import { DropdownPosition } from './ng-select.component'; import { NgOption } from './ng-select.types'; import { isDefined } from './value-utils'; -import { PanelDimensions, NgDropdownPanelService } from './ng-dropdown-panel.service'; const TOP_CSS_CLASS = 'ng-select-top'; const BOTTOM_CSS_CLASS = 'ng-select-bottom'; @@ -172,10 +172,8 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } scrollToTag() { - // TODO: needs fix ? - const el: Element = this.scrollElementRef.nativeElement; - const d = this._panelService.dimensions; - el.scrollTop = d.itemHeight * (this.itemsLength + 1); + const panel = this._scrollablePanel; + panel.scrollTop = panel.scrollHeight - panel.clientHeight; } adjustDropdownPosition() { @@ -307,8 +305,6 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } private _renderItemsRange(scrollTop = null) { - NgZone.assertNotInAngularZone(); - if (scrollTop && this._lastScrollPosition === scrollTop) { return; } @@ -316,7 +312,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { scrollTop = scrollTop || this._scrollablePanel.scrollTop; const range = this._panelService.calculateItems(scrollTop, this.itemsLength, this.bufferAmount); this._updateVirtualHeight(range.scrollHeight); - this._contentPanel.style.transform = 'translateY(' + range.topPadding + 'px)'; + this._contentPanel.style.transform = `translateY(${range.topPadding}px)`; this._zone.run(() => { this.update.emit(this.items.slice(range.start, range.end)); @@ -334,18 +330,17 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { return Promise.resolve(this._panelService.dimensions); } - return new Promise(resolve => { - const [first] = this.items; - this.update.emit([first]); - Promise.resolve().then(() => { - const option = this._dropdown.querySelector(`#${first.htmlId}`); - const optionHeight = option.clientHeight; - this._virtualPadding.style.height = `${optionHeight * this.itemsLength}px`; - const panelHeight = this._scrollablePanel.clientHeight; - this._panelService.setDimensions(optionHeight, panelHeight); + const [first] = this.items; + this.update.emit([first]); - resolve(this._panelService.dimensions); - }); + return Promise.resolve().then(() => { + const option = this._dropdown.querySelector(`#${first.htmlId}`); + const optionHeight = option.clientHeight; + this._virtualPadding.style.height = `${optionHeight * this.itemsLength}px`; + const panelHeight = this._scrollablePanel.clientHeight; + this._panelService.setDimensions(optionHeight, panelHeight); + + return this._panelService.dimensions; }); } diff --git a/src/ng-select/ng-select.component.ts b/src/ng-select/ng-select.component.ts index d293c64df..516f2dae3 100644 --- a/src/ng-select/ng-select.component.ts +++ b/src/ng-select/ng-select.component.ts @@ -745,7 +745,8 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C private _onSelectionChanged() { if (this.isOpen && this.multiple && this.appendTo) { - this._cd.detectChanges(); // make sure items are rendered + // Make sure items are rendered. + this._cd.detectChanges(); this.dropdownPanel.adjustDropdownPosition(); } } From f31b5aee77d1151e21aead1ca495f9f15e4f65c8 Mon Sep 17 00:00:00 2001 From: varnastadeus Date: Tue, 25 Jun 2019 15:53:48 +0300 Subject: [PATCH 23/23] CR fixes --- src/ng-select/items-list.ts | 13 +++++++++---- src/ng-select/ng-dropdown-panel.component.ts | 12 ++++++------ src/ng-select/ng-select.component.ts | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/ng-select/items-list.ts b/src/ng-select/items-list.ts index 64c2074af..22b5aef0d 100644 --- a/src/ng-select/items-list.ts +++ b/src/ng-select/items-list.ts @@ -201,7 +201,8 @@ export class ItemsList { if (this._filteredItems.length === 0) { return; } - const lastMarkedIndex = this._ngSelect.hideSelected ? -1 : this._lastMarkedIndex; + + const lastMarkedIndex = this._getLastMarkedIndex(); if (lastMarkedIndex > -1) { this._markedIndex = lastMarkedIndex; } else { @@ -310,10 +311,14 @@ export class ItemsList { } } - private get _lastMarkedIndex() { + private _getLastMarkedIndex() { + if (this._ngSelect.hideSelected) { + return -1; + } + const selectedIndex = this._filteredItems.indexOf(this.lastSelectedItem); - if (selectedIndex === -1) { - this._markedIndex = selectedIndex; + if (this.lastSelectedItem && selectedIndex < 0) { + return -1; } return Math.max(this.markedIndex, selectedIndex); diff --git a/src/ng-select/ng-dropdown-panel.component.ts b/src/ng-select/ng-dropdown-panel.component.ts index 04e1689d6..3b6cb987c 100644 --- a/src/ng-select/ng-dropdown-panel.component.ts +++ b/src/ng-select/ng-dropdown-panel.component.ts @@ -176,14 +176,14 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { panel.scrollTop = panel.scrollHeight - panel.clientHeight; } - adjustDropdownPosition() { + adjustPosition() { if (this._currentPosition === 'top') { return; } const parent = this._parent.getBoundingClientRect(); const select = this._select.getBoundingClientRect(); - this._setDropdownOffset(parent, select); + this._setOffset(parent, select); } private _handleDropdownPosition() { @@ -201,7 +201,7 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { } if (this.appendTo) { - this._updateDropdownPosition(); + this._updatePosition(); } this._dropdown.style.opacity = '1'; @@ -387,19 +387,19 @@ export class NgDropdownPanelComponent implements OnInit, OnChanges, OnDestroy { this._parent.appendChild(this._dropdown); } - private _updateDropdownPosition() { + private _updatePosition() { const select = this._select.getBoundingClientRect(); const parent = this._parent.getBoundingClientRect(); const offsetLeft = select.left - parent.left; - this._setDropdownOffset(parent, select); + this._setOffset(parent, select); this._dropdown.style.left = offsetLeft + 'px'; this._dropdown.style.width = select.width + 'px'; this._dropdown.style.minWidth = select.width + 'px'; } - private _setDropdownOffset(parent: ClientRect, select: ClientRect) { + private _setOffset(parent: ClientRect, select: ClientRect) { const delta = select.height; if (this._currentPosition === 'top') { diff --git a/src/ng-select/ng-select.component.ts b/src/ng-select/ng-select.component.ts index 516f2dae3..c09b93a4a 100644 --- a/src/ng-select/ng-select.component.ts +++ b/src/ng-select/ng-select.component.ts @@ -747,7 +747,7 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C if (this.isOpen && this.multiple && this.appendTo) { // Make sure items are rendered. this._cd.detectChanges(); - this.dropdownPanel.adjustDropdownPosition(); + this.dropdownPanel.adjustPosition(); } }