mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(autocomplete): allow use of obj values (#2792)
This commit is contained in:
@@ -2,7 +2,8 @@ Space above cards: <input type="number" [formControl]="topHeightCtrl">
|
||||
<div [style.height.px]="topHeightCtrl.value"></div>
|
||||
<div class="demo-autocomplete">
|
||||
<md-card>
|
||||
<div>Reactive value: {{ stateCtrl.value }}</div>
|
||||
Reactive length: {{ reactiveStates.length }}
|
||||
<div>Reactive value: {{ stateCtrl.value | json }}</div>
|
||||
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>
|
||||
|
||||
<md-input-container>
|
||||
@@ -11,7 +12,7 @@ Space above cards: <input type="number" [formControl]="topHeightCtrl">
|
||||
|
||||
<md-card-actions>
|
||||
<button md-button (click)="stateCtrl.reset()">RESET</button>
|
||||
<button md-button (click)="stateCtrl.setValue('California')">SET VALUE</button>
|
||||
<button md-button (click)="stateCtrl.setValue(states[10])">SET VALUE</button>
|
||||
<button md-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
|
||||
TOGGLE DISABLED
|
||||
</button>
|
||||
@@ -39,8 +40,8 @@ Space above cards: <input type="number" [formControl]="topHeightCtrl">
|
||||
</md-card>
|
||||
</div>
|
||||
|
||||
<md-autocomplete #reactiveAuto="mdAutocomplete">
|
||||
<md-option *ngFor="let state of reactiveStates" [value]="state.name">
|
||||
<md-autocomplete #reactiveAuto="mdAutocomplete" [displayWith]="displayFn">
|
||||
<md-option *ngFor="let state of reactiveStates | async" [value]="state">
|
||||
<span>{{ state.name }}</span>
|
||||
<span class="demo-secondary-text"> ({{state.code}}) </span>
|
||||
</md-option>
|
||||
@@ -49,6 +50,5 @@ Space above cards: <input type="number" [formControl]="topHeightCtrl">
|
||||
<md-autocomplete #tdAuto="mdAutocomplete">
|
||||
<md-option *ngFor="let state of tdStates" [value]="state.name">
|
||||
<span>{{ state.name }}</span>
|
||||
<span class="demo-secondary-text"> ({{state.code}}) </span>
|
||||
</md-option>
|
||||
</md-autocomplete>
|
||||
@@ -3,7 +3,7 @@
|
||||
flex-flow: row wrap;
|
||||
|
||||
md-card {
|
||||
width: 350px;
|
||||
width: 400px;
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Component, OnDestroy, ViewEncapsulation} from '@angular/core';
|
||||
import {Component, ViewEncapsulation} from '@angular/core';
|
||||
import {FormControl} from '@angular/forms';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/startWith';
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
@@ -9,15 +10,14 @@ import {Subscription} from 'rxjs/Subscription';
|
||||
styleUrls: ['autocomplete-demo.css'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class AutocompleteDemo implements OnDestroy {
|
||||
stateCtrl = new FormControl();
|
||||
export class AutocompleteDemo {
|
||||
stateCtrl: FormControl;
|
||||
currentState = '';
|
||||
topHeightCtrl = new FormControl(0);
|
||||
|
||||
reactiveStates: any[];
|
||||
reactiveStates: Observable<any>;
|
||||
tdStates: any[];
|
||||
|
||||
reactiveValueSub: Subscription;
|
||||
tdDisabled = false;
|
||||
|
||||
states = [
|
||||
@@ -74,19 +74,20 @@ export class AutocompleteDemo implements OnDestroy {
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.reactiveStates = this.states;
|
||||
this.tdStates = this.states;
|
||||
this.reactiveValueSub =
|
||||
this.stateCtrl.valueChanges.subscribe(val => this.reactiveStates = this.filterStates(val));
|
||||
this.stateCtrl = new FormControl({code: 'CA', name: 'California'});
|
||||
this.reactiveStates = this.stateCtrl.valueChanges
|
||||
.startWith(this.stateCtrl.value)
|
||||
.map(val => this.displayFn(val))
|
||||
.map(name => this.filterStates(name));
|
||||
}
|
||||
|
||||
displayFn(value: any): string {
|
||||
return value && typeof value === 'object' ? value.name : value;
|
||||
}
|
||||
|
||||
filterStates(val: string) {
|
||||
return val ? this.states.filter((s) => s.name.match(new RegExp(val, 'gi'))) : this.states;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.reactiveValueSub.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import {
|
||||
AfterContentInit, Directive, ElementRef, Input, ViewContainerRef, Optional, OnDestroy
|
||||
AfterContentInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
Input,
|
||||
Optional,
|
||||
OnDestroy,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import {NgControl} from '@angular/forms';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core';
|
||||
import {MdAutocomplete} from './autocomplete';
|
||||
import {PositionStrategy} from '../core/overlay/position/position-strategy';
|
||||
@@ -28,6 +35,16 @@ export const AUTOCOMPLETE_OPTION_HEIGHT = 48;
|
||||
/** The total height of the autocomplete panel. */
|
||||
export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
|
||||
|
||||
/**
|
||||
* Provider that allows the autocomplete to register as a ControlValueAccessor.
|
||||
* @docs-private
|
||||
*/
|
||||
export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => MdAutocompleteTrigger),
|
||||
multi: true
|
||||
};
|
||||
|
||||
@Directive({
|
||||
selector: 'input[mdAutocomplete], input[matAutocomplete]',
|
||||
host: {
|
||||
@@ -39,10 +56,13 @@ export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
|
||||
'[attr.aria-expanded]': 'panelOpen.toString()',
|
||||
'[attr.aria-owns]': 'autocomplete?.id',
|
||||
'(focus)': 'openPanel()',
|
||||
'(blur)': '_onTouched()',
|
||||
'(input)': '_onChange($event.target.value)',
|
||||
'(keydown)': '_handleKeydown($event)',
|
||||
}
|
||||
},
|
||||
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
|
||||
})
|
||||
export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
|
||||
export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAccessor, OnDestroy {
|
||||
private _overlayRef: OverlayRef;
|
||||
private _portal: TemplatePortal;
|
||||
private _panelOpen: boolean = false;
|
||||
@@ -54,12 +74,18 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
|
||||
private _keyManager: ActiveDescendantKeyManager;
|
||||
private _positionStrategy: ConnectedPositionStrategy;
|
||||
|
||||
/** View -> model callback called when value changes */
|
||||
_onChange: (value: any) => {};
|
||||
|
||||
/** View -> model callback called when autocomplete has been touched */
|
||||
_onTouched = () => {};
|
||||
|
||||
/* The autocomplete panel to be attached to this trigger. */
|
||||
@Input('mdAutocomplete') autocomplete: MdAutocomplete;
|
||||
|
||||
constructor(private _element: ElementRef, private _overlay: Overlay,
|
||||
private _viewContainerRef: ViewContainerRef,
|
||||
@Optional() private _controlDir: NgControl, @Optional() private _dir: Dir) {}
|
||||
@Optional() private _dir: Dir) {}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap();
|
||||
@@ -123,6 +149,38 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
|
||||
return this._keyManager.activeItem as MdOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the autocomplete's value. Part of the ControlValueAccessor interface
|
||||
* required to integrate with Angular's core forms API.
|
||||
*
|
||||
* @param value New value to be written to the model.
|
||||
*/
|
||||
writeValue(value: any): void {
|
||||
Promise.resolve(null).then(() => this._setTriggerValue(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a callback function to be invoked when the autocomplete's value
|
||||
* changes from user input. Part of the ControlValueAccessor interface
|
||||
* required to integrate with Angular's core forms API.
|
||||
*
|
||||
* @param fn Callback to be triggered when the value changes.
|
||||
*/
|
||||
registerOnChange(fn: (value: any) => {}): void {
|
||||
this._onChange = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a callback function to be invoked when the autocomplete is blurred
|
||||
* by the user. Part of the ControlValueAccessor interface required
|
||||
* to integrate with Angular's core forms API.
|
||||
*
|
||||
* @param fn Callback to be triggered when the component has been touched.
|
||||
*/
|
||||
registerOnTouched(fn: () => {}) {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
_handleKeydown(event: KeyboardEvent): void {
|
||||
if (this.activeOption && event.keyCode === ENTER) {
|
||||
this.activeOption._selectViaInteraction();
|
||||
@@ -178,6 +236,11 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private _setTriggerValue(value: any): void {
|
||||
this._element.nativeElement.value =
|
||||
this.autocomplete.displayWith ? this.autocomplete.displayWith(value) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method closes the panel, and if a value is specified, also sets the associated
|
||||
* control to that value. It will also mark the control as dirty if this interaction
|
||||
@@ -185,10 +248,8 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
|
||||
*/
|
||||
private _setValueAndClose(event: MdOptionSelectEvent | null): void {
|
||||
if (event) {
|
||||
this._controlDir.control.setValue(event.source.value);
|
||||
if (event.isUserInput) {
|
||||
this._controlDir.control.markAsDirty();
|
||||
}
|
||||
this._setTriggerValue(event.source.value);
|
||||
this._onChange(event.source.value);
|
||||
}
|
||||
|
||||
this.closePanel();
|
||||
|
||||
@@ -11,6 +11,7 @@ import {ENTER, DOWN_ARROW, SPACE, UP_ARROW} from '../core/keyboard/keycodes';
|
||||
import {MdOption} from '../core/option/option';
|
||||
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
|
||||
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
|
||||
import {MdAutocomplete} from './autocomplete';
|
||||
|
||||
|
||||
describe('MdAutocomplete', () => {
|
||||
@@ -201,7 +202,26 @@ describe('MdAutocomplete', () => {
|
||||
input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
});
|
||||
|
||||
it('should fill the text field when an option is selected', () => {
|
||||
it('should update control value as user types with input value', () => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
|
||||
input.value = 'a';
|
||||
dispatchEvent('input', input);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.stateCtrl.value)
|
||||
.toEqual('a', 'Expected control value to be updated as user types.');
|
||||
|
||||
input.value = 'al';
|
||||
dispatchEvent('input', input);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.stateCtrl.value)
|
||||
.toEqual('al', 'Expected control value to be updated as user types.');
|
||||
});
|
||||
|
||||
it('should update control value when option is selected with option value', () => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -210,8 +230,126 @@ describe('MdAutocomplete', () => {
|
||||
options[1].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(input.value)
|
||||
.toContain('California', `Expected text field to fill with selected value.`);
|
||||
expect(fixture.componentInstance.stateCtrl.value)
|
||||
.toEqual({code: 'CA', name: 'California'}, 'Expected control value to be option value.');
|
||||
});
|
||||
|
||||
it('should update control back to string if user types after option is selected', () => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
|
||||
const options =
|
||||
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
|
||||
options[1].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
input.value = 'Californi';
|
||||
dispatchEvent('input', input);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.stateCtrl.value)
|
||||
.toEqual('Californi', 'Expected control value to revert back to string.');
|
||||
});
|
||||
|
||||
it('should fill the text field with display value when an option is selected', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
|
||||
const options =
|
||||
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
|
||||
options[1].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(input.value)
|
||||
.toContain('California', `Expected text field to fill with selected value.`);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should fill the text field with value if displayWith is not set', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.panel.displayWith = null;
|
||||
fixture.componentInstance.options.toArray()[1].value = 'test value';
|
||||
fixture.detectChanges();
|
||||
|
||||
const options =
|
||||
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
|
||||
options[1].click();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(input.value)
|
||||
.toContain('test value', `Expected input to fall back to selected option's value.`);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should fill the text field correctly if value is set to obj programmatically', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.stateCtrl.setValue({code: 'AL', name: 'Alabama'});
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(input.value)
|
||||
.toContain('Alabama', `Expected input to fill with matching option's viewValue.`);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should clear the text field if value is reset programmatically', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
input.value = 'Alabama';
|
||||
dispatchEvent('input', input);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(input.value).toEqual('Alabama', `Expected input to start out with a value.`);
|
||||
fixture.componentInstance.stateCtrl.reset();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(input.value).toEqual('', `Expected input value to be empty after reset.`);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should disable input in view when disabled programmatically', () => {
|
||||
const inputUnderline =
|
||||
fixture.debugElement.query(By.css('.md-input-underline')).nativeElement;
|
||||
|
||||
expect(input.disabled)
|
||||
.toBe(false, `Expected input to start out enabled in view.`);
|
||||
expect(inputUnderline.classList.contains('md-disabled'))
|
||||
.toBe(false, `Expected input underline to start out with normal styles.`);
|
||||
|
||||
fixture.componentInstance.stateCtrl.disable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(input.disabled)
|
||||
.toBe(true, `Expected input to be disabled in view when disabled programmatically.`);
|
||||
expect(inputUnderline.classList.contains('md-disabled'))
|
||||
.toBe(true, `Expected input underline to display disabled styles.`);
|
||||
});
|
||||
|
||||
|
||||
it('should mark the autocomplete control as dirty as user types', () => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.stateCtrl.dirty)
|
||||
.toBe(false, `Expected control to start out pristine.`);
|
||||
|
||||
input.value = 'a';
|
||||
dispatchEvent('input', input);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.stateCtrl.dirty)
|
||||
.toBe(true, `Expected control to become dirty when the user types into the input.`);
|
||||
});
|
||||
|
||||
it('should mark the autocomplete control as dirty when an option is selected', () => {
|
||||
@@ -240,6 +378,19 @@ describe('MdAutocomplete', () => {
|
||||
.toBe(false, `Expected control to stay pristine if value is set programmatically.`);
|
||||
});
|
||||
|
||||
it('should mark the autocomplete control as touched on blur', () => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.stateCtrl.touched)
|
||||
.toBe(false, `Expected control to start out untouched.`);
|
||||
|
||||
dispatchEvent('blur', input);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.stateCtrl.touched)
|
||||
.toBe(true, `Expected control to become touched on blur.`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('keyboard events', () => {
|
||||
@@ -353,17 +504,21 @@ describe('MdAutocomplete', () => {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should fill the text field when an option is selected with ENTER', () => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
it('should fill the text field when an option is selected with ENTER', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
|
||||
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
|
||||
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
|
||||
|
||||
expect(input.value)
|
||||
.toContain('Alabama', `Expected text field to fill with selected value on ENTER.`);
|
||||
});
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(input.value)
|
||||
.toContain('Alabama', `Expected text field to fill with selected value on ENTER.`);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should fill the text field, not select an option, when SPACE is entered', () => {
|
||||
fixture.componentInstance.trigger.openPanel();
|
||||
@@ -614,9 +769,9 @@ describe('MdAutocomplete', () => {
|
||||
<input mdInput placeholder="State" [mdAutocomplete]="auto" [formControl]="stateCtrl">
|
||||
</md-input-container>
|
||||
|
||||
<md-autocomplete #auto="mdAutocomplete">
|
||||
<md-option *ngFor="let state of filteredStates" [value]="state.name">
|
||||
{{ state.name }} ({{ state.code }})
|
||||
<md-autocomplete #auto="mdAutocomplete" [displayWith]="displayFn">
|
||||
<md-option *ngFor="let state of filteredStates" [value]="state">
|
||||
<span> {{ state.code }}: {{ state.name }} </span>
|
||||
</md-option>
|
||||
</md-autocomplete>
|
||||
`
|
||||
@@ -627,6 +782,7 @@ class SimpleAutocomplete implements OnDestroy {
|
||||
valueSub: Subscription;
|
||||
|
||||
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
|
||||
@ViewChild(MdAutocomplete) panel: MdAutocomplete;
|
||||
@ViewChildren(MdOption) options: QueryList<MdOption>;
|
||||
|
||||
states = [
|
||||
@@ -643,6 +799,7 @@ class SimpleAutocomplete implements OnDestroy {
|
||||
{code: 'WY', name: 'Wyoming'},
|
||||
];
|
||||
|
||||
|
||||
constructor() {
|
||||
this.filteredStates = this.states;
|
||||
this.valueSub = this.stateCtrl.valueChanges.subscribe(val => {
|
||||
@@ -651,6 +808,10 @@ class SimpleAutocomplete implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
displayFn(value: any): string {
|
||||
return value ? value.name : value;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.valueSub.unsubscribe();
|
||||
}
|
||||
@@ -658,6 +819,7 @@ class SimpleAutocomplete implements OnDestroy {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* TODO: Move this to core testing utility until Angular has event faking
|
||||
* support.
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Component,
|
||||
ContentChildren,
|
||||
ElementRef,
|
||||
Input,
|
||||
QueryList,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
@@ -33,6 +34,9 @@ export class MdAutocomplete {
|
||||
@ViewChild('panel') panel: ElementRef;
|
||||
@ContentChildren(MdOption) options: QueryList<MdOption>;
|
||||
|
||||
/** Function that maps an option's control value to its display value in the trigger. */
|
||||
@Input() displayWith: (value: any) => string;
|
||||
|
||||
/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
|
||||
id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`;
|
||||
|
||||
@@ -41,7 +45,9 @@ export class MdAutocomplete {
|
||||
* options below the fold, as they are not actually being focused when active.
|
||||
*/
|
||||
_setScrollTop(scrollTop: number): void {
|
||||
this.panel.nativeElement.scrollTop = scrollTop;
|
||||
if (this.panel) {
|
||||
this.panel.nativeElement.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets a class on the panel based on its position (used to set y-offset). */
|
||||
|
||||
Reference in New Issue
Block a user