├── .browserslistrc ├── .circleci └── config.yml ├── .codecov.yml ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-footer │ │ ├── app-footer.component.spec.ts │ │ └── app-footer.component.ts │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.module.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── lib │ ├── headroom.component.ts │ ├── headroom.module.ts │ ├── package.json │ ├── public_api.ts │ ├── shouldUpdate.spec.ts │ └── shouldUpdate.ts ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@4 4 | browser-tools: circleci/browser-tools@1 5 | codecov: codecov/codecov@3 6 | jobs: 7 | test: 8 | docker: 9 | - image: cimg/node:current-browsers 10 | environment: 11 | CHROME_BIN: '/usr/bin/google-chrome' 12 | steps: 13 | - browser-tools/install-chrome 14 | - checkout 15 | - node/install-packages 16 | - run: 17 | name: test 18 | command: npm run test:ci 19 | - run: 20 | name: lint 21 | command: npm run lint 22 | - codecov/upload 23 | release: 24 | executor: 25 | name: node/default 26 | tag: 'current' 27 | steps: 28 | - checkout 29 | - node/install-packages 30 | - run: npm run build 31 | - run: cd dist && npx semantic-release 32 | 33 | workflows: 34 | version: 2 35 | test_and_release: 36 | # Run the test jobs first, then the release only when all the test jobs are successful 37 | jobs: 38 | - test 39 | - release: 40 | filters: 41 | branches: 42 | only: 43 | - master 44 | context: 45 | - npm 46 | requires: 47 | - test 48 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "50..100" 3 | status: 4 | project: no 5 | patch: no 6 | comment: 7 | require_changes: yes 8 | behavior: once 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/recommended", 13 | "plugin:@angular-eslint/template/process-inline-templates" 14 | ], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.html"], 19 | "extends": ["plugin:@angular-eslint/template/recommended"], 20 | "rules": {} 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": true, 6 | "printWidth": 100, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Scott Cooper 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

ngx-headroom

3 |
4 | 5 | npm 6 | 7 | 8 | circleci 9 | 10 | 11 | codecov 12 | 13 |
14 |
15 |
16 | 17 | DEMO: https://ngx-headroom.netlify.com/ 18 | 19 | An Angular Component to hide/show your header on scroll. A port of of [React Headroom](https://github.com/KyleAMathews/react-headroom) by KyleAMathews which was based around [headroom.js](https://github.com/WickyNilliams/headroom.js) 20 | 21 | Fixed headers are nice for persistent navigation but they can also get in the way by taking up valuable vertical screen space. Using this component lets you have your persistent navigation while preserving screen space when the navigation is not needed. 22 | 23 | ## Dependencies 24 | 25 | Latest version available for each version of Angular 26 | 27 | | ngx-headroom | Angular | 28 | | ------------ | --------- | 29 | | 2.3.2 | 6.x 7.x | 30 | | 3.0.3 | 8.x 9.x | 31 | | 4.0.0 | 10.x 11.x | 32 | | current | >= 12.x | 33 | 34 | ## Install 35 | 36 | ```sh 37 | npm install @ctrl/ngx-headroom 38 | ``` 39 | 40 | ## Using Angular Headroom 41 | 42 | Import the module. Requires `@angular/animations` 43 | 44 | ```ts 45 | // requires BrowserAnimationsModule 46 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 47 | // import HeadroomModule 48 | import { HeadroomModule } from '@ctrl/ngx-headroom'; 49 | ``` 50 | 51 | Use the module 52 | 53 | ```html 54 | 55 |

You can put anything you'd like inside the Headroom Component

56 |
57 | ``` 58 | 59 | ### Overriding animation 60 | 61 | The component is intended to be plug 'n play meaning it has sensible defaults for animating the header in and out. If you'd like to override the default animation. 62 | 63 | Override animation defaults by passing your own values 64 | 65 | ```html 66 | 67 |

You can put anything you'd like inside the Headroom Component

