├── .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 |
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 |
3 |
4 |
5 |
12 |
16 |
17 | Angular Headroom
18 |
19 |
20 |
27 |
28 |
29 |
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 | Empty Space
65 |
66 |
67 |
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 |
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 |
--------------------------------------------------------------------------------