├── projects ├── demo │ ├── src │ │ ├── typings.d.ts │ │ ├── polyfills.ts │ │ ├── main.server.ts │ │ ├── assets │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-70x70.png │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── browserconfig.xml │ │ │ ├── site.webmanifest │ │ │ ├── web-api.svg │ │ │ ├── logo.svg │ │ │ └── safari-pinned-tab.svg │ │ ├── app │ │ │ ├── app.server.module.ts │ │ │ ├── app.routes.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.browser.module.ts │ │ │ └── app.component.less │ │ ├── main.browser.ts │ │ ├── styles.css │ │ └── index.html │ ├── tsconfig.json │ ├── tsconfig.demo.json │ ├── tsconfig.server.json │ ├── tsconfig.spec.json │ ├── .gitignore │ ├── package.json │ ├── karma.conf.js │ ├── angular.json │ └── server.ts └── intersection-observer │ ├── ng-package.json │ ├── src │ ├── tokens │ │ ├── intersection-root.ts │ │ ├── intersection-threshold.ts │ │ ├── support.ts │ │ ├── tests │ │ │ └── support.spec.ts │ │ └── intersection-root-margin.ts │ ├── utils │ │ ├── root-margin-factory.ts │ │ └── threshold-factory.ts │ ├── directives │ │ ├── intersection-root.directive.ts │ │ ├── intersection-observee.directive.ts │ │ ├── intersection-observer.directive.ts │ │ └── tests │ │ │ └── intersection-observee.spec.ts │ ├── module.ts │ ├── public-api.ts │ ├── test.ts │ └── services │ │ ├── intersection-observee.service.ts │ │ ├── tests │ │ └── intersection-observer.service.spec.ts │ │ └── intersection-observer.service.ts │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── LICENSE │ └── karma.conf.js ├── commitlint.config.js ├── .husky ├── commit-msg └── pre-commit ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ └── ci.yml └── PULL_REQUEST_TEMPLATE.md ├── .editorconfig ├── tsconfig.eslint.json ├── prettier.config.js ├── scripts ├── postbuild.js └── syncVersions.js ├── .gitignore ├── tsconfig.json ├── LICENSE ├── CONTRIBUTING.md ├── .eslintrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── package.json ├── angular.json └── README.md /projects/demo/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*'; 2 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /projects/demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'intersection-observer'; 2 | import 'zone.js'; 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /projects/demo/src/main.server.ts: -------------------------------------------------------------------------------- 1 | export {AppServerModule} from './app/app.server.module'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC1090 3 | . "$(dirname "$0")/_/husky.sh" 4 | 5 | npx commitlint --edit $1 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: ng-web-apis 4 | issuehunt: ng-web-apis 5 | -------------------------------------------------------------------------------- /projects/demo/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=SC1090 3 | . "$(dirname "$0")/_/husky.sh" 4 | 5 | npx lint-staged 6 | npm run typecheck 7 | -------------------------------------------------------------------------------- /projects/demo/src/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/favicon-16x16.png -------------------------------------------------------------------------------- /projects/demo/src/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/favicon-32x32.png -------------------------------------------------------------------------------- /projects/demo/src/assets/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/mstile-70x70.png -------------------------------------------------------------------------------- /projects/demo/src/assets/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/mstile-144x144.png -------------------------------------------------------------------------------- /projects/demo/src/assets/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/mstile-150x150.png -------------------------------------------------------------------------------- /projects/demo/src/assets/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/mstile-310x150.png -------------------------------------------------------------------------------- /projects/demo/src/assets/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/mstile-310x310.png -------------------------------------------------------------------------------- /projects/demo/src/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /projects/demo/src/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /projects/demo/src/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-web-apis/intersection-observer/HEAD/projects/demo/src/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /projects/demo/tsconfig.demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "typeRoots": [], 6 | "paths": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/intersection-observer/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/intersection-observer", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/tokens/intersection-root.ts: -------------------------------------------------------------------------------- 1 | import {ElementRef, InjectionToken} from '@angular/core'; 2 | 3 | export const INTERSECTION_ROOT = new InjectionToken>( 4 | 'Root element for IntersectionObserver', 5 | ); 6 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "angularCompilerOptions": { 7 | "entryModule": "src/app/app.server.module#AppServerModule" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts"], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/utils/root-margin-factory.ts: -------------------------------------------------------------------------------- 1 | import {INTERSECTION_ROOT_MARGIN_DEFAULT} from '../tokens/intersection-root-margin'; 2 | 3 | export function rootMarginFactory(rootMargin: string | null): string { 4 | return rootMargin || INTERSECTION_ROOT_MARGIN_DEFAULT; 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /projects/intersection-observer/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts"], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/utils/threshold-factory.ts: -------------------------------------------------------------------------------- 1 | import {INTERSECTION_THRESHOLD_DEFAULT} from '../tokens/intersection-threshold'; 2 | 3 | export function thresholdFactory(threshold: string | null): number | number[] { 4 | return threshold?.split(',').map(parseFloat) || INTERSECTION_THRESHOLD_DEFAULT; 5 | } 6 | -------------------------------------------------------------------------------- /projects/demo/src/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d89ef 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "baseUrl": ".", 6 | "strict": false, 7 | "incremental": true 8 | }, 9 | "include": ["projects", "scripts"], 10 | "exclude": ["**/node_modules", "**/schematics/**", "**/.*/", "*.js"] 11 | } 12 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {ServerModule} from '@angular/platform-server'; 3 | 4 | import {AppBrowserModule} from './app.browser.module'; 5 | import {AppComponent} from './app.component'; 6 | 7 | @NgModule({ 8 | imports: [AppBrowserModule, ServerModule], 9 | bootstrap: [AppComponent], 10 | }) 11 | export class AppServerModule {} 12 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/tokens/intersection-threshold.ts: -------------------------------------------------------------------------------- 1 | import {InjectionToken} from '@angular/core'; 2 | 3 | export const INTERSECTION_THRESHOLD_DEFAULT = 0; 4 | export const INTERSECTION_THRESHOLD = new InjectionToken( 5 | 'threshold for IntersectionObserver', 6 | { 7 | providedIn: 'root', 8 | factory: () => INTERSECTION_THRESHOLD_DEFAULT, 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/tokens/support.ts: -------------------------------------------------------------------------------- 1 | import {inject, InjectionToken} from '@angular/core'; 2 | import {WINDOW} from '@ng-web-apis/common'; 3 | 4 | export const INTERSECTION_OBSERVER_SUPPORT = new InjectionToken( 5 | 'Intersection Observer API support', 6 | { 7 | providedIn: 'root', 8 | factory: () => !!(inject(WINDOW) as any).IntersectionObserver, 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/tokens/tests/support.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | import {INTERSECTION_OBSERVER_SUPPORT} from '../support'; 3 | 4 | describe('INTERSECTION_OBSERVER_SUPPORT', () => { 5 | it('true in modern browsers', () => { 6 | TestBed.configureTestingModule({}); 7 | 8 | expect(TestBed.get(INTERSECTION_OBSERVER_SUPPORT)).toBe(true); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/tokens/intersection-root-margin.ts: -------------------------------------------------------------------------------- 1 | import {InjectionToken} from '@angular/core'; 2 | 3 | export const INTERSECTION_ROOT_MARGIN_DEFAULT = '0px 0px 0px 0px'; 4 | export const INTERSECTION_ROOT_MARGIN = new InjectionToken( 5 | 'rootMargin for IntersectionObserver', 6 | { 7 | providedIn: 'root', 8 | factory: () => INTERSECTION_ROOT_MARGIN_DEFAULT, 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | import {AppComponent} from './app.component'; 4 | 5 | export const appRoutes = [ 6 | { 7 | path: '**', 8 | component: AppComponent, 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(appRoutes)], 14 | exports: [RouterModule], 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/directives/intersection-root.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef} from '@angular/core'; 2 | import {INTERSECTION_ROOT} from '../tokens/intersection-root'; 3 | 4 | @Directive({ 5 | selector: '[waIntersectionRoot]', 6 | providers: [ 7 | { 8 | provide: INTERSECTION_ROOT, 9 | useExisting: ElementRef, 10 | }, 11 | ], 12 | }) 13 | export class IntersectionRootDirective {} 14 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | const base = require('@tinkoff/prettier-config/angular'); 2 | 3 | module.exports = { 4 | ...base, 5 | singleAttributePerLine: true, 6 | overrides: [ 7 | ...base.overrides, 8 | { 9 | files: ['*.js', '*.ts'], 10 | options: {printWidth: 90, parser: 'typescript'}, 11 | }, 12 | { 13 | files: ['*.html'], 14 | options: {printWidth: 120, parser: 'angular'}, 15 | }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
8 |