68 |
69 | ``` 70 | 71 | ### Inputs 72 | 73 | - `duration` — Duration of animation in ms 74 | - `easing` — Easing of animation 75 | - `upTolerance` — scroll tolerance in px when scrolling up before component is pinned 76 | - `downTolerance` — scroll tolerance in px when scrolling down before component is pinned 77 | - `disable` — disable pinning and unpinning 78 | - `wrapperStyle` — pass styles for the wrapper div (this maintains the components vertical space at the top of the page). 79 | - `parent` — provide a custom 'parent' element for scroll events. `parent` should be a function which resolves to the desired element. 80 | - `pinStart` — height in px where the header should start and stop pinning. Useful when you have another element above Headroom component. 81 | 82 | ### Outputs 83 | 84 | - `pin` - emitted when header is pinned 85 | - `unpin` - emitted when header is unpinned 86 | - `unfix` - emitted when header position is no longer fixed 87 | 88 | --- 89 | 90 | > GitHub [@scttcper](https://github.com/scttcper)  ·  91 | > Twitter [@scttcper](https://twitter.com/scttcper) 92 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-headroom": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.scss" 27 | ], 28 | "scripts": [], 29 | "vendorChunk": true, 30 | "extractLicenses": false, 31 | "buildOptimizer": false, 32 | "sourceMap": true, 33 | "optimization": false, 34 | "namedChunks": true 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "namedChunks": false, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true, 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "2mb", 55 | "maximumError": "5mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "6kb", 60 | "maximumError": "10kb" 61 | } 62 | ] 63 | } 64 | }, 65 | "defaultConfiguration": "" 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "options": { 70 | "browserTarget": "ngx-headroom:build" 71 | }, 72 | "configurations": { 73 | "production": { 74 | "browserTarget": "ngx-headroom:build:production" 75 | } 76 | } 77 | }, 78 | "extract-i18n": { 79 | "builder": "@angular-devkit/build-angular:extract-i18n", 80 | "options": { 81 | "browserTarget": "ngx-headroom:build" 82 | } 83 | }, 84 | "test": { 85 | "builder": "@angular-devkit/build-angular:karma", 86 | "options": { 87 | "main": "src/test.ts", 88 | "polyfills": "src/polyfills.ts", 89 | "tsConfig": "tsconfig.spec.json", 90 | "karmaConfig": "karma.conf.js", 91 | "assets": [ 92 | "src/favicon.ico", 93 | "src/assets" 94 | ], 95 | "styles": [ 96 | "src/styles.scss" 97 | ], 98 | "scripts": [] 99 | } 100 | }, 101 | "lint": { 102 | "builder": "@angular-eslint/builder:lint", 103 | "options": { 104 | "lintFilePatterns": [ 105 | "src/**/*.ts", 106 | "src/**/*.html" 107 | ] 108 | } 109 | } 110 | } 111 | } 112 | }, 113 | "defaultProject": "ngx-headroom", 114 | "cli": { 115 | "defaultCollection": "@angular-eslint/schematics" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /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/zzz'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['coverage-istanbul', 'progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | customLaunchers: { 30 | ChromeCI: { 31 | base: `${process.env['TRAVIS'] ? 'ChromeHeadless' : 'Chrome'}`, 32 | flags: process.env['TRAVIS'] ? ['--no-sandbox'] : [], 33 | }, 34 | }, 35 | singleRun: false, 36 | restartOnFileChange: true, 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ctrl/ngx-headroom", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "repository": "typectrl/ngx-headroom", 6 | "homepage": "https://github.com/typectrl/ngx-headroom", 7 | "scripts": { 8 | "ng": "ng", 9 | "start": "ng serve", 10 | "build": "ng-packagr -p src/lib/package.json", 11 | "postbuild": "cpy README.md LICENSE dist", 12 | "test": "ng test --watch=false", 13 | "test:ci": "ng test --watch=false --code-coverage --no-progress", 14 | "lint": "ng lint", 15 | "lint:fix": "ng lint ngx-headroom --fix", 16 | "ghpages": "ng build --configuration production --no-progress" 17 | }, 18 | "private": true, 19 | "devDependencies": { 20 | "@angular-devkit/build-angular": "13.0.3", 21 | "@angular-eslint/builder": "13.0.1", 22 | "@angular-eslint/eslint-plugin": "13.0.1", 23 | "@angular-eslint/eslint-plugin-template": "13.0.1", 24 | "@angular-eslint/schematics": "13.0.1", 25 | "@angular-eslint/template-parser": "13.0.1", 26 | "@angular/animations": "13.0.2", 27 | "@angular/cli": "13.0.3", 28 | "@angular/common": "13.0.2", 29 | "@angular/compiler": "13.0.2", 30 | "@angular/compiler-cli": "13.0.2", 31 | "@angular/core": "13.0.2", 32 | "@angular/forms": "13.0.2", 33 | "@angular/language-service": "13.0.2", 34 | "@angular/platform-browser": "13.0.2", 35 | "@angular/platform-browser-dynamic": "13.0.2", 36 | "@ctrl/ngx-github-buttons": "7.1.0", 37 | "@types/fs-extra": "9.0.13", 38 | "@types/jasmine": "3.10.2", 39 | "@types/node": "16.11.10", 40 | "@typescript-eslint/eslint-plugin": "5.4.0", 41 | "@typescript-eslint/parser": "5.4.0", 42 | "bootstrap": "4.5.3", 43 | "core-js": "3.19.1", 44 | "cpy-cli": "3.1.1", 45 | "eslint": "8.3.0", 46 | "jasmine-core": "3.10.1", 47 | "karma": "6.3.9", 48 | "karma-chrome-launcher": "3.1.0", 49 | "karma-cli": "2.0.0", 50 | "karma-coverage-istanbul-reporter": "3.0.3", 51 | "karma-jasmine": "4.0.1", 52 | "karma-jasmine-html-reporter": "1.7.0", 53 | "karma-mocha-reporter": "2.2.5", 54 | "ng-packagr": "13.0.8", 55 | "rxjs": "7.4.0", 56 | "tslib": "2.3.1", 57 | "typescript": "4.4.4", 58 | "zone.js": "0.11.4" 59 | }, 60 | "engines": { 61 | "npm": ">= 6", 62 | "node": ">= 8" 63 | }, 64 | "release": { 65 | "branch": "master" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/app-footer/app-footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { AppFooterComponent } from './app-footer.component'; 4 | 5 | describe('AppFooterComponent', () => { 6 | let component: AppFooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AppFooterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AppFooterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/app-footer/app-footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, VERSION } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | template: ` 6 | 13 | `, 14 | styles: [ 15 | ` 16 | .footer { 17 | line-height: 2; 18 | text-align: center; 19 | font-size: 69%; 20 | color: #999; 21 | font-family: var(--font-family-monospace); 22 | } 23 | `, 24 | ], 25 | }) 26 | export class AppFooterComponent { 27 | version = VERSION.full; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 32 |
33 |
34 |
35 |

Angular Headroom

36 |

Hide your header until you need it

37 |

38 | A port of 39 | React Headroom 40 | by Kyle Mathews, this is an Angular Component to hide/show your header on scroll. 41 |

42 | 43 |

44 | Fixed headers are nice for persistent navigation but they can also get in the way by taking 45 | up valuable vertical screen space. Using this component lets you have your persistent 46 | navigation while preserving screen space when the navigation is not needed. 47 |

48 |

49 | View Docs 50 |

51 |
52 |
53 |
54 | 55 | 56 | 57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 |
Empty Space
68 |
69 |
70 |
71 |
72 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { NtkmeButtonModule } from '@ctrl/ngx-github-buttons'; 4 | 5 | import { HeadroomModule } from '../lib/public_api'; 6 | import { AppFooterComponent } from './app-footer/app-footer.component'; 7 | import { AppComponent } from './app.component'; 8 | 9 | describe('AppComponent', () => { 10 | beforeEach( 11 | waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [AppComponent, AppFooterComponent], 14 | imports: [NtkmeButtonModule, HeadroomModule], 15 | }).compileComponents(); 16 | }), 17 | ); 18 | 19 | it( 20 | 'should create the app', 21 | waitForAsync(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.debugElement.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }), 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | }) 7 | export class AppComponent { 8 | handlePin() { 9 | console.log('pinned'); 10 | } 11 | handleUnpin() { 12 | console.log('unpinned'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { NtkmeButtonModule } from '@ctrl/ngx-github-buttons'; 6 | 7 | import { HeadroomModule } from '../lib/public_api'; 8 | import { AppFooterComponent } from './app-footer/app-footer.component'; 9 | import { AppComponent } from './app.component'; 10 | 11 | @NgModule({ 12 | declarations: [AppComponent, AppFooterComponent], 13 | imports: [BrowserModule, BrowserAnimationsModule, NtkmeButtonModule, HeadroomModule], 14 | providers: [], 15 | bootstrap: [AppComponent], 16 | }) 17 | export class AppModule {} 18 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeCtrl/ngx-headroom/2eebb3c9742037ce4dd1001b977586a2f04fa06e/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeCtrl/ngx-headroom/2eebb3c9742037ce4dd1001b977586a2f04fa06e/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Headroom 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/headroom.component.ts: -------------------------------------------------------------------------------- 1 | import { animate, state, style, transition, trigger } from '@angular/animations'; 2 | import { DOCUMENT } from '@angular/common'; 3 | import { 4 | AfterContentInit, 5 | ChangeDetectionStrategy, 6 | Component, 7 | ElementRef, 8 | EventEmitter, 9 | HostListener, 10 | Inject, 11 | Input, 12 | OnInit, 13 | Output, 14 | ViewChild, 15 | } from '@angular/core'; 16 | 17 | import shouldUpdate from './shouldUpdate'; 18 | 19 | @Component({ 20 | selector: 'ngx-headroom', 21 | template: ` 22 |
27 |
44 | 45 |
46 |
47 | `, 48 | animations: [ 49 | trigger('headroom', [ 50 | state( 51 | 'unfixed', 52 | style({ 53 | transform: 'translateY(0)', 54 | }), 55 | ), 56 | state( 57 | 'unpinned', 58 | style({ 59 | transform: 'translateY(-100%)', 60 | }), 61 | ), 62 | state( 63 | 'pinned', 64 | style({ 65 | transform: 'translateY(0px)', 66 | }), 67 | ), 68 | transition('unpinned <=> pinned', animate('{{ duration }}ms {{ easing }}')), 69 | ]), 70 | ], 71 | preserveWhitespaces: false, 72 | changeDetection: ChangeDetectionStrategy.OnPush, 73 | }) 74 | export class HeadroomComponent implements OnInit, AfterContentInit { 75 | @Input() wrapperClassName = ''; 76 | @Input() innerClassName = ''; 77 | @Input() innerStyle: any = { 78 | top: '0', 79 | left: '0', 80 | right: '0', 81 | zIndex: '1', 82 | position: 'relative', 83 | }; 84 | /** 85 | * pass styles for the wrapper div 86 | * (this maintains the components vertical space at the top of the page) 87 | */ 88 | @Input() wrapperStyle: any = {}; 89 | /** disable pinning and unpinning */ 90 | @Input() disable = false; 91 | /** scroll tolerance in px when scrolling up before component is pinned */ 92 | @Input() upTolerance = 5; 93 | /** scroll tolerance in px when scrolling down before component is pinned */ 94 | @Input() downTolerance = 0; 95 | /** 96 | * height in px where the header should start and stop pinning. 97 | * Useful when you have another element above Headroom 98 | */ 99 | @Input() pinStart = 0; 100 | @Input() calcHeightOnResize = true; 101 | /** Duration of animation in ms */ 102 | @Input() duration = 200; 103 | /** Easing of animation */ 104 | @Input() easing = 'ease-in-out'; 105 | @Output() pin = new EventEmitter(); 106 | @Output() unpin = new EventEmitter(); 107 | @Output() unfix = new EventEmitter(); 108 | @ViewChild('ref', { static: true }) inner: ElementRef; 109 | wrapperHeight = 0; 110 | currentScrollY = 0; 111 | lastKnownScrollY = 0; 112 | scrolled = false; 113 | resizeTicking = false; 114 | state = 'unfixed'; 115 | translateY = '0px'; 116 | height: number; 117 | scrollTicking = false; 118 | /** 119 | * provide a custom 'parent' element for scroll events. 120 | * `parent` should be a function which resolves to the desired element. 121 | */ 122 | @Input() parent: () => any; 123 | @Input() 124 | @HostListener('window:scroll') 125 | scroll() { 126 | this.handleScroll(); 127 | } 128 | @Input() 129 | @HostListener('window:resize') 130 | resize() { 131 | this.handleResize(); 132 | } 133 | 134 | constructor(@Inject(DOCUMENT) private document: any) {} 135 | 136 | ngOnInit() { 137 | this.innerStyle.transform = `translateY(${this.translateY})`; 138 | 139 | if (this.disable === true) { 140 | this.handleUnfix(); 141 | } 142 | } 143 | getParent() { 144 | if (this.parent) { 145 | return this.parent(); 146 | } 147 | if (this.document.documentElement && this.document.documentElement.scrollTop) { 148 | return this.document.documentElement; 149 | } 150 | if (this.document.body && this.document.body.scrollTop) { 151 | return this.document.body; 152 | } 153 | if (this.document.body && this.document.body.parentNode.scrollTop) { 154 | return this.document.body.parentNode; 155 | } 156 | return this.document; 157 | } 158 | ngAfterContentInit() { 159 | this.setHeightOffset(); 160 | this.wrapperHeight = this.height ? this.height : null; 161 | } 162 | setHeightOffset() { 163 | this.height = null; 164 | setTimeout(() => { 165 | this.height = this.inner.nativeElement.offsetHeight; 166 | this.resizeTicking = false; 167 | }, 0); 168 | } 169 | getScrollY() { 170 | if (this.getParent().pageYOffset !== undefined) { 171 | return this.getParent().pageYOffset; 172 | } 173 | return this.getParent().scrollTop || 0; 174 | } 175 | getViewportHeight() { 176 | return ( 177 | this.getParent().innerHeight || 178 | this.document.documentElement.clientHeight || 179 | this.document.body.clientHeight 180 | ); 181 | } 182 | getDocumentHeight() { 183 | const body = this.document.body; 184 | const documentElement = this.document.documentElement; 185 | 186 | return Math.max( 187 | body.scrollHeight, 188 | documentElement.scrollHeight, 189 | body.offsetHeight, 190 | documentElement.offsetHeight, 191 | body.clientHeight, 192 | documentElement.clientHeight, 193 | ); 194 | } 195 | getElementPhysicalHeight(elm: any) { 196 | return Math.max(elm.offsetHeight, elm.clientHeight); 197 | } 198 | getElementHeight(elm: any) { 199 | return Math.max(elm.scrollHeight, elm.offsetHeight, elm.clientHeight); 200 | } 201 | getScrollerPhysicalHeight() { 202 | const parent = this.getParent(); 203 | 204 | return parent === this.getParent() || parent === this.document.body 205 | ? this.getViewportHeight() 206 | : this.getElementPhysicalHeight(parent); 207 | } 208 | getScrollerHeight() { 209 | const parent = this.getParent(); 210 | 211 | return parent === this.getParent() || parent === this.document.body 212 | ? this.getDocumentHeight() 213 | : this.getElementHeight(parent); 214 | } 215 | isOutOfBound(currentScrollY) { 216 | const pastTop = currentScrollY < 0; 217 | 218 | const scrollerPhysicalHeight = this.getScrollerPhysicalHeight(); 219 | const scrollerHeight = this.getScrollerHeight(); 220 | 221 | const pastBottom = currentScrollY + scrollerPhysicalHeight > scrollerHeight; 222 | 223 | return pastTop || pastBottom; 224 | } 225 | handleScroll() { 226 | if (this.disable) { 227 | return; 228 | } 229 | if (!this.scrollTicking) { 230 | this.scrollTicking = true; 231 | this.update(); 232 | } 233 | } 234 | handleResize() { 235 | if (this.disable || !this.calcHeightOnResize) { 236 | return; 237 | } 238 | if (!this.resizeTicking) { 239 | this.resizeTicking = true; 240 | this.setHeightOffset(); 241 | } 242 | } 243 | handleUnpin() { 244 | this.unpin.emit(); 245 | this.state = 'unpinned'; 246 | this.innerStyle.position = this.disable || this.state === 'unfixed' ? 'relative' : 'fixed'; 247 | } 248 | handlePin() { 249 | this.pin.emit(); 250 | this.state = 'pinned'; 251 | this.innerStyle.position = this.disable || this.state === 'unfixed' ? 'relative' : 'fixed'; 252 | } 253 | handleUnfix() { 254 | this.unfix.emit(); 255 | this.state = 'unfixed'; 256 | this.innerStyle.position = this.disable || this.state === 'unfixed' ? 'relative' : 'fixed'; 257 | } 258 | update() { 259 | this.currentScrollY = this.getScrollY(); 260 | 261 | if (!this.isOutOfBound(this.currentScrollY)) { 262 | const { action } = shouldUpdate( 263 | this.lastKnownScrollY, 264 | this.currentScrollY, 265 | this.disable, 266 | this.pinStart, 267 | this.downTolerance, 268 | this.upTolerance, 269 | this.state, 270 | this.height, 271 | ); 272 | 273 | if (action === 'pin') { 274 | this.handlePin(); 275 | } else if (action === 'unpin') { 276 | this.handleUnpin(); 277 | } else if (action === 'unfix') { 278 | this.handleUnfix(); 279 | } 280 | } 281 | 282 | this.lastKnownScrollY = this.currentScrollY; 283 | this.scrollTicking = false; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/lib/headroom.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { HeadroomComponent } from './headroom.component'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule], 8 | exports: [HeadroomComponent], 9 | declarations: [HeadroomComponent], 10 | }) 11 | export class HeadroomModule { } 12 | -------------------------------------------------------------------------------- /src/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "name": "@ctrl/ngx-headroom", 4 | "version": "0.0.0", 5 | "description": "Headroom.js implementation in Angular", 6 | "peerDependencies": { 7 | "@angular/animations": ">=12.0.0", 8 | "@angular/core": ">=12.0.0" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "repository": "typectrl/ngx-headroom", 14 | "homepage": "https://github.com/typectrl/ngx-headroom", 15 | "license": "MIT", 16 | "bugs": "https://github.com/typectrl/ngx-headroom/issues", 17 | "keywords": [ 18 | "ngx", 19 | "angular", 20 | "angular-component", 21 | "headroom" 22 | ], 23 | "ngPackage": { 24 | "lib": { 25 | "entryFile": "public_api.ts" 26 | }, 27 | "dest": "../../dist" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/public_api.ts: -------------------------------------------------------------------------------- 1 | export { HeadroomModule } from './headroom.module'; 2 | export { HeadroomComponent } from './headroom.component'; 3 | -------------------------------------------------------------------------------- /src/lib/shouldUpdate.spec.ts: -------------------------------------------------------------------------------- 1 | import shouldUpdate from './shouldUpdate'; 2 | 3 | let disable = false; 4 | let pinStart = 0; 5 | let downTolerance = 0; 6 | let upTolerance = 0; 7 | 8 | describe('shouldUpdate', () => { 9 | beforeEach(() => { 10 | disable = false; 11 | pinStart = 0; 12 | downTolerance = 0; 13 | upTolerance = 0; 14 | }); 15 | 16 | it('should exist', () => { 17 | expect(shouldUpdate).toBeTruthy(); 18 | }); 19 | 20 | it('should return an object', () => { 21 | expect(shouldUpdate()).toEqual(jasmine.any(Object)); 22 | }); 23 | 24 | // Test scrolling direction detection. 25 | it('should report scrolling down when currentScroll is greater than lastKnownScrollY', () => { 26 | expect(shouldUpdate(0, 10).scrollDirection).toEqual('down'); 27 | }); 28 | 29 | it('should report scrolling upwhen currentScroll is less than lastKnownScrollY', () => { 30 | expect(shouldUpdate(10, 0).scrollDirection).toEqual('up'); 31 | }); 32 | 33 | // Test action logic. 34 | it('should return an action of "none" if scrolling down and already unpinned', () => { 35 | const height = 0; 36 | const state = 'unpinned'; 37 | 38 | const result = shouldUpdate( 39 | 0, 40 | 10, 41 | disable, 42 | pinStart, 43 | downTolerance, 44 | upTolerance, 45 | state, 46 | height, 47 | ); 48 | expect(result.action).toEqual('none'); 49 | }); 50 | 51 | it('should return an action of "none" if scrolling up and already pinned', () => { 52 | const height = 0; 53 | const state = 'pinned'; 54 | 55 | const result = shouldUpdate( 56 | 100, 57 | 90, 58 | disable, 59 | pinStart, 60 | downTolerance, 61 | upTolerance, 62 | state, 63 | height, 64 | ); 65 | expect(result.action).toEqual('none'); 66 | }); 67 | 68 | it('should return an action of `unpin` if scrolling down and pinned', () => { 69 | const height = 0; 70 | const state = 'pinned'; 71 | 72 | const result = shouldUpdate( 73 | 0, 74 | 10, 75 | disable, 76 | pinStart, 77 | downTolerance, 78 | upTolerance, 79 | state, 80 | height, 81 | ); 82 | expect(result.action).toEqual('unpin'); 83 | }); 84 | 85 | it( 86 | 'should not return an action of `unpin` if scrolling down and unfixed ' + 87 | 'but the scrolling amount is less than pinStart', 88 | () => { 89 | pinStart = 200; 90 | const height = 0; 91 | const state = 'unfixed'; 92 | 93 | const result = shouldUpdate( 94 | 100, 95 | 110, 96 | disable, 97 | pinStart, 98 | downTolerance, 99 | upTolerance, 100 | state, 101 | height, 102 | ); 103 | expect(result.action).toEqual('none'); 104 | }, 105 | ); 106 | 107 | it( 108 | 'should not return an action of `unpin` if scrolling down and pinned ' + 109 | 'but the scrolling amount is less than downTolerance', 110 | () => { 111 | downTolerance = 1000; 112 | const height = 0; 113 | const state = 'pinned'; 114 | const result = shouldUpdate( 115 | 100, 116 | 110, 117 | disable, 118 | pinStart, 119 | downTolerance, 120 | upTolerance, 121 | state, 122 | height, 123 | ); 124 | expect(result.action).toEqual('none'); 125 | }, 126 | ); 127 | 128 | it('should return an action of `pin` if scrolling up and unpinned', () => { 129 | const height = 0; 130 | const state = 'unpinned'; 131 | const result = shouldUpdate( 132 | 10, 133 | 1, 134 | disable, 135 | pinStart, 136 | downTolerance, 137 | upTolerance, 138 | state, 139 | height, 140 | ); 141 | expect(result.action).toEqual('pin'); 142 | }); 143 | 144 | it( 145 | 'should not return an action of `pin` if scrolling up and unpinned' + 146 | 'but the scrolling amount is less than upTolerance', 147 | () => { 148 | upTolerance = 1000; 149 | const height = 0; 150 | const state = 'unpinned'; 151 | const result = shouldUpdate( 152 | 110, 153 | 100, 154 | disable, 155 | pinStart, 156 | downTolerance, 157 | upTolerance, 158 | state, 159 | height, 160 | ); 161 | expect(result.action).toEqual('none'); 162 | }, 163 | ); 164 | 165 | it("should return an action of 'none' if haven't scrolled past height of header", () => { 166 | const height = 100; 167 | const state = 'unfixed'; 168 | const result = shouldUpdate( 169 | 0, 170 | 10, 171 | disable, 172 | pinStart, 173 | downTolerance, 174 | upTolerance, 175 | state, 176 | height, 177 | ); 178 | expect(result.action).toEqual('none'); 179 | }); 180 | 181 | it( 182 | 'should return an action of `none` if scrolling up ' + 'when pinned within height of header', 183 | () => { 184 | const height = 100; 185 | const state = 'pinned'; 186 | const result = shouldUpdate( 187 | 50, 188 | 10, 189 | disable, 190 | pinStart, 191 | downTolerance, 192 | upTolerance, 193 | state, 194 | height, 195 | ); 196 | expect(result.action).toEqual('none'); 197 | }, 198 | ); 199 | 200 | it( 201 | 'should return an action of `pin` if scrolling up when unpinned within height of header ' + 202 | 'regardless of the upTolerance value', 203 | () => { 204 | upTolerance = 1000; 205 | let height = 100; 206 | let state = 'unpinned'; 207 | let result = shouldUpdate( 208 | 50, 209 | 10, 210 | disable, 211 | pinStart, 212 | downTolerance, 213 | upTolerance, 214 | state, 215 | height, 216 | ); 217 | 218 | expect(result.action).toEqual('pin'); 219 | 220 | height = 100; 221 | state = 'unpinned'; 222 | result = shouldUpdate(50, 1, disable, pinStart, downTolerance, upTolerance, state, height); 223 | expect(result.action).toEqual('pin'); 224 | }, 225 | ); 226 | 227 | it( 228 | 'should return an action of `none` if scrolling down ' + 'when pinned within height of header', 229 | () => { 230 | const height = 100; 231 | const state = 'pinned'; 232 | const result = shouldUpdate( 233 | 50, 234 | 80, 235 | disable, 236 | pinStart, 237 | downTolerance, 238 | upTolerance, 239 | state, 240 | height, 241 | ); 242 | expect(result.action).toEqual('none'); 243 | }, 244 | ); 245 | 246 | it( 247 | 'should return an action of `none` if scrolling up ' + 248 | 'when pinned within height of header or at the top', 249 | () => { 250 | const height = 100; 251 | const state = 'pinned'; 252 | const result = shouldUpdate( 253 | 100, 254 | 1, 255 | disable, 256 | pinStart, 257 | downTolerance, 258 | upTolerance, 259 | state, 260 | height, 261 | ); 262 | 263 | expect(result.action).toEqual('none'); 264 | }, 265 | ); 266 | 267 | it("should return an action of 'unfix' if currentScroll is less than or equal to pinStart", () => { 268 | pinStart = 20; 269 | 270 | const height = 100; 271 | const state = 'pinned'; 272 | let result = shouldUpdate( 273 | 100, 274 | 10, 275 | disable, 276 | pinStart, 277 | downTolerance, 278 | upTolerance, 279 | state, 280 | height, 281 | ); 282 | 283 | expect(result.action).toEqual('unfix'); 284 | 285 | result = shouldUpdate(100, 20, disable, pinStart, downTolerance, upTolerance, state, height); 286 | 287 | expect(result.action).toEqual('unfix'); 288 | }); 289 | 290 | it("should not return an action of 'unfix' if currentScroll is more than pinStart", () => { 291 | pinStart = 20; 292 | const height = 100; 293 | const state = 'pinned'; 294 | const result = shouldUpdate( 295 | 100, 296 | 50, 297 | disable, 298 | pinStart, 299 | downTolerance, 300 | upTolerance, 301 | state, 302 | height, 303 | ); 304 | 305 | expect(result.action).toEqual('none'); 306 | }); 307 | 308 | it("should return an action of 'unpin' if scroll down past height of header", () => { 309 | const height = 100; 310 | const state = 'unfixed'; 311 | const result = shouldUpdate( 312 | 100, 313 | 110, 314 | disable, 315 | pinStart, 316 | downTolerance, 317 | upTolerance, 318 | state, 319 | height, 320 | ); 321 | expect(result.action).toEqual('unpin'); 322 | }); 323 | }); 324 | -------------------------------------------------------------------------------- /src/lib/shouldUpdate.ts: -------------------------------------------------------------------------------- 1 | export default function ( 2 | lastKnownScrollY = 0, 3 | currentScrollY = 0, 4 | disable?: boolean, 5 | pinStart?: number, 6 | downTolerance?: number, 7 | upTolerance?: number, 8 | state?: string, 9 | height?: number, 10 | ) { 11 | const scrollDirection = currentScrollY >= lastKnownScrollY ? 'down' : 'up'; 12 | const distanceScrolled = Math.abs(currentScrollY - lastKnownScrollY); 13 | 14 | // We're disabled 15 | if (disable) { 16 | return { 17 | action: 'none', 18 | scrollDirection, 19 | distanceScrolled, 20 | }; 21 | // We're at the top and not fixed yet. 22 | } else if (currentScrollY <= pinStart && state !== 'unfixed') { 23 | return { 24 | action: 'unfix', 25 | scrollDirection, 26 | distanceScrolled, 27 | }; 28 | // We're unfixed and headed down. Carry on. 29 | } else if (currentScrollY <= height && scrollDirection === 'down' && state === 'unfixed') { 30 | return { 31 | action: 'none', 32 | scrollDirection, 33 | distanceScrolled, 34 | }; 35 | // We're past the header and scrolling down. 36 | // We transition to "unpinned" if necessary. 37 | } else if ( 38 | scrollDirection === 'down' && 39 | ['pinned', 'unfixed'].indexOf(state) >= 0 && 40 | currentScrollY > height + pinStart && 41 | distanceScrolled > downTolerance 42 | ) { 43 | return { 44 | action: 'unpin', 45 | scrollDirection, 46 | distanceScrolled, 47 | }; 48 | // We're scrolling up, we transition to "pinned" 49 | } else if ( 50 | scrollDirection === 'up' && 51 | distanceScrolled > upTolerance && 52 | ['pinned', 'unfixed'].indexOf(state) < 0 53 | ) { 54 | return { 55 | action: 'pin', 56 | scrollDirection, 57 | distanceScrolled, 58 | }; 59 | // We're scrolling up, and inside the header. 60 | // We transition to pin regardless of upTolerance 61 | } else if ( 62 | scrollDirection === 'up' && 63 | currentScrollY <= height && 64 | ['pinned', 'unfixed'].indexOf(state) < 0 65 | ) { 66 | return { 67 | action: 'pin', 68 | scrollDirection, 69 | distanceScrolled, 70 | }; 71 | } else { 72 | return { 73 | action: 'none', 74 | scrollDirection, 75 | distanceScrolled, 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags.ts'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/functions"; 2 | @import "~bootstrap/scss/variables"; 3 | @import "~bootstrap/scss/mixins"; 4 | @import "~bootstrap/scss/root"; 5 | @import "~bootstrap/scss/reboot"; 6 | @import "~bootstrap/scss/type"; 7 | // @import "~bootstrap/scss/images"; 8 | // @import "~bootstrap/scss/code"; 9 | @import "~bootstrap/scss/grid"; 10 | @import "~bootstrap/scss/tables"; 11 | // @import "~bootstrap/scss/forms"; 12 | // @import "~bootstrap/scss/buttons"; 13 | // @import "~bootstrap/scss/transitions"; 14 | // @import "~bootstrap/scss/dropdown"; 15 | // @import "~bootstrap/scss/button-group"; 16 | // @import "~bootstrap/scss/input-group"; 17 | // @import "~bootstrap/scss/custom-forms"; 18 | // @import "~bootstrap/scss/nav"; 19 | @import "~bootstrap/scss/navbar"; 20 | @import "~bootstrap/scss/card"; 21 | // @import "~bootstrap/scss/breadcrumb"; 22 | // @import "~bootstrap/scss/pagination"; 23 | // @import "~bootstrap/scss/badge"; 24 | // @import "~bootstrap/scss/jumbotron"; 25 | // @import "~bootstrap/scss/alert"; 26 | // @import "~bootstrap/scss/progress"; 27 | // @import "~bootstrap/scss/media"; 28 | // @import "~bootstrap/scss/list-group"; 29 | // @import "~bootstrap/scss/close"; 30 | // @import "~bootstrap/scss/modal"; 31 | // @import "~bootstrap/scss/tooltip"; 32 | // @import "~bootstrap/scss/popover"; 33 | // @import "~bootstrap/scss/carousel"; 34 | @import "~bootstrap/scss/utilities"; 35 | @import "~bootstrap/scss/print"; 36 | 37 | nav { 38 | margin-bottom: 3em; 39 | } 40 | -------------------------------------------------------------------------------- /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/testing'; 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserDynamicTestingModule, 6 | platformBrowserDynamicTesting, 7 | } from '@angular/platform-browser-dynamic/testing'; 8 | 9 | declare const require: any; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting(), { 15 | teardown: { destroyAfterEach: false } 16 | }, 17 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "include": [ 8 | "src/**/*.d.ts" 9 | ], 10 | "files": [ 11 | "src/main.ts", 12 | "src/polyfills.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "importHelpers": true, 12 | "target": "es2015", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2018", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------