Skip to content

Commit

Permalink
fix: scroll spy threshold is customizable (#578)
Browse files Browse the repository at this point in the history
* scroll spy enhancement

* added HTMLElement type
  • Loading branch information
MattL75 committed Feb 27, 2019
1 parent d63fbc8 commit f1b7657
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="header-container">
<div class="header-item" *ngFor="let item of items" [ngClass]="{'selected': selectedSpy === item.id}">
<span class="icon">
<fd-icon [glyph]="'arrow-right'" [size]="'xs'"></fd-icon>
</span>
{{item.name}}
</div>
</div>

<div class="list-container" fdScrollSpy [trackedTags]="['div']" [targetPercent]="0.5" (spyChange)="selectedSpy = $event.id">
<div class="item" *ngFor="let item of items" [id]="item.id">{{item.name}}</div>
</div>

<div class="middle-marker-container">
<div class="marker"></div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
:host {
display: flex;
flex-direction: row;
height: 190px;
position: relative;
}

.item {
height: 40px;
display: flex;
align-items: center;

&:last-child {
height: 220px;
align-items: flex-start;
padding-top: 10px;
}
}

.list-container {
border: 1px dashed black;
overflow: auto;
flex-grow: 1;
}

.header-container {
flex-direction: column;
display: flex;
width: 140px;
.header-item {
display: inline-block;
}
}

.icon {
left: -4px;
opacity: 0;
}

.selected {
font-weight: bold;
.icon {
opacity: 1;
}
}

.middle-marker-container {
position: absolute;
top: 50%;
right: 0;
height: 0;
width: calc(100% - 140px);
.marker {
height: 1px;
border: 0.5px dashed black;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'fd-scroll-spy-custom-example',
templateUrl: './scroll-spy-custom-example.component.html',
styleUrls: ['./scroll-spy-custom-example.component.scss']
})
export class ScrollSpyCustomExampleComponent implements OnInit {

selectedSpy = 'element-2';
items: any[] = [];

ngOnInit() {
this.generateItems(9);
}

generateItems(count: number): void {
for (let i = 0; i < count; ++i) {
this.items.push({name: 'Element ' + i, id: 'element-' + i});
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
</div>
</div>

<div class="list-container" fdScrollSpy [trackedTags]="['div']" (topSpyChange)="selectedSpy = $event">
<div class="list-container" fdScrollSpy [trackedTags]="['div']" (spyChange)="selectedSpy = $event.id">
<div class="item" *ngFor="let item of items" [id]="item.id">{{item.name}}</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
.list-container {
border: 1px dashed black;
overflow: auto;
flex-grow: 0.75;
flex-grow: 1;
}

.header-container {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<description>
Scroll Spy is a tool <strong>directive</strong> designed to help navigation elements determine the element currently in view of the user.
It takes in an array of tags to track, and when one of those tags goes over the top scroll limit, an event is fired.
<br /><br />
The examples below make use of the id to determine what element is spied on, but many other approaches are also possible.
</description>
<import module="ScrollSpyModule"
path="fundamental-ngx"></import>
Expand All @@ -12,10 +14,11 @@ <h1>fdScrollSpy</h1>
<properties [properties]="{
inputs: [
{ name: 'trackedTags', description: 'String array. The tags to track, such as \'div\'. Case insensitive.'},
{ name: 'fireEmpty', description: 'Boolean. Whether the directive should fire empty events. Defaults to false.'}
{ name: 'fireEmpty', description: 'Boolean. Whether the directive should fire empty events. Defaults to false.'},
{ name: 'targetPercent', description: 'Number between 0 and 1. Percentage of the container at which to fire events. 0 is top, 1 is bottom.'}
],
outputs: [
{ name: 'topSpyChange', description: 'Fires when the top element being spied changes. Returns the id of the element.' }
{ name: 'spyChange', description: 'Fires when the element being spied changes. Returns the element.' }
]
}"></properties>

Expand All @@ -30,3 +33,14 @@ <h2>Standard Scroll Spy</h2>
</component-example>
<code-example [code]="scrollSpyHtml" [language]="'HTML'"></code-example>
<code-example [code]="scrollSpyTs" [language]="'typescript'"></code-example>

<separator></separator>
<h2>Custom Detection Threshold</h2>
<description>
The <code>targetPercent</code> property allows you to control at what location in the container the event is fired. For example, a value of <code>0.5</code> would fire the events in the middle of the container, <code>0</code> for the top and <code>1</code> for the bottom.
</description>
<component-example [name]="'ex2'">
<fd-scroll-spy-custom-example></fd-scroll-spy-custom-example>
</component-example>
<code-example [code]="scrollSpyCustomHtml" [language]="'HTML'"></code-example>
<code-example [code]="scrollSpyCustomTs" [language]="'typescript'"></code-example>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Component } from '@angular/core';
import * as standardH from '!raw-loader!./examples/scroll-spy-example/scroll-spy-example.component.html';
import * as standardT from '!raw-loader!./examples/scroll-spy-example/scroll-spy-example.component.ts';

import * as customH from '!raw-loader!./examples/scroll-spy-custom-example/scroll-spy-custom-example.component.html';
import * as customT from '!raw-loader!./examples/scroll-spy-custom-example/scroll-spy-custom-example.component.ts';

@Component({
selector: 'app-scroll-spy-docs',
templateUrl: './scroll-spy-docs.component.html',
Expand All @@ -12,4 +15,7 @@ export class ScrollSpyDocsComponent {

scrollSpyHtml = standardH;
scrollSpyTs = standardT;

scrollSpyCustomHtml = customH;
scrollSpyCustomTs = customT;
}
7 changes: 4 additions & 3 deletions docs/modules/documentation/documentation.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ import { DatetimeNonMeridianExampleComponent } from './containers/datetime-picke
import { DatetimeProgramExampleComponent } from './containers/datetime-picker/examples/datetime-program-example/datetime-program-example.component';
import { ScrollSpyDocsComponent } from './containers/scroll-spy/scroll-spy-docs.component';
import { ScrollSpyExampleComponent } from './containers/scroll-spy/examples/scroll-spy-example/scroll-spy-example.component';

import { ScrollSpyCustomExampleComponent } from './containers/scroll-spy/examples/scroll-spy-custom-example/scroll-spy-custom-example.component';
import { MultiInputFilterExampleComponent } from './containers/multi-input/examples/multi-input-filter-example/multi-input-filter-example.component';
import { MultiInputDisplaywithExampleComponent } from './containers/multi-input/examples/multi-input-displaywith-example/multi-input-displaywith-example.component';
import { MultiInputDocsComponent } from './containers/multi-input/multi-input-docs.component';
import { MultiInputExampleComponent } from './containers/multi-input/examples/multi-input-example/multi-input-example.component';

Expand All @@ -238,8 +240,6 @@ import { COMPONENT_SCHEMAS } from './containers/schemas';
import * as hljs from 'highlight.js';
import { HighlightJsModule, HIGHLIGHT_JS } from 'angular-highlight-js';
import { UtilsModule } from '../../../library/src/lib/utils/utils.module';
import { MultiInputFilterExampleComponent } from './containers/multi-input/examples/multi-input-filter-example/multi-input-filter-example.component';
import { MultiInputDisplaywithExampleComponent } from './containers/multi-input/examples/multi-input-displaywith-example/multi-input-displaywith-example.component';

export function highlightJsFactory() {
return hljs;
Expand Down Expand Up @@ -440,6 +440,7 @@ const ROUTES: Routes = [
PopoverProgrammaticOpenExampleComponent,
ScrollSpyDocsComponent,
ScrollSpyExampleComponent,
ScrollSpyCustomExampleComponent,
SearchInputExampleComponent,
SearchInputDynamicExampleComponent,
ShellbarBasicExampleComponent,
Expand Down
17 changes: 9 additions & 8 deletions library/src/lib/scroll-spy/scroll-spy.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { By } from '@angular/platform-browser';
@Component({
selector: 'fd-scroll-spy-test-component',
template: `
<div fdScrollSpy [trackedTags]="['div']" (topSpyChange)="selectedSpy = $event">
<div style="top: 11px; position: absolute;" id="div1"></div>
<span style="top: 12px; position: absolute;" id="span1"></span>
<div style="top: 99px; position: absolute;" id="div2"></div>
<div fdScrollSpy [trackedTags]="['div']" (spyChange)="selectedSpy = $event.id">
<div id="div1"></div>
<span id="span1"></span>
<div id="div2"></div>
</div>`
})
export class ScrollSpyTestComponent {
Expand Down Expand Up @@ -53,12 +53,13 @@ describe('ScrollSpyDirective', () => {
const mockEvent = {
target: {
scrollTop: 5,
offsetTop: 10
offsetTop: 700,
offsetHeight: 20
}
};
spyOn(directiveInstance.topSpyChange, 'emit');
spyOn(directiveInstance.spyChange, 'emit');
directiveInstance.onScroll(mockEvent);
expect(directiveInstance.currentTop).toEqual('div1');
expect(directiveInstance.topSpyChange.emit).toHaveBeenCalledWith(directiveInstance.currentTop);
expect(directiveInstance.currentActive.id).toEqual('div2');
expect(directiveInstance.spyChange.emit).toHaveBeenCalledWith(directiveInstance.currentActive);
});
});
23 changes: 13 additions & 10 deletions library/src/lib/scroll-spy/scroll-spy.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,35 @@ export class ScrollSpyDirective {
@Input()
public fireEmpty: boolean = false;

@Input()
public targetPercent: number = 0;

@Output()
public topSpyChange: EventEmitter<string> = new EventEmitter<string>();
public spyChange: EventEmitter<HTMLElement> = new EventEmitter<HTMLElement>();

private currentTop: string;
private currentActive: HTMLElement;

constructor(private elRef: ElementRef) {}

@HostListener('scroll', ['$event'])
onScroll(event: any) {
let topSpiedTag: string;
let spiedTag: HTMLElement;
const children = this.elRef.nativeElement.children;
const targetScrollTop = event.target.scrollTop;
const targetOffsetTop = event.target.offsetTop;

for (let i = 0; i < children.length; i++) {
const element = children[i];
if (this.trackedTags.some(tag => tag.toLocaleLowerCase() === element.tagName.toLocaleLowerCase())) {
if ((element.offsetTop - targetOffsetTop) <= targetScrollTop) {
topSpiedTag = element.id;
const element: HTMLElement = children[i];
if (this.trackedTags.some(tag => tag.toLocaleUpperCase() === element.tagName.toLocaleUpperCase())) {
if ((element.offsetTop - targetOffsetTop) <= targetScrollTop + event.target.offsetHeight * this.targetPercent) {
spiedTag = element;
}
}
}

if ((topSpiedTag || this.fireEmpty) && topSpiedTag !== this.currentTop) {
this.currentTop = topSpiedTag;
this.topSpyChange.emit(this.currentTop);
if ((spiedTag || this.fireEmpty) && spiedTag !== this.currentActive) {
this.currentActive = spiedTag;
this.spyChange.emit(this.currentActive);
}
}

Expand Down

0 comments on commit f1b7657

Please sign in to comment.