13 | I'm being observed 14 |

15 |
16 | 17 | Your browser does not support Intersection Observer API 18 | 19 | -------------------------------------------------------------------------------- /projects/demo/src/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/intersection-observer/assets/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/intersection-observer/assets/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /projects/demo/src/main.browser.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | 3 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 4 | import {AppBrowserModule} from './app/app.browser.module'; 5 | 6 | platformBrowserDynamic() 7 | .bootstrapModule(AppBrowserModule) 8 | .then(ref => { 9 | const windowRef: any = window; 10 | 11 | // Ensure Angular destroys itself on hot reloads for Stackblitz 12 | if (windowRef['ngRef']) { 13 | windowRef['ngRef'].destroy(); 14 | } 15 | 16 | windowRef['ngRef'] = ref; 17 | }) 18 | .catch(err => console.error(err)); 19 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/directives/intersection-observee.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, Inject} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | 4 | import {IntersectionObserveeService} from '../services/intersection-observee.service'; 5 | 6 | @Directive({ 7 | selector: '[waIntersectionObservee]', 8 | outputs: ['waIntersectionObservee'], 9 | providers: [IntersectionObserveeService], 10 | }) 11 | export class IntersectionObserveeDirective { 12 | constructor( 13 | @Inject(IntersectionObserveeService) 14 | readonly waIntersectionObservee: Observable, 15 | ) {} 16 | } 17 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, Inject} from '@angular/core'; 2 | import {INTERSECTION_OBSERVER_SUPPORT} from '@ng-web-apis/intersection-observer'; 3 | 4 | @Component({ 5 | selector: 'main', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.less'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | }) 10 | export class AppComponent { 11 | ratio = 0; 12 | 13 | constructor(@Inject(INTERSECTION_OBSERVER_SUPPORT) readonly support: boolean) {} 14 | 15 | onIntersection(intersections: IntersectionObserverEntry[]) { 16 | this.ratio = Math.round(intersections[0].intersectionRatio * 10); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | 2 | # ================================================================================== 3 | # ================================================================================== 4 | # @ng-web-apis/intersection-observer codeowners 5 | # ================================================================================== 6 | # ================================================================================== 7 | # 8 | # Configuration of code ownership and review approvals for the @ng-web-apis/intersection-observer repo. 9 | # 10 | # More info: https://help.github.com/articles/about-codeowners/ 11 | # 12 | 13 | * @waterplea @MarsiBarsi 14 | # will be requested for review when someone opens a pull request 15 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {IntersectionObserveeDirective} from './directives/intersection-observee.directive'; 3 | import {IntersectionObserverDirective} from './directives/intersection-observer.directive'; 4 | import {IntersectionRootDirective} from './directives/intersection-root.directive'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | IntersectionObserverDirective, 9 | IntersectionObserveeDirective, 10 | IntersectionRootDirective, 11 | ], 12 | exports: [ 13 | IntersectionObserverDirective, 14 | IntersectionObserveeDirective, 15 | IntersectionRootDirective, 16 | ], 17 | }) 18 | export class IntersectionObserverModule {} 19 | -------------------------------------------------------------------------------- /projects/intersection-observer/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "lib": ["dom", "es2018"] 9 | }, 10 | "angularCompilerOptions": { 11 | "annotateForClosureCompiler": true, 12 | "skipTemplateCodegen": true, 13 | "strictMetadataEmit": true, 14 | "fullTemplateTypeCheck": true, 15 | "strictInjectionParameters": true, 16 | "enableResourceInlining": true, 17 | "enableIvy": true, 18 | "compilationMode": "partial" 19 | }, 20 | "exclude": ["src/test.ts", "**/*.spec.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Public API Surface of @ng-web-apis/intersection-observer 3 | */ 4 | 5 | /* Directives */ 6 | export * from './directives/intersection-observee.directive'; 7 | export * from './directives/intersection-observer.directive'; 8 | export * from './directives/intersection-root.directive'; 9 | 10 | /* Modules */ 11 | export * from './module'; 12 | 13 | /* Services */ 14 | export * from './services/intersection-observee.service'; 15 | export * from './services/intersection-observer.service'; 16 | 17 | /* Tokens */ 18 | export * from './tokens/intersection-root'; 19 | export * from './tokens/intersection-root-margin'; 20 | export * from './tokens/intersection-threshold'; 21 | export * from './tokens/support'; 22 | -------------------------------------------------------------------------------- /projects/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | /out-tsc 5 | # Only exists if Bazel was run 6 | /bazel-out 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # profiling files 12 | chrome-profiler-events.json 13 | speed-measure-plugin.json 14 | 15 | # IDEs and editors 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | 24 | # IDE - VSCode 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | .history/* 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | yarn-error.log 39 | testem.log 40 | /typings 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | labels: '' 6 | assignee: waterplea 7 | --- 8 | 9 | # 🐞 Bug report 10 | 11 | ### Description 12 | 13 | 14 | 15 | ### Reproduction 16 | 17 | 18 | 19 | http://www.stackblitz.com/... 20 | 21 | ### Expected behavior 22 | 23 | 24 | 25 | ### Versions 26 | 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Angular [e.g. 8] 30 | 31 | ### Additional context 32 | 33 | 34 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | import 'zone.js'; 3 | import 'zone.js/testing'; 4 | 5 | import {getTestBed} from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting, 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting(), 17 | ); 18 | 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const DIST_LIB_PATH = 'dist/intersection-observer/'; 4 | const README_PATH = 'README.md'; 5 | const PATH_TO_README = DIST_LIB_PATH + README_PATH; 6 | 7 | copyExtraFiles(); 8 | 9 | function copyExtraFiles() { 10 | if (!fs.existsSync(README_PATH)) { 11 | throw new Error('Requested files do not exit'); 12 | } else { 13 | copyReadmeIntoDistFolder(README_PATH, PATH_TO_README); 14 | } 15 | } 16 | 17 | function copyReadmeIntoDistFolder(srcPath, toPath) { 18 | const fileBody = fs.readFileSync(srcPath).toString(); 19 | const withoutLogos = fileBody 20 | .replace('![ng-web-apis logo](projects/demo/src/assets/logo.svg) ', '') 21 | .replace(' ', ''); 22 | 23 | fs.writeFileSync(toPath, withoutLogos); 24 | } 25 | -------------------------------------------------------------------------------- /scripts/syncVersions.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const glob = require('glob'); 3 | const JSON_INDENTATION_LEVEL = 4; 4 | const {version} = require('../package.json'); 5 | 6 | // Sync libraries package.json versions with main package.json 7 | syncVersions('projects'); 8 | 9 | function syncVersions(root) { 10 | glob(root + '/**/package.json', (_, files) => { 11 | files.forEach(file => { 12 | const packageJson = JSON.parse(fs.readFileSync(file)); 13 | 14 | fs.writeFileSync( 15 | file, 16 | JSON.stringify( 17 | { 18 | ...packageJson, 19 | version, 20 | }, 21 | null, 22 | JSON_INDENTATION_LEVEL, 23 | ), 24 | ); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE]' 5 | labels: '' 6 | assignee: waterplea 7 | --- 8 | 9 | # 🚀 Feature request 10 | 11 | ### Is your feature request related to a problem? 12 | 13 | 14 | I'm always frustrated when... 15 | 16 | ### Describe the solution you'd like 17 | 18 | 19 | 20 | 21 | ### Describe alternatives you've considered 22 | 23 | 24 | 25 | 26 | ### Additional context 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled schematics 2 | schematics/library-starter/*.js 3 | schematics/library-starter/*.js.map 4 | schematics/library-starter/*.d.ts 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | # Only exists if Bazel was run 11 | /bazel-out 12 | 13 | # dependencies 14 | /node_modules 15 | 16 | # profiling files 17 | chrome-profiler-events.json 18 | speed-measure-plugin.json 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | .history/* 36 | 37 | # misc 38 | /.sass-cache 39 | /connect.lock 40 | /coverage 41 | /libpeerconnection.log 42 | npm-debug.log 43 | yarn-error.log 44 | testem.log 45 | /typings 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "strict": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitReturns": true, 16 | "noUnusedParameters": true, 17 | "noUnusedLocals": true, 18 | "target": "es5", 19 | "typeRoots": ["node_modules/@types"], 20 | "lib": ["es2018", "dom"], 21 | "paths": { 22 | "@ng-web-apis/intersection-observer": [ 23 | "projects/intersection-observer/src/public-api" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/intersection-observer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ng-web-apis/intersection-observer", 3 | "version": "3.0.1", 4 | "description": "A library for declarative use of Intersection Observer API with Angular", 5 | "keywords": [ 6 | "angular", 7 | "ng", 8 | "intersection", 9 | "observer" 10 | ], 11 | "homepage": "https://github.com/ng-web-apis/intersection-observer#README", 12 | "bugs": "https://github.com/ng-web-apis/intersection-observer/issues", 13 | "repository": "https://github.com/ng-web-apis/intersection-observer", 14 | "license": "MIT", 15 | "author": { 16 | "name": "Alexander Inkin", 17 | "email": "alexander@inkin.ru" 18 | }, 19 | "contributors": [ 20 | "Roman Sedov <79601794011@ya.ru>" 21 | ], 22 | "peerDependencies": { 23 | "@angular/core": ">=12.0.0", 24 | "@ng-web-apis/common": ">=2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* Base demo styles */ 2 | body, 3 | html { 4 | display: flex; 5 | flex-direction: column; 6 | margin: 0; 7 | height: 100%; 8 | font-family: Roboto, 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 9 | 'Lucida Grande', sans-serif; 10 | color: #444; 11 | } 12 | 13 | header { 14 | display: flex; 15 | width: 100%; 16 | max-width: 800px; 17 | margin: 0 auto; 18 | padding: 40px 10px; 19 | box-sizing: border-box; 20 | border-bottom: 1px solid gainsboro; 21 | } 22 | 23 | main { 24 | flex: 1; 25 | display: flex; 26 | justify-content: center; 27 | padding: 40px 0; 28 | } 29 | 30 | footer { 31 | padding: 16px; 32 | font-size: 12px; 33 | border-top: 1px solid gainsboro; 34 | text-align: center; 35 | } 36 | 37 | a { 38 | color: #1976D2; 39 | text-decoration: none; 40 | } 41 | 42 | .logo { 43 | margin-right: 20px; 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Web APIs CI 2 | 3 | on: push 4 | 5 | jobs: 6 | ci: 7 | # Setup part 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: '12.x' 15 | - name: Cache Node.js modules 16 | uses: actions/cache@v2 17 | with: 18 | path: ~/.npm 19 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 20 | restore-keys: | 21 | ${{ runner.OS }}-node- 22 | ${{ runner.OS }}- 23 | - name: Install dependencies 24 | run: npm ci 25 | # End of setup 26 | - run: | 27 | npm run build 28 | npm run test 29 | npm run lint 30 | - name: Coveralls 31 | uses: coverallsapp/github-action@master 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | path-to-lcov: ./coverage/intersection-observer/lcov.info -------------------------------------------------------------------------------- /projects/intersection-observer/src/services/intersection-observee.service.ts: -------------------------------------------------------------------------------- 1 | import {ElementRef, Inject, Injectable} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | import {share} from 'rxjs/operators'; 4 | import {IntersectionObserverDirective} from '../directives/intersection-observer.directive'; 5 | 6 | @Injectable() 7 | export class IntersectionObserveeService extends Observable { 8 | constructor( 9 | @Inject(ElementRef) {nativeElement}: ElementRef, 10 | @Inject(IntersectionObserverDirective) 11 | observer: IntersectionObserverDirective, 12 | ) { 13 | super(subscriber => { 14 | observer.observe(nativeElement, entries => { 15 | subscriber.next(entries); 16 | }); 17 | 18 | return () => { 19 | observer.unobserve(nativeElement); 20 | }; 21 | }); 22 | 23 | return this.pipe(share()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.browser.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APP_BASE_HREF, 3 | CommonModule, 4 | LocationStrategy, 5 | PathLocationStrategy, 6 | } from '@angular/common'; 7 | import {NgModule} from '@angular/core'; 8 | import {FormsModule} from '@angular/forms'; 9 | import {BrowserModule} from '@angular/platform-browser'; 10 | import {IntersectionObserverModule} from '@ng-web-apis/intersection-observer'; 11 | import {AppComponent} from './app.component'; 12 | import {AppRoutingModule} from './app.routes'; 13 | 14 | @NgModule({ 15 | bootstrap: [AppComponent], 16 | imports: [ 17 | CommonModule, 18 | FormsModule, 19 | BrowserModule.withServerTransition({appId: 'demo'}), 20 | AppRoutingModule, 21 | IntersectionObserverModule, 22 | ], 23 | declarations: [AppComponent], 24 | providers: [ 25 | { 26 | provide: LocationStrategy, 27 | useClass: PathLocationStrategy, 28 | }, 29 | { 30 | provide: APP_BASE_HREF, 31 | useValue: '', 32 | }, 33 | ], 34 | }) 35 | export class AppBrowserModule {} 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] The commit message follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) 6 | - [ ] Tests for the changes have been added (for bug fixes / features) 7 | - [ ] Docs have been added / updated (for bug fixes / features) 8 | 9 | ## PR Type 10 | 11 | What kind of change does this PR introduce? 12 | 13 | 14 | 15 | - [ ] Bugfix 16 | - [ ] Feature 17 | - [ ] Refactoring (no functional changes, no api changes) 18 | - [ ] Other... Please describe: 19 | 20 | ## What is the current behavior? 21 | 22 | 23 | 24 | Issue Number: N/A 25 | 26 | ## What is the new behavior? 27 | 28 | ## Does this PR introduce a breaking change? 29 | 30 | - [ ] Yes 31 | - [ ] No 32 | 33 | 34 | 35 | ## Other information 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexander Inkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/intersection-observer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexander Inkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "3.0.1", 4 | "private": true, 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build" 9 | }, 10 | "dependencies": { 11 | "@angular/common": "12.2.16", 12 | "@angular/compiler": "12.2.16", 13 | "@angular/core": "12.2.16", 14 | "@angular/forms": "12.2.16", 15 | "@angular/platform-browser": "12.2.16", 16 | "@angular/platform-browser-dynamic": "12.2.16", 17 | "@angular/router": "12.2.16", 18 | "@ng-web-apis/common": "latest", 19 | "@ng-web-apis/intersection-observer": "latest", 20 | "core-js": "3.20.3", 21 | "intersection-observer": "^0.12.2", 22 | "rxjs": "7.5.2", 23 | "zone.js": "0.11.4" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "12.2.16", 27 | "@angular-devkit/core": "12.2.16", 28 | "@angular/cli": "12.2.16", 29 | "@angular/compiler-cli": "12.2.16", 30 | "@angular/language-service": "12.2.16", 31 | "@types/node": "18.0.4", 32 | "ts-node": "9.0.0", 33 | "tslint": "6.1.3", 34 | "typescript": "4.3.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > Thank you for considering contributing to our project. Your help if very welcome! 4 | 5 | When contributing, it's better to first discuss the change you wish to make via issue, 6 | email, or any other method with the owners of this repository before making a change. 7 | 8 | All members of our community are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). 9 | Please make sure you are welcoming and friendly in all of our spaces. 10 | 11 | ## Getting started 12 | 13 | In order to make your contribution please make a fork of the repository. After you've pulled 14 | the code, follow these steps to kick start the development: 15 | 16 | 1. Run `npm ci` to install dependencies 17 | 2. Run `npm start` to launch demo project where you could test your changes 18 | 3. Use following commands to ensure code quality 19 | 20 | ``` 21 | npm run lint 22 | npm run build 23 | npm run test 24 | ``` 25 | 26 | ## Pull Request Process 27 | 28 | 1. We follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) 29 | in our commit messages, i.e. `feat(core): improve typing` 30 | 2. Update [README.md](README.md) to reflect changes related to public API and everything relevant 31 | 3. Make sure you cover all code changes with unit tests 32 | 4. When you are ready, create Pull Request of your fork into original repository 33 | -------------------------------------------------------------------------------- /projects/demo/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/demo'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['ChromeHeadless'], 29 | singleRun: true, 30 | customLaunchers: { 31 | ChromeHeadless: { 32 | base: 'Chrome', 33 | flags: [ 34 | '--no-sandbox', 35 | '--headless', 36 | '--disable-gpu', 37 | '--remote-debugging-port=9222', 38 | ], 39 | }, 40 | }, 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.less: -------------------------------------------------------------------------------- 1 | :host { 2 | perspective: 150vw; 3 | user-select: none; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .wrapper { 9 | position: relative; 10 | height: 200px; 11 | width: 80vw; 12 | overflow: auto; 13 | box-shadow: 0 12px 36px rgba(0, 0, 0, 0.2); 14 | 15 | &:before { 16 | content: ''; 17 | display: block; 18 | height: 900px; 19 | } 20 | } 21 | 22 | .element { 23 | position: absolute; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | margin: 0; 28 | top: 300px; 29 | left: 10vw; 30 | width: 60vw; 31 | height: 200px; 32 | transition: background 0.1s; 33 | 34 | &[data-ratio='0'] { 35 | background: #8591eb; 36 | } 37 | 38 | &[data-ratio='1'] { 39 | background: #85a0eb; 40 | } 41 | 42 | &[data-ratio='2'] { 43 | background: #84aeeb; 44 | } 45 | 46 | &[data-ratio='3'] { 47 | background: #83beeb; 48 | } 49 | 50 | &[data-ratio='4'] { 51 | background: #86d2eb; 52 | } 53 | 54 | &[data-ratio='5'] { 55 | background: #87ddeb; 56 | } 57 | 58 | &[data-ratio='6'] { 59 | background: #8ae5eb; 60 | } 61 | 62 | &[data-ratio='7'] { 63 | background: #8bebdf; 64 | } 65 | 66 | &[data-ratio='8'] { 67 | background: #83ebc8; 68 | } 69 | 70 | &[data-ratio='9'] { 71 | background: #6beb99; 72 | } 73 | 74 | &[data-ratio='10'] { 75 | background: #4ceb60; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/services/tests/intersection-observer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {take} from 'rxjs/operators'; 2 | import {IntersectionObserverService} from '../intersection-observer.service'; 3 | 4 | describe('IntersectionObserverService', () => { 5 | it('works', done => { 6 | let called = false; 7 | 8 | const nativeElement = document.createElement('div'); 9 | const service = new IntersectionObserverService( 10 | { 11 | nativeElement, 12 | }, 13 | true, 14 | '0px 0px 0px 0px', 15 | 0, 16 | { 17 | nativeElement: document.body, 18 | }, 19 | ); 20 | 21 | service.pipe(take(1)).subscribe({ 22 | next: () => { 23 | called = true; 24 | }, 25 | }); 26 | 27 | document.body.appendChild(nativeElement); 28 | 29 | setTimeout(() => { 30 | expect(called).toBe(true); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('throws when not supported', () => { 36 | let error = false; 37 | const service = new IntersectionObserverService( 38 | { 39 | nativeElement: document.createElement('DIV'), 40 | }, 41 | false, 42 | '0px 0px 0px 0px', 43 | 0, 44 | null, 45 | ); 46 | 47 | service.subscribe({ 48 | error: () => { 49 | error = true; 50 | }, 51 | }); 52 | 53 | expect(error).toBe(true); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /projects/intersection-observer/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/intersection-observer'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['ChromeHeadless'], 29 | singleRun: true, 30 | customLaunchers: { 31 | ChromeHeadless: { 32 | base: 'Chrome', 33 | flags: [ 34 | '--no-sandbox', 35 | '--headless', 36 | '--disable-gpu', 37 | '--disable-web-security', 38 | '--remote-debugging-port=9222', 39 | ], 40 | }, 41 | }, 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/services/intersection-observer.service.ts: -------------------------------------------------------------------------------- 1 | import {ElementRef, Inject, Injectable, Optional} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | import {share} from 'rxjs/operators'; 4 | import {INTERSECTION_ROOT} from '../tokens/intersection-root'; 5 | import {INTERSECTION_ROOT_MARGIN} from '../tokens/intersection-root-margin'; 6 | import {INTERSECTION_THRESHOLD} from '../tokens/intersection-threshold'; 7 | import {INTERSECTION_OBSERVER_SUPPORT} from '../tokens/support'; 8 | 9 | @Injectable() 10 | export class IntersectionObserverService extends Observable { 11 | constructor( 12 | @Inject(ElementRef) {nativeElement}: ElementRef, 13 | @Inject(INTERSECTION_OBSERVER_SUPPORT) support: boolean, 14 | @Inject(INTERSECTION_ROOT_MARGIN) rootMargin: string, 15 | @Inject(INTERSECTION_THRESHOLD) threshold: number | number[], 16 | @Optional() @Inject(INTERSECTION_ROOT) root: ElementRef | null, 17 | ) { 18 | super(subscriber => { 19 | if (!support) { 20 | subscriber.error('IntersectionObserver is not supported in your browser'); 21 | 22 | return; 23 | } 24 | 25 | const observer = new IntersectionObserver( 26 | entries => { 27 | subscriber.next(entries); 28 | }, 29 | { 30 | root: root && root.nativeElement, 31 | rootMargin, 32 | threshold, 33 | }, 34 | ); 35 | 36 | observer.observe(nativeElement); 37 | 38 | return () => { 39 | observer.disconnect(); 40 | }; 41 | }); 42 | 43 | return this.pipe(share()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/directives/intersection-observer.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Attribute, 3 | Directive, 4 | ElementRef, 5 | Inject, 6 | OnDestroy, 7 | Optional, 8 | } from '@angular/core'; 9 | import {INTERSECTION_ROOT} from '../tokens/intersection-root'; 10 | import {rootMarginFactory} from '../utils/root-margin-factory'; 11 | import {thresholdFactory} from '../utils/threshold-factory'; 12 | 13 | @Directive({ 14 | selector: '[waIntersectionObserver]', 15 | exportAs: 'IntersectionObserver', 16 | }) 17 | export class IntersectionObserverDirective extends IntersectionObserver 18 | implements OnDestroy { 19 | private readonly callbacks = new Map(); 20 | 21 | constructor( 22 | @Optional() @Inject(INTERSECTION_ROOT) root: ElementRef | null, 23 | @Attribute('waIntersectionRootMargin') rootMargin: string | null, 24 | @Attribute('waIntersectionThreshold') threshold: string | null, 25 | ) { 26 | super( 27 | entries => { 28 | this.callbacks.forEach((callback, element) => { 29 | const filtered = entries.filter(({target}) => target === element); 30 | 31 | return filtered.length && callback(filtered, this); 32 | }); 33 | }, 34 | { 35 | root: root && root.nativeElement, 36 | rootMargin: rootMarginFactory(rootMargin), 37 | threshold: thresholdFactory(threshold), 38 | }, 39 | ); 40 | } 41 | 42 | observe(target: Element, callback: IntersectionObserverCallback = () => {}) { 43 | super.observe(target); 44 | this.callbacks.set(target, callback); 45 | } 46 | 47 | unobserve(target: Element) { 48 | super.unobserve(target); 49 | this.callbacks.delete(target); 50 | } 51 | 52 | ngOnDestroy() { 53 | this.disconnect(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('eslint').Linter.Config} 3 | */ 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | // TODO: warning No cached ProjectGraph is available. The rule will be skipped. @nrwl/nx/enforce-module-boundaries 8 | // If you encounter this error as part of running standard `nx` commands then please open an issue on 9 | // https://github.com/nrwl/nx 10 | // './scripts/eslint/nx.js', 11 | '@tinkoff/eslint-config-angular', 12 | '@tinkoff/eslint-config-angular/html', 13 | '@tinkoff/eslint-config-angular/imports', 14 | '@tinkoff/eslint-config-angular/line-statements', 15 | '@tinkoff/eslint-config-angular/member-ordering', 16 | ], 17 | ignorePatterns: ['projects/**/test.ts', '*.js', '*.json', '*.less', '*.md'], 18 | parserOptions: { 19 | ecmaVersion: 'latest', 20 | sourceType: 'module', 21 | project: [require.resolve('./tsconfig.eslint.json')], 22 | }, 23 | parser: '@typescript-eslint/parser', 24 | rules: { 25 | 'dot-notation': 'off', 26 | '@typescript-eslint/dot-notation': [ 27 | 'error', 28 | { 29 | allowPrivateClassPropertyAccess: true, 30 | allowProtectedClassPropertyAccess: true, 31 | allowIndexSignaturePropertyAccess: true, 32 | }, 33 | ], 34 | '@typescript-eslint/no-useless-constructor': 'off', 35 | 'no-prototype-builtins': 'off', 36 | '@typescript-eslint/no-unnecessary-type-constraint': 'error', 37 | '@typescript-eslint/prefer-includes': 'error', 38 | 'prefer-template': 'error', 39 | '@typescript-eslint/explicit-function-return-type': [ 40 | 'error', 41 | { 42 | allowExpressions: true, 43 | allowTypedFunctionExpressions: true, 44 | allowHigherOrderFunctions: true, 45 | allowDirectConstAssertionInArrowFunctions: true, 46 | allowConciseArrowFunctionExpressionsStartingWithVoid: true, 47 | }, 48 | ], 49 | '@typescript-eslint/no-base-to-string': 'error', 50 | '@typescript-eslint/ban-types': 'error', 51 | '@typescript-eslint/no-for-in-array': 'error', 52 | '@typescript-eslint/prefer-nullish-coalescing': 'error', 53 | '@typescript-eslint/prefer-optional-chain': 'error', 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /projects/demo/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/demo", 17 | "index": "src/index.html", 18 | "main": "src/main.browser.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.demo.json", 21 | "aot": false, 22 | "assets": [ 23 | { 24 | "glob": "**/*", 25 | "input": "projects/demo/src/assets/", 26 | "output": "./assets/" 27 | }, 28 | "src/favicon.ico" 29 | ], 30 | "styles": ["src/styles.css"], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "optimization": true, 36 | "outputHashing": "all", 37 | "sourceMap": false, 38 | "extractCss": true, 39 | "namedChunks": false, 40 | "aot": true, 41 | "extractLicenses": true, 42 | "vendorChunk": false, 43 | "buildOptimizer": true 44 | } 45 | } 46 | }, 47 | "serve": { 48 | "builder": "@angular-devkit/build-angular:dev-server", 49 | "options": { 50 | "browserTarget": "demo:build" 51 | }, 52 | "configurations": { 53 | "production": { 54 | "browserTarget": "demo:build:production" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "defaultProject": "demo" 62 | } 63 | -------------------------------------------------------------------------------- /projects/demo/server.ts: -------------------------------------------------------------------------------- 1 | import '@ng-web-apis/universal/mocks'; 2 | import 'zone.js/node'; 3 | 4 | import {APP_BASE_HREF} from '@angular/common'; 5 | import {provideLocation, provideUserAgent} from '@ng-web-apis/universal'; 6 | import {ngExpressEngine} from '@nguniversal/express-engine'; 7 | import * as express from 'express'; 8 | import {existsSync} from 'fs'; 9 | import {join} from 'path'; 10 | 11 | import {AppServerModule} from './src/main.server'; 12 | 13 | // The Express app is exported so that it can be used by serverless Functions. 14 | export function app(): express.Express { 15 | const server = express(); 16 | const distFolder = join(process.cwd(), 'dist/demo/browser'); 17 | const indexHtml = existsSync(join(distFolder, 'index.original.html')) 18 | ? 'index.original.html' 19 | : 'index'; 20 | 21 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 22 | server.engine( 23 | 'html', 24 | ngExpressEngine({ 25 | bootstrap: AppServerModule, 26 | }), 27 | ); 28 | 29 | server.set('view engine', 'html'); 30 | server.set('views', distFolder); 31 | 32 | // Example Express Rest API endpoints 33 | // server.get('/api/**', (req, res) => { }); 34 | // Serve static files from /browser 35 | server.get( 36 | '*.*', 37 | express.static(distFolder, { 38 | maxAge: '1y', 39 | }), 40 | ); 41 | 42 | // All regular routes use the Universal engine 43 | server.get('*', (req, res) => { 44 | res.render(indexHtml, { 45 | req, 46 | providers: [ 47 | {provide: APP_BASE_HREF, useValue: req.baseUrl}, 48 | provideLocation(req), 49 | provideUserAgent(req), 50 | ], 51 | }); 52 | }); 53 | 54 | return server; 55 | } 56 | 57 | function run(): void { 58 | const port = process.env.PORT || 4000; 59 | const server = app(); 60 | 61 | server.listen(port, () => { 62 | console.info(`Node Express server listening on http://localhost:${port}`); 63 | }); 64 | } 65 | 66 | // Webpack will replace 'require' with '__webpack_require__' 67 | // '__non_webpack_require__' is a proxy to Node 'require' 68 | // The below code is to ensure that the server is run only when not requiring the bundle. 69 | declare const __non_webpack_require__: NodeRequire; 70 | const mainModule = __non_webpack_require__.main; 71 | const moduleFilename = mainModule?.filename || ''; 72 | 73 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { 74 | run(); 75 | } 76 | 77 | export * from './src/main.server'; 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See 4 | [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 5 | 6 | ### [3.0.1](https://github.com/ng-web-apis/intersection-observer/compare/v3.0.0...v3.0.1) (2023-05-25) 7 | 8 | - add missing README 9 | 10 | ## [3.0.0](https://github.com/ng-web-apis/intersection-observer/compare/v2.1.0...v3.0.0) (2022-07-15) 11 | 12 | ### ⚠ BREAKING CHANGES 13 | 14 | - update to Angular 12 and Ivy distribution 15 | ([90e166b](https://github.com/ng-web-apis/intersection-observer/commit/90e166b7404f2e6edac8713dfbb56cd344e861f7)) 16 | 17 | ## [2.1.0](https://github.com/ng-web-apis/intersection-observer/compare/v2.0.1...v2.1.0) (2020-09-19) 18 | 19 | ### Features 20 | 21 | - **directive:** add `exportAs` ([8341df3](https://github.com/ng-web-apis/intersection-observer/commit/8341df3)) 22 | 23 | ### [2.0.1](https://github.com/ng-web-apis/intersection-observer/compare/v2.0.0...v2.0.1) (2020-08-26) 24 | 25 | ### Bug Fixes 26 | 27 | - **service:** fix exception in Internet Explorer 28 | ([5f28df4](https://github.com/ng-web-apis/intersection-observer/commit/5f28df4)) 29 | 30 | ## [2.0.0](https://github.com/ng-web-apis/intersection-observer/compare/v1.1.3...v2.0.0) (2020-05-15) 31 | 32 | ### Features 33 | 34 | - **observer:** Add new directives and service to create single observer and observe multiple elements, rename old 35 | directive ([c2de2ff](https://github.com/ng-web-apis/intersection-observer/commit/c2de2ff)) 36 | 37 | BREAKING CHANGE: New directives names and mechanics 38 | 39 | ### [1.1.3](https://github.com/ng-web-apis/intersection-observer/compare/v1.1.2...v1.1.3) (2020-04-24) 40 | 41 | - add default values for tokens and remove @Optional 42 | 43 | ### [1.1.2](https://github.com/ng-web-apis/intersection-observer/compare/v1.1.1...v1.1.2) (2020-04-06) 44 | 45 | ### Bug Fixes 46 | 47 | - **directive:** add attributes to constructor to make code completion work in IDE 48 | ([ec3c25a](https://github.com/ng-web-apis/intersection-observer/commit/ec3c25a)) 49 | 50 | ### [1.1.1](https://github.com/ng-web-apis/intersection-observer/compare/v1.1.0...v1.1.1) (2020-03-30) 51 | 52 | ### Bug Fixes 53 | 54 | - **service:** only observe when there are subscriptions 55 | ([2a8e267](https://github.com/ng-web-apis/intersection-observer/commit/2a8e267)) 56 | 57 | ## [1.1.0](https://github.com/ng-web-apis/intersection-observer/compare/v1.0.0...v1.1.0) (2020-03-26) 58 | 59 | ### Features 60 | 61 | - **service:** add `IntersectionObserverService` 62 | ([65e79aa](https://github.com/ng-web-apis/intersection-observer/commit/65e79aa)) 63 | 64 | ## 1.0.0 (2020-03-25) 65 | -------------------------------------------------------------------------------- /projects/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Intersection Observer API for Angular 4 | 9 | 15 | 21 | 22 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 |
38 | 46 |
47 |

Intersection Observer API for Angular

48 | Part of 49 | 50 | Web APIs logo 58 | Web APIs for Angular 59 | 60 |
61 |
62 |
loading
63 |
64 | Get it here: 65 | GitHub | 66 | NPM 69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /projects/demo/src/assets/web-api.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | background 6 | 7 | 8 | 9 | 10 | 11 | 12 | Layer 1 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /projects/intersection-observer/src/directives/tests/intersection-observee.spec.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewChild} from '@angular/core'; 2 | import {ComponentFixture, TestBed} from '@angular/core/testing'; 3 | import {IntersectionObserverModule} from '../../module'; 4 | import {INTERSECTION_ROOT_MARGIN} from '../../tokens/intersection-root-margin'; 5 | import {INTERSECTION_THRESHOLD} from '../../tokens/intersection-threshold'; 6 | import {IntersectionObserverDirective} from '../intersection-observer.directive'; 7 | 8 | describe('IntersectionObserveeDirective', () => { 9 | @Component({ 10 | template: ` 11 |
Hello
12 |
21 |
Height expander
22 |

26 | I'm being observed 27 |

28 |

33 | Default values 34 |

35 |
36 | `, 37 | }) 38 | class TestComponent { 39 | @ViewChild('root', {read: IntersectionObserverDirective}) 40 | observer!: IntersectionObserverDirective; 41 | 42 | onIntersection = jasmine.createSpy('onIntersection'); 43 | observe = true; 44 | } 45 | 46 | let fixture: ComponentFixture; 47 | let testComponent: TestComponent; 48 | 49 | beforeEach(() => { 50 | TestBed.configureTestingModule({ 51 | imports: [IntersectionObserverModule], 52 | declarations: [TestComponent], 53 | }); 54 | 55 | fixture = TestBed.createComponent(TestComponent); 56 | testComponent = fixture.componentInstance; 57 | fixture.detectChanges(); 58 | testComponent.onIntersection.calls.reset(); 59 | }); 60 | 61 | it('Emits intersections', done => { 62 | document.querySelector('#observer_root')!.scrollTop = 350; 63 | fixture.detectChanges(); 64 | 65 | setTimeout(() => { 66 | expect(testComponent.onIntersection).toHaveBeenCalled(); 67 | document.querySelector('#observer_root')!.scrollTop = 0; 68 | fixture.detectChanges(); 69 | testComponent.observe = false; 70 | fixture.detectChanges(); 71 | done(); 72 | }, 100); 73 | }); 74 | 75 | it('Compatible with native method signature', () => { 76 | expect(() => 77 | testComponent.observer.observe(document.querySelector('#manual_observee')!), 78 | ).not.toThrow(); 79 | }); 80 | 81 | it('Default options', () => { 82 | // https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver 83 | expect(TestBed.get(INTERSECTION_ROOT_MARGIN)).toBe('0px 0px 0px 0px'); 84 | expect(TestBed.get(INTERSECTION_THRESHOLD)).toBe(0); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /projects/demo/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 17 | 18 | 19 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project at ng.web.apis@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /projects/demo/src/assets/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ng-web-apis/intersection-observer", 3 | "version": "3.0.1", 4 | "description": "A library for declarative use of Intersection Observer API with Angular", 5 | "keywords": [ 6 | "angular", 7 | "ng", 8 | "intersection", 9 | "observer" 10 | ], 11 | "homepage": "https://github.com/ng-web-apis/intersection-observer#README", 12 | "bugs": "https://github.com/ng-web-apis/intersection-observer/issues", 13 | "repository": "https://github.com/ng-web-apis/intersection-observer", 14 | "license": "MIT", 15 | "author": { 16 | "name": "Alexander Inkin", 17 | "email": "alexander@inkin.ru" 18 | }, 19 | "contributors": [ 20 | "Roman Sedov <79601794011@ya.ru>" 21 | ], 22 | "scripts": { 23 | "postinstall": "husky install", 24 | "ng": "ng", 25 | "start": "ng serve", 26 | "start:ssr": "ng run demo:serve-ssr", 27 | "serve:ssr": "node dist/demo/server/main.js", 28 | "build:ssr": "ng build && ng run demo:server", 29 | "prerender": "ng run demo:prerender", 30 | "build": "ng build", 31 | "postbuild": "node scripts/postbuild.js", 32 | "test": "ng test", 33 | "stylelint": "stylelint '**/*.{less,css}'", 34 | "lint": "eslint --cache --cache-location node_modules/.cache/eslint", 35 | "typecheck": "tsc --noEmit --skipLibCheck", 36 | "release": "standard-version", 37 | "release:patch": "npm run release -- --release-as patch", 38 | "release:minor": "npm run release -- --release-as minor", 39 | "release:major": "npm run release -- --release-as major", 40 | "publish": "npm run build && npm publish ./dist/intersection-observer" 41 | }, 42 | "lint-staged": { 43 | "*.{js,ts,html,md,less,json}": [ 44 | "npm run lint -- --fix", 45 | "prettier --write", 46 | "git add" 47 | ], 48 | "*.less": [ 49 | "stylelint --fix", 50 | "git add" 51 | ] 52 | }, 53 | "dependencies": { 54 | "@angular/common": "12.2.16", 55 | "@angular/compiler": "12.2.16", 56 | "@angular/core": "12.2.16", 57 | "@angular/forms": "12.2.16", 58 | "@angular/platform-browser": "12.2.16", 59 | "@angular/platform-browser-dynamic": "12.2.16", 60 | "@angular/platform-server": "12.2.16", 61 | "@angular/router": "12.2.16", 62 | "@ng-web-apis/common": "^2.0.0", 63 | "@ng-web-apis/universal": "^2.0.0", 64 | "@nguniversal/express-engine": "12.1.3", 65 | "core-js": "3.20.3", 66 | "intersection-observer": "^0.12.2", 67 | "rxjs": "7.5.2", 68 | "tslib": "2.3.1", 69 | "zone.js": "0.11.4" 70 | }, 71 | "devDependencies": { 72 | "@angular-devkit/build-angular": "12.2.16", 73 | "@angular-devkit/core": "12.2.16", 74 | "@angular/cli": "12.2.16", 75 | "@angular/compiler-cli": "12.2.16", 76 | "@angular/language-service": "12.2.16", 77 | "@commitlint/cli": "^11.0.0", 78 | "@commitlint/config-conventional": "^11.0.0", 79 | "@nguniversal/builders": "12.1.3", 80 | "@tinkoff/eslint-config": "1.36.1", 81 | "@tinkoff/eslint-config-angular": "1.36.1", 82 | "@tinkoff/prettier-config": "1.32.1", 83 | "@types/estree": "1.0.0", 84 | "@types/express": "4.17.13", 85 | "@types/jasmine": "3.10.3", 86 | "@types/jasminewd2": "2.0.10", 87 | "@types/node": "18.0.4", 88 | "coveralls": "3.1.1", 89 | "husky": "7.0.4", 90 | "jasmine-core": "4.0.0", 91 | "jasmine-spec-reporter": "7.0.0", 92 | "karma": "6.3.11", 93 | "karma-chrome-launcher": "3.1.0", 94 | "karma-coverage-istanbul-reporter": "3.0.3", 95 | "karma-jasmine": "4.0.1", 96 | "karma-jasmine-html-reporter": "1.7.0", 97 | "lint-staged": "12.2.1", 98 | "ng-packagr": "12.2.6", 99 | "prettier": "2.5.1", 100 | "standard-version": "9.3.2", 101 | "ts-node": "9.0.0", 102 | "tslint": "6.1.3", 103 | "typescript": "4.3.5" 104 | }, 105 | "engines": { 106 | "node": ">= 10", 107 | "npm": ">= 3" 108 | }, 109 | "standard-version": { 110 | "scripts": { 111 | "postbump": "node scripts/syncVersions.js && git add **/package.json" 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "projects/demo", 10 | "sourceRoot": "projects/demo/src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "baseHref": "/intersection-observer/", 17 | "deployUrl": "/intersection-observer/", 18 | "outputPath": "dist/demo/browser", 19 | "index": "projects/demo/src/index.html", 20 | "main": "projects/demo/src/main.browser.ts", 21 | "polyfills": "projects/demo/src/polyfills.ts", 22 | "tsConfig": "tsconfig.json", 23 | "assets": [ 24 | { 25 | "glob": "**/*", 26 | "input": "projects/demo/src/assets/", 27 | "output": "./assets/" 28 | }, 29 | "projects/demo/src/favicon.ico" 30 | ], 31 | "styles": ["projects/demo/src/styles.css"], 32 | "showCircularDependencies": false, 33 | "vendorChunk": true, 34 | "extractLicenses": false, 35 | "buildOptimizer": false, 36 | "sourceMap": true, 37 | "optimization": false, 38 | "namedChunks": true, 39 | "scripts": [] 40 | }, 41 | "configurations": { 42 | "production": { 43 | "baseHref": "/intersection-observer/", 44 | "deployUrl": "/intersection-observer/", 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "namedChunks": false, 49 | "buildOptimizer": true, 50 | "statsJson": false, 51 | "progress": false, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | } 58 | ] 59 | }, 60 | "development": { 61 | "baseHref": "/", 62 | "deployUrl": "/" 63 | } 64 | }, 65 | "defaultConfiguration": "production" 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "options": { 70 | "browserTarget": "demo:build" 71 | }, 72 | "configurations": { 73 | "production": { 74 | "browserTarget": "demo:build:production" 75 | } 76 | } 77 | }, 78 | "server": { 79 | "builder": "@angular-devkit/build-angular:server", 80 | "options": { 81 | "outputPath": "dist/demo/server", 82 | "main": "projects/demo/server.ts", 83 | "tsConfig": "projects/demo/tsconfig.server.json", 84 | "inlineStyleLanguage": "less" 85 | }, 86 | "configurations": { 87 | "production": { 88 | "outputHashing": "media", 89 | "fileReplacements": [ 90 | { 91 | "replace": "projects/demo/src/environments/environment.ts", 92 | "with": "projects/demo/src/environments/environment.prod.ts" 93 | } 94 | ] 95 | }, 96 | "development": { 97 | "optimization": false, 98 | "sourceMap": true, 99 | "extractLicenses": false 100 | } 101 | }, 102 | "defaultConfiguration": "production" 103 | }, 104 | "serve-ssr": { 105 | "builder": "@nguniversal/builders:ssr-dev-server", 106 | "configurations": { 107 | "development": { 108 | "browserTarget": "demo:build:development", 109 | "serverTarget": "demo:server:development" 110 | }, 111 | "production": { 112 | "browserTarget": "demo:build:production", 113 | "serverTarget": "demo:server:production" 114 | } 115 | }, 116 | "defaultConfiguration": "development" 117 | }, 118 | "prerender": { 119 | "builder": "@nguniversal/builders:prerender", 120 | "options": { 121 | "routes": ["/"] 122 | }, 123 | "configurations": { 124 | "production": { 125 | "browserTarget": "demo:build:production", 126 | "serverTarget": "demo:server:production" 127 | }, 128 | "development": { 129 | "browserTarget": "demo:build:development", 130 | "serverTarget": "demo:server:development" 131 | } 132 | }, 133 | "defaultConfiguration": "production" 134 | } 135 | } 136 | }, 137 | "intersection-observer": { 138 | "projectType": "library", 139 | "root": "projects/intersection-observer", 140 | "sourceRoot": "projects/intersection-observer/src", 141 | "architect": { 142 | "build": { 143 | "builder": "@angular-devkit/build-angular:ng-packagr", 144 | "options": { 145 | "tsConfig": "projects/intersection-observer/tsconfig.lib.json", 146 | "project": "projects/intersection-observer/ng-package.json" 147 | } 148 | }, 149 | "test": { 150 | "builder": "@angular-devkit/build-angular:karma", 151 | "options": { 152 | "main": "projects/intersection-observer/src/test.ts", 153 | "tsConfig": "projects/intersection-observer/tsconfig.spec.json", 154 | "karmaConfig": "projects/intersection-observer/karma.conf.js", 155 | "codeCoverage": true, 156 | "browsers": "ChromeHeadless" 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "defaultProject": "intersection-observer" 163 | } 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ___ 2 | ___ 3 | **Attention!** This repository is archived and the library has been moved to [tinkoff/ng-web-apis](https://github.com/Tinkoff/ng-web-apis) monorepository 4 | ___ 5 | ___ 6 | # ![ng-web-apis logo](projects/demo/src/assets/logo.svg) Intersection Observer API for Angular 7 | 8 | > Part of [Web APIs for Angular](https://ng-web-apis.github.io/) 9 | 10 | [![npm version](https://img.shields.io/npm/v/@ng-web-apis/intersection-observer.svg)](https://npmjs.com/package/@ng-web-apis/intersection-observer) 11 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@ng-web-apis/intersection-observer)](https://bundlephobia.com/result?p=@ng-web-apis/intersection-observer) 12 | [![.github/workflows/ci.yml](https://github.com/ng-web-apis/intersection-observer/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ng-web-apis/intersection-observer/actions/workflows/ci.yml) 13 | [![Coveralls github](https://img.shields.io/coveralls/github/ng-web-apis/intersection-observer)](https://coveralls.io/github/ng-web-apis/intersection-observer?branch=master) 14 | [![angular-open-source-starter](https://img.shields.io/badge/made%20with-angular--open--source--starter-d81676?logo=angular)](https://github.com/TinkoffCreditSystems/angular-open-source-starter) 15 | 16 | This is a library for declarative use of 17 | [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) 18 | with Angular. 19 | 20 | ## Install 21 | 22 | If you do not have [@ng-web-apis/common](https://github.com/ng-web-apis/common): 23 | 24 | ``` 25 | npm i @ng-web-apis/common 26 | ``` 27 | 28 | Now install the package: 29 | 30 | ``` 31 | npm i @ng-web-apis/intersection-observer 32 | ``` 33 | 34 | ## Usage 35 | 36 | 1. Import `IntersectionObserverModule` for directives to work 37 | 2. Create [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) with `waIntersectionObserver` directive 38 | 3. Observe elements with `waIntersectionObservee` directive 39 | 4. _Optional:_ provide root element with `waIntersectionRoot` directive and 40 | use `waIntersectionThreshold` and `waIntersectionRootMargin` attributes to configure 41 | [IntersectionObserver options](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver) 42 | 43 | > **NOTE:** Keep in mind these are used one time in constructor so you cannot use binding, only strings. Pass comma separated numbers to set an array of thresholds. 44 | 45 | ### Usage with Jest 46 | 47 | DOM environment provided by Jest does not emulate IntersectionObserver API and need to be mocked. You can add the following line to your `setup.ts`: 48 | 49 | ```ts 50 | // setup.ts 51 | import '@ng-web-apis/universal/mocks'; 52 | ``` 53 | 54 | to use mocks from [@ng-web-apis/universal](https://github.com/ng-web-apis/universal) package. 55 | 56 | ## Examples 57 | 58 | Observing multiple elements intersecting with viewport using single observer 59 | 60 | ```html 61 |
62 |
63 | I'm being observed 64 |
65 |
66 | I'm being observed 67 |
68 |
69 | ``` 70 | 71 | Observing elements intersecting with parent element, 72 | each having different configuration therefore using individual observers: 73 | 74 | ```html 75 |
76 |
81 | I'm being observed 82 |
83 |
88 | I'm being observed 89 |
90 |
91 | ``` 92 | 93 | ## Services 94 | 95 | Alternatively you can use `Observable`-based services: 96 | 97 | 1. `IntersectionObserveeService` can be used to observe elements under `waIntersectionObserver` 98 | directive in the DI tree 99 | 100 | 2. `IntersectionObserverService` can be used to observe single element independently. 101 | Provide tokens manually to configure it: 102 | 103 | ```typescript 104 | @Component({ 105 | selector: 'my-component', 106 | providers: [ 107 | IntersectionObserverService, 108 | { 109 | provide: INTERSECTION_THRESHOLD, 110 | useValue: 0.5, 111 | }, 112 | { 113 | provide: INTERSECTION_ROOT_MARGIN, 114 | useValue: '10px', 115 | }, 116 | ], 117 | }) 118 | export class MyComponent { 119 | constructor( 120 | @Inject(IntersectionObserverService) entries$: IntersectionObserverService, 121 | ) { 122 | entries$.subscribe(entries => { 123 | // Don't forget to unsubscribe 124 | console.log(entries); 125 | }); 126 | } 127 | } 128 | ``` 129 | 130 | > In this case provide `INTERSECTION_ROOT` up the DI tree if you 131 | > want to observe intersection with a particular parent element 132 | 133 | ## Browser support 134 | 135 | | [IE / Edge](http://godban.github.io/browsers-support-badges/) | [Firefox](http://godban.github.io/browsers-support-badges/) | [Chrome](http://godban.github.io/browsers-support-badges/) | [Safari](http://godban.github.io/browsers-support-badges/) | 136 | | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 137 | | 15+ | 55+ | 51+ | 12.2+ | 138 | 139 | > You can use [polyfill](https://www.npmjs.com/package/intersection-observer) to support older browsers 140 | 141 | ## Angular Universal 142 | 143 | If you want to use this package with SSR, you need to mock `IntersectionObserver` class on the server. 144 | You can use our Universal package for this, see [this example](https://github.com/ng-web-apis/universal#mocks). 145 | 146 | ## Demo 147 | 148 | You can [try online demo here](https://ng-web-apis.github.io/intersection-observer) 149 | 150 | ## See also 151 | 152 | Other [Web APIs for Angular](https://ng-web-apis.github.io/) by [@ng-web-apis](https://github.com/ng-web-apis) 153 | 154 | ## Open-source 155 | 156 | Do you also want to open-source something, but hate the collateral work? 157 | Check out this [Angular Open-source Library Starter](https://github.com/TinkoffCreditSystems/angular-open-source-starter) 158 | we’ve created for our projects. It got you covered on continuous integration, 159 | pre-commit checks, linting, versioning + changelog, code coverage and all that jazz. 160 | --------------------------------------------------------------------------------