From 3c8a144c4f4eda5319ab9c5f5783663964d1bd3e Mon Sep 17 00:00:00 2001 From: nklincoln Date: Fri, 16 Jun 2017 15:19:49 +0100 Subject: [PATCH] add auto scroll to selected item in list --- .../composer-playground/src/app/app.module.ts | 2 + .../src/app/directives/scroll/index.ts | 1 + .../scroll-to-element.directive.spec.ts | 182 ++++++++++++++++++ .../scroll/scroll-to-element.directive.ts | 85 ++++++++ .../src/app/editor/editor.component.html | 4 +- .../src/app/editor/editor.component.scss | 2 +- .../src/app/editor/editor.component.spec.ts | 3 +- .../src/app/editor/editor.component.ts | 3 + 8 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 packages/composer-playground/src/app/directives/scroll/index.ts create mode 100644 packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.spec.ts create mode 100644 packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.ts diff --git a/packages/composer-playground/src/app/app.module.ts b/packages/composer-playground/src/app/app.module.ts index 973641936f..47c5575f16 100644 --- a/packages/composer-playground/src/app/app.module.ts +++ b/packages/composer-playground/src/app/app.module.ts @@ -67,6 +67,7 @@ import { SampleBusinessNetworkService } from './services/samplebusinessnetwork.s import { AboutService } from './services/about.service'; import { AlertService } from './services/alert.service'; import { EditorService } from './services/editor.service'; +import { ScrollToElementDirective } from './directives/scroll'; let actionBasedIcons = require.context('../assets/svg/action-based', false, /.*\.svg$/); actionBasedIcons.keys().forEach(actionBasedIcons); @@ -145,6 +146,7 @@ type StoreType = { RegistryComponent, ReplaceComponent, ResourceComponent, + ScrollToElementDirective, SuccessComponent, SwitchIdentityComponent, TestComponent, diff --git a/packages/composer-playground/src/app/directives/scroll/index.ts b/packages/composer-playground/src/app/directives/scroll/index.ts new file mode 100644 index 0000000000..954325e72c --- /dev/null +++ b/packages/composer-playground/src/app/directives/scroll/index.ts @@ -0,0 +1 @@ +export * from './scroll-to-element.directive'; diff --git a/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.spec.ts b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.spec.ts new file mode 100644 index 0000000000..9493f0da36 --- /dev/null +++ b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.spec.ts @@ -0,0 +1,182 @@ +/* tslint:disable:no-unused-variable */ +/* tslint:disable:no-unused-expression */ +/* tslint:disable:no-var-requires */ +/* tslint:disable:max-classes-per-file */ +import { ComponentFixture, TestBed, async, fakeAsync, tick, inject } from '@angular/core/testing'; +import { Component, Renderer, QueryList, ElementRef } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +import * as sinon from 'sinon'; +import * as chai from 'chai'; +import { ScrollToElementDirective } from './scroll-to-element.directive'; + +let should = chai.should(); + +@Component({ + selector: 'editorFileList', + template: ` +
+ +
` +}) + +class TestComponent { + listItem: string = 'editorFileList1'; +} + +describe('ScrollToElementDirective', () => { + let component: TestComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, ScrollToElementDirective], + providers: [Renderer] + }) + .compileComponents(); + })); + + it('should create the directive', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + tick(); + + component.should.be.ok; + }))); + + describe('#retreiveSelectedItem', () => { + + it('should return an array of nativeElement.id\'s that match the selected', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + let matchItem: ElementRef[] = directiveInstance.retreiveSelectedItem(); + matchItem[0].nativeElement.id.should.equal('editorFileList1'); + }); + + it('should return an empty array if no matches for nativeElement.id\'s', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + directiveInstance.items = new QueryList(); + + let emptyList = new QueryList(); + let matchItem: ElementRef[] = directiveInstance.retreiveSelectedItem(); + matchItem.should.be.empty; + }); + }); + + describe('#performScrollAction', () => { + + it('should call stepVerticalScoll', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + let stepSpy = sinon.spy(directiveInstance, 'stepVerticalScoll'); + + fixture.detectChanges(); + directiveInstance.performScrollAction(); + + // Big fake step to ensure all setTimeout actinos completed + tick(1000); + + // Check initial call based on test information was correct + stepSpy.getCall(0).args[0].should.equal(0.08, 0); + }))); + + it('should not action if no items matched for selected element', () => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + let stepSpy = sinon.spy(directiveInstance, 'stepVerticalScoll'); + + directiveInstance.items = new QueryList(); + + directiveInstance.performScrollAction(); + + // Check no call + stepSpy.should.not.have.been.called; + }); + }); + + describe('#isOvershoot', () => { + + it('should detect positive overshoot', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + directiveInstance.isOvershoot(1, 10, 1).should.be.true; + }); + + it('should detect negative overshoot', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + directiveInstance.isOvershoot(-1, -10, -1).should.be.true; + }); + + }); + + describe('inputs', () => { + + it('should do nothing if data not initialised', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + component.listItem = 'editorFileList2'; + let scrollMock = sinon.stub(directiveInstance, 'performScrollAction'); + + fixture.detectChanges(); + tick(); + + scrollMock.should.not.have.been.called; + }))); + + it('should call performScrollAction and data initialised', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + // initialise data + directiveInstance.ngAfterContentInit(); + + // set input + component.listItem = 'editorFileList2'; + let scrollMock = sinon.stub(directiveInstance, 'performScrollAction'); + + fixture.detectChanges(); + tick(); + + scrollMock.should.have.been.called; + }))); + + it('should update when selecteditem changes and data initialised', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + // initialise data + directiveInstance.ngAfterContentInit(); + + component.listItem = 'editorFileList2'; + let scrollMock = sinon.stub(directiveInstance, 'performScrollAction'); + + fixture.detectChanges(); + tick(); + + scrollMock.should.have.been.called; + }))); + }); +}); diff --git a/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.ts b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.ts new file mode 100644 index 0000000000..607fad4fc8 --- /dev/null +++ b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.ts @@ -0,0 +1,85 @@ +import { + Directive, + ElementRef, + Input, + Renderer, + ContentChildren, + AfterContentInit, + QueryList +} from '@angular/core'; + +@Directive({ + selector: '[scroll-to-element]', +}) + +export class ScrollToElementDirective implements AfterContentInit { + + @Input() + set elementId(elementId) { + this._thing = elementId; + if (this._thing && this.initialised) { + this.performScrollAction(); + } + } + + @ContentChildren('editorFileList') items: QueryList; + + private _thing = null; + private initialised = false; + + constructor(private el: ElementRef, private renderer: Renderer) { + } + + ngAfterContentInit() { + this.initialised = true; + } + + performScrollAction() { + let element = this.el; + let selectedItem = this.retreiveSelectedItem(); + if (selectedItem && selectedItem.length > 0) { + let parentOffset = element.nativeElement.offsetTop; + let selectOffset = selectedItem[0].nativeElement.offsetTop; + + let endScrollTop = selectOffset - parentOffset - 10; + let startScrollTop = element.nativeElement.scrollTop; + let scrollDiff = startScrollTop - endScrollTop; + + let steps = 100; let timer = 0; let slow = 4; + let step = scrollDiff / steps; + let stepTarget = startScrollTop - step; + while (steps > 0) { + this.stepVerticalScoll(stepTarget, slow * timer); + timer++; // slow on approach to target + steps--; // while condition + stepTarget -= step; + // Prevent overshoot + if ( this.isOvershoot(scrollDiff, endScrollTop, stepTarget) ) { + steps = 0; + } + // Final adjust + if (steps === 0) { + this.stepVerticalScoll(endScrollTop, slow * timer); + } + } + } + } + + isOvershoot(scrollDiff, endScrollTop, stepTarget) { + if (scrollDiff < 0) { + return stepTarget > endScrollTop; + } else { + return stepTarget < endScrollTop; + } + } + + retreiveSelectedItem() { + return this.items.filter( (item) => { return item.nativeElement.id === this._thing; }); + } + + stepVerticalScoll(yLocation, duration) { + setTimeout(() => { + this.renderer.setElementProperty(this.el.nativeElement, 'scrollTop', yLocation); + }, duration); + } +} diff --git a/packages/composer-playground/src/app/editor/editor.component.html b/packages/composer-playground/src/app/editor/editor.component.html index 36ce78daed..99380d13b0 100644 --- a/packages/composer-playground/src/app/editor/editor.component.html +++ b/packages/composer-playground/src/app/editor/editor.component.html @@ -1,9 +1,9 @@