diff --git a/package.json b/package.json index ca27b79..dbf673b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "virtual-repeat-angular-lib-app", - "version": "0.4.4", + "version": "0.4.5", "scripts": { "serve": "ng serve", "build_prod": "ng build --prod", @@ -11,7 +11,7 @@ "build_lib": "ng build --prod virtual-repeat-angular-lib", "npm_pack": "cp README.md dist/virtual-repeat-angular-lib && cd dist/virtual-repeat-angular-lib && npm pack && cp *.tgz ../releases/", "npm_build_and_package": "npm run build_lib && npm run npm_pack", - "npm_publish": "npm publish dist/virtual-repeat-angular-lib/virtual-repeat-angular-0.4.4.tgz", + "npm_publish": "npm publish dist/virtual-repeat-angular-lib/virtual-repeat-angular-0.4.5.tgz", "ngh_publish": "npm run build_lib && ng build --prod --base-href https://gerardcarbo.github.io/virtual-repeat-angular/ && ngh --dir dist/virtual-repeat-angular-lib-app", "git_push": "git push -u origin master", "git_push_force": "git push -u -f origin master", diff --git a/projects/virtual-repeat-angular-lib/package.json b/projects/virtual-repeat-angular-lib/package.json index c52ba9f..9e6b290 100644 --- a/projects/virtual-repeat-angular-lib/package.json +++ b/projects/virtual-repeat-angular-lib/package.json @@ -1,6 +1,6 @@ { "name": "virtual-repeat-angular", - "version": "0.4.4", + "version": "0.4.5", "author": { "name": "Gerard Carbó" }, diff --git a/projects/virtual-repeat-angular-lib/src/lib/logger.service.ts b/projects/virtual-repeat-angular-lib/src/lib/logger.service.ts index 3150003..3609d52 100644 --- a/projects/virtual-repeat-angular-lib/src/lib/logger.service.ts +++ b/projects/virtual-repeat-angular-lib/src/lib/logger.service.ts @@ -1,34 +1,36 @@ -import { isDevMode } from '@angular/core'; +import { noop } from 'rxjs'; export class LoggerService { + log: any; constructor() { - const forceLog = Boolean(localStorage.getItem('gcvra_force_log')); // force log even in devMode - const filterLog = localStorage.getItem('gcvra_filter_log'); // filter log lines (; separated list) + const bLog = Boolean(localStorage.getItem('gcvra_log')); // enable log + const filterLog = localStorage.getItem('gcvra_log_filter'); // filter log lines (; separated list) let filterLogTerms: string[]; if (filterLog) { - filterLogTerms = filterLog.split(';').map(term => term.trim().toLowerCase()).filter(term => !!term); + filterLogTerms = filterLog + .split(';') + .map(term => term.trim().toLowerCase()) + .filter(term => !!term); } - setTimeout(() => { // hack to be able to use isDevMode() - if (isDevMode() || forceLog) { - if (filterLog) { - this.log = function (text: string, ...args: any[]) { - let done = false; - filterLogTerms.forEach(term => { - if (!done && text.toLowerCase().indexOf(term) !== -1) { - console.log(text, ...args); - done = true; - } - }); - return; - }; - } else { - this.log = function (text: string, ...args: any[]) { - console.log(text, ...args); - }; - } + if (bLog) { + if (filterLog) { + this.log = function(text: string, ...args: any[]) { + let done = false; + filterLogTerms.forEach(term => { + if (!done && text.toLowerCase().indexOf(term) !== -1) { + console.log(text, ...args); + done = true; + } + }); + return; + }; + } else { + this.log = function(text: string, ...args: any[]) { + console.log(text, ...args); + }; } - }, 100); + } else { + this.log = noop; + } } - - log(text: string, ...args: any[]) { } } diff --git a/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.spec.ts b/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.spec.ts index 82e379a..a4fa685 100644 --- a/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.spec.ts +++ b/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.spec.ts @@ -2,11 +2,14 @@ import { TestBed, inject } from '@angular/core/testing'; import { LoggerService } from './logger.service'; import { Subject } from 'rxjs'; -import { deglitch, throttleTimeUntilChanged } from './rxjs.operators'; +import { + deglitch, + throttleTimeUntilChanged, + deglitchFalse +} from './rxjs.operators'; import { tap } from 'rxjs/operators'; describe('RXJs operators', () => { - beforeEach(() => { TestBed.configureTestingModule({ providers: [LoggerService] @@ -111,7 +114,7 @@ describe('RXJs operators', () => { }); describe('deglitch', () => { - it('should change simple', function(done) { + it('should change simple (0-->1)', function(done) { inject([LoggerService], function(logger: LoggerService) { let finalState; let count = 0; @@ -322,6 +325,178 @@ describe('RXJs operators', () => { })(); }); }); + + describe('deglitchFalse', () => { + it('should change simple (0-->1)', function(done) { + inject([LoggerService], function(logger: LoggerService) { + logger.log('test: should change simple (0-->1)'); + let finalState; + let count = 0; + const test = new Subject(); + test + .pipe( + deglitchFalse(100), + tap(state => { + logger.log('test: tap: ' + state + ' count: ' + count); + finalState = state; + if (count === 0) { + expect(state).toBe(false); + } else { + expect(state).toBe(true); + } + count++; + }) + ) + .subscribe({ + complete: () => { + expect(finalState).toBe(true); + done(); + } + }); + + setTimeout(() => test.next(false), 0); + setTimeout(() => test.next(true), 500); + setTimeout(() => test.complete(), 650); + })(); + }); + + it('should NOT deglitch simple true (0-->1->0)', function(done) { + inject([LoggerService], function(logger: LoggerService) { + logger.log('test: should NOT deglitch simple true (0-->1->0)'); + let finalState; + const test = new Subject(); + let count = 0; + test + .pipe( + deglitchFalse(100), + tap(state => { + finalState = state; + count++; + logger.log('test: tap: ' + state + ' count: ' + count); + }) + ) + .subscribe({ + complete: () => { + expect(count).toBe(3); + expect(finalState).toBeFalsy(); + done(); + } + }); + setTimeout(() => test.next(false), 0); + setTimeout(() => test.next(true), 500); + setTimeout(() => test.next(false), 550); + setTimeout(() => test.complete(), 650); + })(); + }); + + it('should deglitch simple false (1-->0->1)', function(done) { + inject([LoggerService], function(logger: LoggerService) { + logger.log('test: should deglitch simple false (1-->0->1)'); + let finalState; + const test = new Subject(); + let count = 0; + test + .pipe( + deglitchFalse(100), + tap(state => { + finalState = state; + count++; + logger.log('test: tap: ' + state + ' count: ' + count); + }) + ) + .subscribe({ + complete: () => { + expect(count).toBe(1); + expect(finalState).toBe(true); + done(); + } + }); + setTimeout(() => test.next(true), 0); + setTimeout(() => test.next(false), 500); + setTimeout(() => test.next(true), 550); + setTimeout(() => test.complete(), 650); + })(); + }); + + it('should deglitch double false (0-->1-->0->1)', function(done) { + inject([LoggerService], function(logger: LoggerService) { + let finalState; + let count = 0; + const test = new Subject(); + test + .pipe( + deglitchFalse(100), + tap(state => { + logger.log('test: tap: ' + state + ' count: ' + count); + finalState = state; + if (count === 0) { + expect(state).toBe(false); + } + if (count === 1) { + expect(state).toBe(true); + } + count++; + }) + ) + .subscribe({ + complete: () => { + expect(count).toBe(2); // 0->1 + expect(finalState).toBe(true); + done(); + } + }); + + setTimeout(() => test.next(false), 0); + setTimeout(() => test.next(true), 500); + setTimeout(() => test.next(false), 650); + setTimeout(() => test.next(true), 700); + setTimeout(() => test.complete(), 750); + })(); + }); + + fit('should NOT deglitch double true (1-->0-->1->0)', function(done) { + inject([LoggerService], function(logger: LoggerService) { + let finalState; + let count = 0; + const test = new Subject(); + test + .pipe( + deglitchFalse(100), + tap(state => { + logger.log('test: tap: ' + state + ' count: ' + count); + finalState = state; + if (count === 0) { + expect(state).toBe(true); + } + if (count === 1) { + expect(state).toBe(false); + } + if (count === 2) { + expect(state).toBe(true); + } + if (count === 3) { + expect(state).toBe(false); + } + count++; + }) + ) + .subscribe({ + complete: () => { + expect(count).toBe(4); // 1->0->1->0 + expect(finalState).toBe(false); + done(); + } + }); + + setTimeout(() => test.next(true), 0); + setTimeout(() => test.next(false), 500); + setTimeout(() => test.next(true), 650); + setTimeout(() => test.next(false), 700); + setTimeout(() => test.complete(), 750); + })(); + }); + + }); }); // Check logging disabled times diff --git a/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.ts b/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.ts index 472d5c6..a54ba10 100644 --- a/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.ts +++ b/projects/virtual-repeat-angular-lib/src/lib/rxjs.operators.ts @@ -89,3 +89,61 @@ export function deglitch(glitchSize: number) { }); }; } + +/** + * Remove spurious falses on a boolean observable. + * @param {number} glitchSize max size of the gitches (in miliseconds) to be removed. + */ +export function deglitchFalse(glitchSize: number) { + return (source: Observable) => { + return new Observable(observer => { + let currentState: boolean; + let lastState: boolean; + let lastStateTime: number; + + return source + .pipe( + flatMap((value: boolean) => { + // logger.log(`deglitchFalse: value: ${value} currentState: ${currentState} `); + lastStateTime = Date.now(); + lastState = value; + if (currentState === undefined || (value === true && currentState !== value)) { + currentState = value; + return of(value); + } + if (value === currentState) { + return empty(); + } else { + return of(value).pipe( + delay(glitchSize), + flatMap((value_: boolean) => { + const elapsed = Date.now() - lastStateTime; + // logger.log(`deglitchFalse -> delay elapsed: ${elapsed} value_: ${value_} lastState: ${lastState} currentState: ${currentState} `); + + if (value_ !== lastState) { + if (lastState === currentState) { + // logger.log(`deglitchFalse -> delay lastState === currentState -> empty()`); + return empty(); + } else { + // logger.log(`deglitchFalse -> delay elapsed: ${elapsed} lastState !== currentState -> ${lastState}`); + currentState = lastState; + return of(currentState); + } + } + // logger.log(`deglitchFalse -> delay value_ !== lastState -> ${value_}`); + if (elapsed < glitchSize) { + // logger.log(`deglitchFalse -> delay ${elapsed} < glitchSize -> empty()`); + return empty(); + } + currentState = value_; + return of(currentState); + }) + ); + } + }) + ) + .subscribe(observer); + }); + }; +} + diff --git a/projects/virtual-repeat-angular-lib/src/lib/virtual-repeat-container.ts b/projects/virtual-repeat-angular-lib/src/lib/virtual-repeat-container.ts index 76c4336..c8916a4 100644 --- a/projects/virtual-repeat-angular-lib/src/lib/virtual-repeat-container.ts +++ b/projects/virtual-repeat-angular-lib/src/lib/virtual-repeat-container.ts @@ -26,7 +26,7 @@ import { } from 'rxjs/operators'; import { LoggerService } from './logger.service'; import { IVirtualRepeat } from './virtual-repeat.base'; -import { deglitch } from './rxjs.operators'; +import { deglitch, deglitchFalse } from './rxjs.operators'; export const SCROLL_STOP_TIME_THRESHOLD = 200; // in milliseconds @@ -187,13 +187,13 @@ export class VirtualRepeatContainer implements AfterViewInit, OnDestroy { private _processingSubject = new Subject(); public processingRaw$ = this._processingSubject.pipe( tap(state => { - console.log('processingRaw$ ' + state); + this.logger.log('processingRaw$ ' + state); }) ); public processing$ = this._processingSubject.pipe( - deglitch(200), + deglitchFalse(500), tap(state => { - console.log('processing$ ' + state); + this.logger.log('processing$ ' + state); }) );