Repository: MrFrankel/ngx-popper Branch: master Commit: d53971a125df Files: 38 Total size: 80.2 KB Directory structure: gitextract_bco3m_wf/ ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── example/ │ ├── app/ │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── index.ts │ ├── index.html │ ├── tsconfig.json │ └── webpack.config.js ├── jest.config.js ├── ng-package.json ├── package.json ├── public_api.ts ├── puppeteer_environment.js ├── setup.js ├── src/ │ ├── index.ts │ ├── popper-content.css │ ├── popper-content.ts │ ├── popper-directive.ts │ ├── popper-model.ts │ └── popper.module.ts ├── teardown.js ├── test/ │ ├── __tests__/ │ │ └── basic.test.js │ ├── app/ │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── index.ts │ ├── index.html │ ├── tsconfig.json │ ├── utils.js │ └── webpack.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ *.iml *.xml .idea dist dist-tsc test_dist ================================================ FILE: .npmignore ================================================ .idea example node_modules /src /*.* ./dist/dist-tsc ./dist/example ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '10' dist: trusty addons: apt: packages: # This is required to run new chrome on old trusty - libnss3 notifications: email: false cache: directories: - node_modules # allow headful tests before_install: # Enable user namespace cloning - "sysctl kernel.unprivileged_userns_clone=1" # Launch XVFB - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016-2017 Jacob Harasimo. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ngx-popper [![npm](https://img.shields.io/npm/v/ngx-popper.svg?style=flat-square)](https://www.npmjs.com/package/ngx-popper) [![npm](https://img.shields.io/npm/dm/ngx-popper.svg?style=flat-square)](https://www.npmjs.com/package/ngx-popper) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/MrFrankel/ngx-popper/blob/master/LICENSE) [![Greenkeeper badge](https://badges.greenkeeper.io/MrFrankel/ngx-popper.svg)](https://greenkeeper.io/) Stable Release Size Stable Release Size [![Build Status](https://travis-ci.org/MrFrankel/ngx-popper.svg?branch=master)](https://travis-ci.org/MrFrankel/ngx-popper) ngx-popper is an angular wrapper for the [Popper.js](https://popper.js.org/) library. ## Changes As of version 6.0.0 popper content runs in onPush change detection strategy, therefore forceChangeDetection is no longer necessary and is removed As of version 4.0.0 ngx-popper now use innerHTML binding for string popper i.e: ```HTML
``` This should make no difference but you should be aware. As of version 4.0.0 popper.model is now popper-model, due to some angular-cli issues, if you are referencing this please update your references. ### Installation node and npm are required to run this package. 1. Use npm/yarn to install the package: ```terminal $ npm install popper.js --save $ npm install ngx-popper --save ``` Or ```terminal $ yarn add popper.js --save $ yarn add ngx-popper --save ``` 2. You simply add into your module `NgxPopperModule`: ```typescript import {NgxPopperModule} from 'ngx-popper'; @NgModule({ // ... imports: [ // ... NgxPopperModule ] }) ``` SystemJS ``` System.config({ paths: { // paths serve as alias 'npm:': 'libs/' }, // map tells the System loader where to look for things map: { ... , 'ngx-popper': 'npm:ngx-popper', 'popper-directive.js': 'npm:ngx-popper', 'popper.module': 'npm:ngx-popper', }, // packages tells the System loader how to load when no filename and/or no extension packages: { ... , 'ngx-popper': { main: 'index.js', defaultExtension: 'js' }, 'popper.js': { main: 'popper-directive.js', defaultExtension: 'js' }, 'popper.module': { main: './popper.module.js', defaultExtension: 'js' } } }); ``` 3. Add to view: ```HTML

Hey!

Choose where to put your popper!

Popper on bottom

``` 4. As text: ```HTML

Pop

on the bottom

``` ```HTML

Pop

on the bottom

``` 5. Position fixed, breaking overflow: ```HTML
``` 6. Specific target: ```HTML
``` 7. hide/show programmatically: ```HTML

Pop

on the bottom

This is a tooltip with text

Close
``` 8. Attributes map: | Option | Type | Default | Description | |:----------------------- |:---------------- |:--------- | :------------------------------------------------------------------------------------------------------ | | popperDisableAnimation | boolean | false | Disable the default animation on show/hide | | popperDisableStyle | boolean | false | Disable the default styling | | popperDisabled | boolean | false | Disable the popper, ignore all events | | popperDelay | number | 0 | Delay time until popper it shown | | popperTimeout | number | 0 | Set delay before the popper is hidden | | popperTimeoutAfterShow | number | 0 | Set a time on which the popper will be hidden after it is shown | | popperPlacement | Placement(string) | auto | The placement to show the popper relative to the reference element | | popperTarget | HtmlElement | auto | Specify a different reference element other the the one hosting the directive | | popperBoundaries | string(selector) | undefined | Specify a selector to serve as the boundaries of the element | | popperShowOnStart | boolean | false | Popper default to show | | popperTrigger | Trigger(string) | hover | Trigger/Event on which to show/hide the popper | | popperPositionFixed | boolean | false | Set the popper element to use position: fixed | | popperAppendTo | string | undefined | append The popper-content element to a given selector, if multiple will apply to first | | popperPreventOverflow | boolean | undefined | Prevent the popper from being positioned outside the boundary | | popperHideOnClickOutside | boolean | true | Popper will hide on a click outside | | popperHideOnScroll | boolean | false | Popper will hide on scroll | | popperHideOnMouseLeave | boolean | false | Popper will hide on mouse leave | | popperModifiers | popperModifier | undefined | popper.js custom modifiers hock | | popperApplyClass | string | undefined | list of comma separated class to apply on ngpx__container | | popperStyles | Object | undefined | Apply the styles object, aligned with ngStyles | | popperApplyArrowClass | string | undefined | list of comma separated class to apply on ngpx__arrow | | popperOnShown | EventEmitter<> | $event | Event handler when popper is shown | | popperOnHidden | EventEmitter<> | $event | Event handler when popper is hidden | | popperOnUpdate | EventEmitter<> | $event | Event handler when popper is updated | | popperAriaDescribeBy | string | undefined | Define value for aria-describeby attribute | | popperAriaRole | string | popper | Define value for aria-role attribute | 9. Override defaults: Ngx-popper comes with a few default properties you can override in default to effect all instances These are overridden by any child attributes. ```JavaScript NgModule({ imports: [ BrowserModule, FormsModule, NgxPopperModule.forRoot({placement: 'bottom'})], declarations: [AppComponent], providers: [], bootstrap: [AppComponent] }) ``` | Options | Type | Default | |:------------------- |:---------------- |:--------- | | showDelay               | number           | 0       | | disableAnimation | boolean | false | | disableDefaultStyling | boolean | false | | placement | Placement(string) | auto | | boundariesElement | string(selector) | undefined | | trigger | Trigger(string) | hover | | popperModifiers | popperModifier | undefined | | positionFixed | boolean | false | | hideOnClickOutside | boolean | true | | hideOnMouseLeave | boolean | false | | hideOnScroll | boolean | false | | applyClass | string | undefined | | styles | Object | undefined | | applyArrowClass | string | undefined | | ariaDescribeBy | string | undefined | | ariaRole | string | undefined | | appendTo | string | undefined | | preventOverflow | boolean | undefined | 10. popperPlacement: | 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'bottom-start' | 'left-start' | 'right-start' | 'top-end' | 'bottom-end' | 'left-end' | 'right-end' | 'auto' | 'auto-start' | 'auto-end' | Function 11. popperTrigger: | 'click' | 'mousedown' | 'hover' | 'none' ### Demo Demo ### Contribute Hell ya!!! ```terminal $ npm install $ npm run build $ npm run dev //run example $ npm run start_test //run tests ``` ================================================ FILE: _config.yml ================================================ theme: jekyll-theme-cayman ================================================ FILE: example/app/app.component.css ================================================ p.thin { font-weight: 100; margin: 0; line-height: 1.2em; } p.bold { font-weight: 900; margin: 0; margin-top: -5px; } .rel { width: 30%; margin: 0 auto; position: relative; text-align: center; padding: 20px; border-style: dotted; border-color: white; border-width: medium; } .ngxp__container { width: 150px; background: #FFC107; color: black; padding: 10px; text-align: center; } #example10positionSelector { margin-top: 1em; } .ngxp__container[x-placement^="top"] .ngxp__arrow { border-color: #FFC107 transparent transparent transparent } .ngxp__container[x-placement^="bottom"] .ngxp__arrow { border-color: transparent transparent #FFC107 transparent } .ngxp__container[x-placement^="right"] .ngxp__arrow { border-color: transparent #FFC107 transparent transparent } .ngxp__container[x-placement^="left"] .ngxp__arrow { border-color: transparent transparent transparent #FFC107 } .example2__fake-body { overflow-x: scroll; height: 450px; flex: 1; } .example2__scroll-box { height: 200%; display: flex; align-content: center; align-items: center; } #example3 .reference1:hover { background: rgba(255, 255, 255, 0.2); } #example4 .ngxp__container1 { height: 150px; } p.thin { font-weight: 100; margin: 0; line-height: 1.2em; } p.bold { font-weight: 900; margin: 0; margin-top: -5px; } .rel { width: 30%; margin: 0 auto; position: relative; text-align: center; padding: 20px; border-style: dotted; border-color: white; border-width: medium; } /* Spectral by HTML5 UP html5up.net | @n33co Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) */ /* Reset */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } body { -webkit-text-size-adjust: none; } /* Box Model */ *, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } /* Basic */ @-ms-viewport { width: device-width; } body { background: #2e3842; } body.is-loading *, body.is-loading *:before, body.is-loading *:after { -moz-animation: none !important; -webkit-animation: none !important; -ms-animation: none !important; å animation: none !important; -moz-transition: none !important; -webkit-transition: none !important; -ms-transition: none !important; transition: none !important; } body, input, select, textarea { color: #fff; font-family: "Open Sans", Helvetica, sans-serif; font-size: 15pt; font-weight: 400; letter-spacing: 0.075em; line-height: 1.65em; } @media screen and (max-width: 1680px) { body, input, select, textarea { font-size: 13pt; } } @media screen and (max-width: 1280px) { body, input, select, textarea { font-size: 12pt; } } @media screen and (max-width: 736px) { body, input, select, textarea { font-size: 11pt; letter-spacing: 0.0375em; } } a { -moz-transition: color 0.2s ease, border-bottom-color 0.2s ease; -webkit-transition: color 0.2s ease, border-bottom-color 0.2s ease; -ms-transition: color 0.2s ease, border-bottom-color 0.2s ease; transition: color 0.2s ease, border-bottom-color 0.2s ease; border-bottom: dotted 1px; color: inherit; text-decoration: none; } a:hover { border-bottom-color: transparent; } strong, b { color: #fff; font-weight: 600; } em, i { font-style: italic; } p { margin: 0 0 2em 0; } h1, h2, h3, h4, h5, h6 { color: #fff; font-weight: 800; letter-spacing: 0.225em; line-height: 1em; margin: 0 0 1em 0; text-transform: uppercase; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; text-decoration: none; } h2 { font-size: 1.35em; line-height: 1.75em; } @media screen and (max-width: 736px) { h2 { font-size: 1.1em; line-height: 1.65em; } } h3 { font-size: 1.15em; line-height: 1.75em; } @media screen and (max-width: 736px) { h3 { font-size: 1em; line-height: 1.65em; } } h4 { font-size: 1em; line-height: 1.5em; } h5 { font-size: 0.8em; line-height: 1.5em; } h6 { font-size: 0.7em; line-height: 1.5em; } sub { font-size: 0.8em; position: relative; top: 0.5em; } sup { font-size: 0.8em; position: relative; top: -0.5em; } hr { border: 0; border-bottom: solid 2px #fff; margin: 3em 0; } hr.major { margin: 4.5em 0; } blockquote { border-left: solid 4px #fff; font-style: italic; margin: 0 0 2em 0; padding: 0.5em 0 0.5em 2em; } code { background: rgba(144, 144, 144, 0.25); border-radius: 3px; font-family: "Courier New", monospace; font-size: 0.9em; letter-spacing: 0; margin: 0 0.25em; padding: 0.25em 0.65em; } pre { -webkit-overflow-scrolling: touch; font-family: "Courier New", monospace; font-size: 0.8em; margin: 0 0 2em 0; text-align: left; } pre code { display: block; line-height: 1.75em; padding: 1em 1.5em; overflow-x: auto; } header p { color: rgba(255, 255, 255, 0.5); position: relative; top: -0.25em; } header h3 + p { font-size: 1.1em; } header h4 + p, header h5 + p, header h6 + p { font-size: 0.9em; } header.major { margin: 0 0 3.5em 0; } header.major h2, header.major h3, header.major h4, header.major h5, header.major h6 { border-bottom: solid 2px #fff; display: inline-block; padding-bottom: 1em; position: relative; } header.major h2:after, header.major h3:after, header.major h4:after, header.major h5:after, header.major h6:after { content: ''; display: block; height: 1px; } header.major p { color: #fff; top: 0; } @media screen and (max-width: 736px) { header.major { margin: 0 0 2em 0; } } @media screen and (max-width: 980px) { header br { display: none; } } /* Form */ form { margin: 0 0 2em 0; } label { color: #fff; display: block; font-size: 0.9em; font-weight: 600; margin: 0 0 1em 0; } @media screen and (max-width: 736px) { .wrapper.style1 .features li { border-top-color: rgba(0, 0, 0, 0.125); } } .wrapper.style2 { background-color: #2e3842; } select { -moz-appearance: none; -webkit-appearance: none; -ms-appearance: none; appearance: none; background: rgba(144, 144, 144, 0.25); border-radius: 3px; border: none; color: inherit; display: block; outline: 0; padding: 0 1em; text-decoration: none; width: 100%; } ::-webkit-input-placeholder { color: rgba(255, 255, 255, 0.5) !important; opacity: 1.0; } :-moz-placeholder { color: rgba(255, 255, 255, 0.5) !important; opacity: 1.0; } ::-moz-placeholder { color: rgba(255, 255, 255, 0.5) !important; opacity: 1.0; } :-ms-input-placeholder { color: rgba(255, 255, 255, 0.5) !important; opacity: 1.0; } .formerize-placeholder { color: rgba(255, 255, 255, 0.5) !important; opacity: 1.0; } /* Spotlight */ .spotlight { display: -moz-flex; display: -webkit-flex; display: -ms-flex; display: flex; } .spotlight:nth-child(2n) { -moz-flex-direction: row-reverse; -webkit-flex-direction: row-reverse; -ms-flex-direction: row-reverse; flex-direction: row-reverse; } .spotlight:nth-child(1) { background-color: rgba(0, 0, 0, 0.075); } .spotlight:nth-child(2) { background-color: rgba(0, 0, 0, 0.15); } .spotlight:nth-child(3) { background-color: rgba(0, 0, 0, 0.225); } .spotlight:nth-child(4) { background-color: rgba(0, 0, 0, 0.3); } .spotlight:nth-child(5) { background-color: rgba(0, 0, 0, 0.375); } .spotlight:nth-child(6) { background-color: rgba(0, 0, 0, 0.45); } .spotlight:nth-child(7) { background-color: rgba(0, 0, 0, 0.525); } .spotlight:nth-child(8) { background-color: rgba(0, 0, 0, 0.6); } .spotlight:nth-child(9) { background-color: rgba(0, 0, 0, 0.675); } .spotlight:nth-child(10) { background-color: rgba(0, 0, 0, 0.75); } @media screen and (max-width: 1280px) { .spotlight .image { width: 45%; } .spotlight .content { width: 55%; } } @media screen and (max-width: 980px) { .spotlight { display: block; } .spotlight br { display: none; } .spotlight .image, .spotlight .example { width: 100%; } .spotlight .content { padding: 4em 3em 2em 3em; max-width: none; text-align: center; width: 100%; } } @media screen and (max-width: 736px) { .spotlight .content { padding: 3em 2em 1em 2em; } } /* Wrapper */ .wrapper { padding: 6em 0 4em 0; } .wrapper > .inner { width: 60em; margin: 0 auto; } @media screen and (max-width: 1280px) { .wrapper > .inner { width: 90%; } } @media screen and (max-width: 980px) { .wrapper > .inner { width: 100%; } } .wrapper.alt { padding: 0; } .wrapper.style2 { background-color: #2e3842; } /* Spotlight */ .spotlight { display: -moz-flex; display: -webkit-flex; display: -ms-flex; display: flex; } .spotlight .image { -moz-order: 1; -webkit-order: 1; -ms-order: 1; order: 1; border-radius: 0; width: 40%; } .spotlight .image img { border-radius: 0; width: 100%; } .spotlight .example { -moz-order: 1; -webkit-order: 1; -ms-order: 1; order: 1; position: relative; min-height: 450px; width: 40%; background: rgba(0, 0, 0, 0.3); display: flex; align-content: center; align-items: center; } .spotlight .content { padding: 2em 4em 0.1em 4em; -moz-order: 2; -webkit-order: 2; -ms-order: 2; order: 2; max-width: 48em; width: 60%; } .spotlight:nth-child(2n) { -moz-flex-direction: row-reverse; -webkit-flex-direction: row-reverse; -ms-flex-direction: row-reverse; flex-direction: row-reverse; } .spotlight:nth-child(1) { background-color: rgba(0, 0, 0, 0.075); } .spotlight:nth-child(2) { background-color: rgba(0, 0, 0, 0.15); } .spotlight:nth-child(3) { background-color: rgba(0, 0, 0, 0.225); } .spotlight:nth-child(4) { background-color: rgba(0, 0, 0, 0.3); } .spotlight:nth-child(5) { background-color: rgba(0, 0, 0, 0.375); } .spotlight:nth-child(6) { background-color: rgba(0, 0, 0, 0.45); } .spotlight:nth-child(7) { background-color: rgba(0, 0, 0, 0.525); } .spotlight:nth-child(8) { background-color: rgba(0, 0, 0, 0.6); } .spotlight:nth-child(9) { background-color: rgba(0, 0, 0, 0.675); } .spotlight:nth-child(10) { background-color: rgba(0, 0, 0, 0.75); } @media screen and (max-width: 1280px) { .spotlight .image { width: 45%; } .spotlight .content { width: 55%; } } @media screen and (max-width: 980px) { .spotlight { display: block; } .spotlight br { display: none; } .spotlight .image, .spotlight .example { width: 100%; } .spotlight .content { padding: 4em 3em 2em 3em; max-width: none; text-align: center; width: 100%; } } @media screen and (max-width: 736px) { .spotlight .content { padding: 3em 2em 1em 2em; } } @media screen and (max-width: 980px) { .wrapper { padding: 4em 3em 2em 3em; } } @media screen and (max-width: 736px) { .wrapper { padding: 3em 2em 1em 2em; } } ================================================ FILE: example/app/app.component.html ================================================

Hey!

Choose where to put your popper!

Popper on {{example1select}}

Popper on your side!

What are you waiting for? Select a popper from that dropdown.
Placing poppers around elements is just that easy!

Scroll me

up and down

I follow it

staying between boundaries

Popper on scrolling container

In this example we have a relative div which contains a div with overflow: scroll.
Inside it, there are our popper and reference elements.

Drag me

on the edges

Flipping popper

which never flips to right

Custom flip behavior

Try dragging the reference element on the left side, its popper will move on its bottom edge. Then, try to move the reference element on the bottom left corner, it will move on its top edge.

Reference

Shifted popper

on start

Shifted poppers

Shift your poppers on start or end of its reference element side.

Pop

on the bottom

Popper on bottom

Flips when hits viewport

Viewport boundaries

By default, poppers use as boundaries the page viewport.
Scroll the page to see the popper flip when hits the page viewport margins.

================================================ FILE: example/app/app.component.ts ================================================ import {Component, ElementRef, ViewEncapsulation, OnInit, ViewChild} from '@angular/core'; import {PopperContent} from '../../dist'; /** * This class represents the main application component. */ @Component({ selector: 'demo-app', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], encapsulation: ViewEncapsulation.None }) export class AppComponent implements OnInit { example3modifiers = { flip: { behavior: ['right', 'bottom', 'top'] } }; example1select: string = 'top'; @ViewChild('popper3Content') popper3Content: any; constructor(private elem: ElementRef) { } ngOnInit() { setInterval(() => { this.popper3Content.update(); }, 10); } changeExample1(popperRef: PopperContent) { setTimeout(() => { this.elem.nativeElement.querySelector('#example10reference1').dispatchEvent(new Event('click')); }, 100) } } ================================================ FILE: example/app/app.module.ts ================================================ import {NgModule} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {BrowserModule} from '@angular/platform-browser'; import {AppComponent} from './app.component'; import {NgxPopperModule, Triggers} from '../../dist'; import {Draggable} from 'ng2draggable/draggable.directive'; @NgModule({ imports: [ BrowserModule, FormsModule, NgxPopperModule.forRoot({ trigger: Triggers.CLICK, hideOnClickOutside: false })], declarations: [ Draggable, AppComponent], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ================================================ FILE: example/app/index.ts ================================================ import 'zone.js'; import 'reflect-metadata'; import '@angular/common'; import '@angular/compiler'; import '@angular/core'; import '@angular/forms'; import '@angular/platform-browser-dynamic'; import '@angular/platform-browser'; import 'rxjs'; import { enableProdMode } from '@angular/core'; // The browser platform with a compiler //import {platformBrowser} from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; // The app module import { AppModule } from './app.module'; enableProdMode(); platformBrowserDynamic().bootstrapModule(AppModule); ================================================ FILE: example/index.html ================================================ ngx-popper ================================================ FILE: example/tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist-tsc", "module": "commonjs", "target": "es5", "sourceMap": true, "declaration": true, "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "strictNullChecks": true, "skipLibCheck": true, "lib": [ "es7", "dom", "es2017.object", "es2015.iterable", "ScriptHost" ] }, "typeRoots": [ "../node_modules/@types", "../node_modules" ], "types": [ "node" ], "include": [ "." ], "exclude": [ "../dist", "../dist-tsc", "../test" ], "angularCompilerOptions": { "preserveWhiteSpace": false } } ================================================ FILE: example/webpack.config.js ================================================ const path = require('path'); const {CheckerPlugin} = require('awesome-typescript-loader'); const webpack = require('webpack'); const chalk = require('chalk'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); module.exports = { entry: './example/app/index.ts', output: { filename: '[name].js', path: path.resolve(__dirname, 'dist') }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.html', '.css'] }, devtool: 'source-map', module: { rules: [ { test: /\.ts?$/, use: ['awesome-typescript-loader?configFileName="./tsconfig.json"', 'angular2-template-loader'], // exclude: [/node_modules/, /dist/, /dist-tsc/, /test/, /public_api/] }, { test: /\.(html|css)$/, use: 'raw-loader', // exclude: [/node_modules/, /dist/, /dist-tsc/, /test/] } ] }, plugins: [ new CheckerPlugin(), new HtmlWebpackPlugin({ inject: true, template: './example/index.html' }), new ProgressBarPlugin({ format: ' build [' + chalk.blue.bold(':bar') + ']' + chalk.green.bold(':percent') + ' (:elapsed seconds) => :msg... ', clear: false }), new webpack.HotModuleReplacementPlugin() ], devServer: { // https: true, hot: true, stats: 'errors-only', port: 8888, inline: true, historyApiFallback: { index: './example/' }, open: false } }; ================================================ FILE: jest.config.js ================================================ module.exports = { globalSetup: './setup.js', globalTeardown: './teardown.js', testEnvironment: './puppeteer_environment.js', }; ================================================ FILE: ng-package.json ================================================ { "$schema": "./node_modules/ng-packagr/ng-package.schema.json", "dest": "dist", "workingDirectory": ".ng_build", "lib": { "entryFile": "public_api.ts" } } ================================================ FILE: package.json ================================================ { "name": "ngx-popper", "version": "7.0.0", "description": "ngx-popper is an Angular wrapper for popper.js", "directories": { "test": "test" }, "scripts": { "test-server": "http-server test_dist -p 8888", "create_test_server": "rm -rf test_dist && webpack --config ./test/webpack.config.js && npm run test-server", "test": "npm install && npm run start_test", "run_jest": "jest", "start_test": "npm run build && start-server-and-test create_test_server http://localhost:8888 run_jest", "build": "ng-packagr -p ng-package.json", "deploy": "npm run build && npm publish --access=public dist", "dev": "webpack-dev-server --config ./example/webpack.config.js" }, "repository": { "type": "git", "url": "git@github.com:MrFrankel/ngx-popper" }, "keywords": [ "Angular", "ngx", "ngx-popper", "ngx-tooltip", "popper", "popperjs" ], "engines": { "node": ">=6.0.0", "npm": ">=3.4.0" }, "author": "MrFrankel", "license": "MIT", "bugs": { "url": "/issues" }, "homepage": "", "devDependencies": { "@angular/animations": "^7.2.0", "@angular/cli": "^7.3.9", "@angular/common": "^7.2.0", "@angular/compiler": "^7.2.0", "@angular/compiler-cli": "^7.2.15", "@angular/core": "^7.2.0", "@angular/forms": "^7.2.0", "@angular/http": "^7.2.0", "@angular/platform-browser": "^7.2.0", "@angular/platform-browser-dynamic": "^7.2.0", "@angular/platform-server": "^7.2.0", "@ngtools/webpack": "^7.2.1", "@types/node": "^10.12.2", "angular2-template-loader": "^0.6.2", "awesome-typescript-loader": "^5.2.1", "chalk": "^2.3.0", "css-loader": "^1.0.1", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "http-server": "^0.11.1", "install": "^0.12.2", "jest": "^23.0.0", "jest-puppeteer": "^3.8.0", "ng-packagr": "^4.5.0", "ng2draggable": "^1.3.2", "popper.js": "^1.14.3", "progress-bar-webpack-plugin": "^1.10.0", "puppeteer": "^1.15.0", "raw-loader": "^0.5.1", "reflect-metadata": "^0.1.12", "rxjs": "^6.3.3", "script-loader": "^0.7.2", "standard-version": "^4.3.0", "start-server-and-test": "^1.7.11", "style-loader": "^0.23.1", "tsickle": "^0.34.0", "typescript": "^3.1.6", "webpack": "^4.24.0", "webpack-cli": "^3.2.1", "webpack-dev-server": "^3.1.14", "webpack-shell-plugin": "^0.5.0", "zone.js": "^0.8.20" }, "peerDependencies": { "popper.js": "^1.14.3" } } ================================================ FILE: public_api.ts ================================================ export * from "./src/index"; ================================================ FILE: puppeteer_environment.js ================================================ const chalk = require('chalk'); const NodeEnvironment = require('jest-environment-node'); const puppeteer = require('puppeteer'); const fs = require('fs'); const os = require('os'); const path = require('path'); const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); class PuppeteerEnvironment extends NodeEnvironment { constructor(config) { super(config) } async setup() { console.log(chalk.yellow('Setup Test Environment.')); await super.setup(); const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8'); if (!wsEndpoint) { throw new Error('wsEndpoint not found') } this.global.__BROWSER__ = await puppeteer.connect({ browserWSEndpoint: wsEndpoint, }) } async teardown() { console.log(chalk.yellow('Teardown Test Environment.')); await super.teardown() } runScript(script) { return super.runScript(script) } } module.exports = PuppeteerEnvironment; ================================================ FILE: setup.js ================================================ const chalk = require('chalk'); const puppeteer = require('puppeteer'); const fs = require('fs'); const mkdirp = require('mkdirp'); const os = require('os'); const path = require('path'); const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); module.exports = async function() { console.log(chalk.green('Setup Puppeteer')); const browser = await puppeteer.launch({devtools: true}); global.__BROWSER__ = browser; mkdirp.sync(DIR); fs.writeFileSync(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint()); }; ================================================ FILE: src/index.ts ================================================ export * from './popper-model'; export * from './popper-directive'; export * from './popper-content'; export * from './popper.module'; ================================================ FILE: src/popper-content.css ================================================ .ngxp__container { display: none; position: absolute; border-radius: 3px; border: 1px solid grey; box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); padding: 10px; } .ngxp__container.ngxp__animation { -webkit-animation: ngxp-fadeIn 150ms ease-out; -moz-animation: ngxp-fadeIn 150ms ease-out; -o-animation: ngxp-fadeIn 150ms ease-out; animation: ngxp-fadeIn 150ms ease-out; } .ngxp__container > .ngxp__arrow { border-color: grey; width: 0; height: 0; border-style: solid; position: absolute; margin: 5px; } .ngxp__container[x-placement^="top"], .ngxp__container[x-placement^="bottom"], .ngxp__container[x-placement^="right"], .ngxp__container[x-placement^="left"] { display: block; } .ngxp__container[x-placement^="top"] { margin-bottom: 5px; } .ngxp__container[x-placement^="top"] > .ngxp__arrow { border-width: 5px 5px 0 5px; border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; bottom: -5px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; } .ngxp__container[x-placement^="top"] > .ngxp__arrow.__force-arrow { border-right-color: transparent !important; border-bottom-color: transparent !important; border-left-color: transparent !important; } .ngxp__container[x-placement^="bottom"] { margin-top: 5px; } .ngxp__container[x-placement^="bottom"] > .ngxp__arrow { border-width: 0 5px 5px 5px; border-top-color: transparent; border-right-color: transparent; border-left-color: transparent; top: -5px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; } .ngxp__container[x-placement^="bottom"] > .ngxp__arrow.__force-arrow { border-top-color: transparent !important; border-right-color: transparent !important; border-left-color: transparent !important; } .ngxp__container[x-placement^="right"] { margin-left: 5px; } .ngxp__container[x-placement^="right"] > .ngxp__arrow { border-width: 5px 5px 5px 0; border-top-color: transparent; border-bottom-color: transparent; border-left-color: transparent; left: -5px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; } .ngxp__container[x-placement^="right"] > .ngxp__arrow.__force-arrow { border-top-color: transparent !important; border-bottom-color: transparent !important; border-left-color: transparent !important; } .ngxp__container[x-placement^="left"] { margin-right: 5px; } .ngxp__container[x-placement^="left"] > .ngxp__arrow { border-width: 5px 0 5px 5px; border-top-color: transparent; border-bottom-color: transparent; border-right-color: transparent; right: -5px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; } .ngxp__container[x-placement^="left"] > .ngxp__arrow.__force-arrow { border-top-color: transparent !important; border-bottom-color: transparent !important; border-right-color: transparent !important; } @-webkit-keyframes ngxp-fadeIn { 0% { display: none; opacity: 0; } 1% { display: block; opacity: 0; } 100% { display: block; opacity: 1; } } @keyframes ngxp-fadeIn { 0% { display: none; opacity: 0; } 1% { display: block; opacity: 0; } 100% { display: block; opacity: 1; } } ================================================ FILE: src/popper-content.ts ================================================ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, OnDestroy, Renderer2, ViewChild, ViewContainerRef, ViewEncapsulation, } from "@angular/core" import Popper from 'popper.js' import {Placements, PopperContentOptions, Triggers} from './popper-model' @Component({ selector: "popper-content", encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: `
`, styleUrls: ['./popper-content.css'], }) export class PopperContent implements OnDestroy { popperOptions: PopperContentOptions = { disableAnimation: false, disableDefaultStyling: false, placement: Placements.Auto, boundariesElement: '', trigger: Triggers.HOVER, positionFixed: false, appendToBody: false, popperModifiers: {} }; referenceObject: HTMLElement; isMouseOver: boolean = false; onHidden = new EventEmitter(); text: string; popperInstance: Popper; displayType: string = "none"; opacity: number = 0; ariaHidden: string = 'true'; arrowColor: string | null = null; onUpdate: Function; state: boolean = true; private globalResize: any; @ViewChild("popperViewRef") popperViewRef: ElementRef; @HostListener('mouseover') onMouseOver() { this.isMouseOver = true; } @HostListener('mouseleave') showOnLeave() { this.isMouseOver = false; if (this.popperOptions.trigger !== Triggers.HOVER && !this.popperOptions.hideOnMouseLeave) { return; } this.hide(); } onDocumentResize() { this.update(); } constructor( public elemRef: ElementRef, private renderer: Renderer2, private viewRef: ViewContainerRef, private CDR: ChangeDetectorRef) { } ngOnDestroy() { this.clean(); if(this.popperOptions.appendTo && this.elemRef && this.elemRef.nativeElement && this.elemRef.nativeElement.parentNode){ this.viewRef.detach(); this.elemRef.nativeElement.parentNode.removeChild(this.elemRef.nativeElement); } } clean() { this.toggleVisibility(false); if (!this.popperInstance) { return; } (this.popperInstance as any).disableEventListeners(); this.popperInstance.destroy(); } show(): void { if (!this.referenceObject) { return; } const appendToParent = this.popperOptions.appendTo && document.querySelector(this.popperOptions.appendTo); if (appendToParent && this.elemRef.nativeElement.parentNode !== appendToParent) { this.elemRef.nativeElement.parentNode && this.elemRef.nativeElement.parentNode.removeChild(this.elemRef.nativeElement); appendToParent.appendChild(this.elemRef.nativeElement); } let popperOptions: Popper.PopperOptions = { placement: this.popperOptions.placement, positionFixed: this.popperOptions.positionFixed, modifiers: { arrow: { element: this.popperViewRef.nativeElement.querySelector('.ngxp__arrow') } } }; if (this.onUpdate) { popperOptions.onUpdate = this.onUpdate as any; } let boundariesElement = this.popperOptions.boundariesElement && document.querySelector(this.popperOptions.boundariesElement); if (popperOptions.modifiers && boundariesElement) { popperOptions.modifiers.preventOverflow = {boundariesElement}; } if (popperOptions.modifiers && this.popperOptions.preventOverflow !== undefined) { popperOptions.modifiers.preventOverflow = popperOptions.modifiers.preventOverflow || {}; popperOptions.modifiers.preventOverflow.enabled = this.popperOptions.preventOverflow; if (!popperOptions.modifiers.preventOverflow.enabled) { popperOptions.modifiers.hide = {enabled: false}; } } this.determineArrowColor(); popperOptions.modifiers = Object.assign(popperOptions.modifiers, this.popperOptions.popperModifiers); this.popperInstance = new Popper( this.referenceObject, this.popperViewRef.nativeElement, popperOptions, ); (this.popperInstance as any).enableEventListeners(); this.scheduleUpdate(); this.toggleVisibility(true); this.globalResize = this.renderer.listen('document', 'resize', this.onDocumentResize.bind(this)) } private determineArrowColor() { ['background-color', 'backgroundColor'].some((clr) => { if (!this.popperOptions.styles) { return false; } if (this.popperOptions.styles.hasOwnProperty(clr)) { this.arrowColor = this.popperOptions.styles[clr]; return true; } return false; }) } update(): void { this.popperInstance && (this.popperInstance as any).update(); } scheduleUpdate(): void { this.popperInstance && (this.popperInstance as any).scheduleUpdate(); } hide(): void { if (this.popperInstance) { this.popperInstance.destroy(); } this.toggleVisibility(false); this.onHidden.emit(); } toggleVisibility(state: boolean) { if (!state) { this.opacity = 0; this.displayType = "none"; this.ariaHidden = 'true'; this.state = false; } else { this.opacity = 1; this.displayType = "block"; this.ariaHidden = 'false'; this.state = true; } if (!this.CDR['destroyed']) { this.CDR.detectChanges(); } } extractAppliedClassListExpr(classList?: string): Object | null { if (!classList || typeof classList !== 'string') { return null; } try { return classList .replace(/ /, '') .split(',') .reduce((acc, clss) => { acc[clss] = true; return acc; }, {}) } catch (e) { return null; } } private clearGlobalResize() { this.globalResize && typeof this.globalResize === 'function' && this.globalResize(); } } ================================================ FILE: src/popper-directive.ts ================================================ import { Directive, ComponentRef, ViewContainerRef, ComponentFactoryResolver, Input, OnChanges, SimpleChange, Output, OnDestroy, EventEmitter, OnInit, Renderer2, ChangeDetectorRef, Inject, ElementRef } from '@angular/core'; import {Placement, Placements, PopperContentOptions, Trigger, Triggers} from './popper-model'; import {PopperContent} from './popper-content'; @Directive({ selector: '[popper]', exportAs: 'popper' }) export class PopperController implements OnInit, OnDestroy, OnChanges { private popperContentClass = PopperContent; private popperContentRef: ComponentRef; private shown: boolean = false; private scheduledShowTimeout: any; private scheduledHideTimeout: any; private subscriptions: any[] = []; private eventListeners: any[] = []; private globalEventListeners: any[] = []; private popperContent: PopperContent; constructor(private viewContainerRef: ViewContainerRef, private changeDetectorRef: ChangeDetectorRef, private resolver: ComponentFactoryResolver, private elementRef: ElementRef, private renderer: Renderer2, @Inject('popperDefaults') private popperDefaults: PopperContentOptions = {}) { PopperController.baseOptions = {...PopperController.baseOptions, ...this.popperDefaults}; } public static baseOptions: PopperContentOptions = { showDelay: 0, placement: Placements.Auto, hideOnClickOutside: true, hideOnMouseLeave: false, hideOnScroll: false, showTrigger: Triggers.HOVER, appendTo: undefined, ariaRole: 'popper', ariaDescribe: '', styles: {} }; @Input('popper') content: string | PopperContent; @Input('popperDisabled') disabled: boolean; @Input('popperPlacement') placement: Placement; @Input('popperTrigger') showTrigger: Trigger | undefined; @Input('popperTarget') targetElement: HTMLElement; @Input('popperDelay') showDelay: number | undefined; @Input('popperTimeout') hideTimeout: number = 0; @Input('popperTimeoutAfterShow') timeoutAfterShow: number = 0; @Input('popperBoundaries') boundariesElement: string; @Input('popperShowOnStart') showOnStart: boolean; @Input('popperCloseOnClickOutside') closeOnClickOutside: boolean; @Input('popperHideOnClickOutside') hideOnClickOutside: boolean | undefined; @Input('popperHideOnScroll') hideOnScroll: boolean | undefined; @Input('popperHideOnMouseLeave') hideOnMouseLeave: boolean | undefined; @Input('popperPositionFixed') positionFixed: boolean; @Input('popperModifiers') popperModifiers: {}; @Input('popperDisableStyle') disableStyle: boolean; @Input('popperDisableAnimation') disableAnimation: boolean; @Input('popperApplyClass') applyClass: string; @Input('popperApplyArrowClass') applyArrowClass: string; @Input('popperAriaDescribeBy') ariaDescribe: string | undefined; @Input('popperAriaRole') ariaRole: string | undefined; @Input('popperStyles') styles: Object | undefined; @Input('popperAppendTo') appendTo: string; @Input('popperPreventOverflow') preventOverflow: boolean; @Output() popperOnShown: EventEmitter = new EventEmitter(); @Output() popperOnHidden: EventEmitter = new EventEmitter(); @Output() popperOnUpdate: EventEmitter = new EventEmitter(); hideOnClickOutsideHandler($event: MouseEvent): void { if (this.disabled || !this.hideOnClickOutside || $event.srcElement && $event.srcElement === this.popperContent.elemRef.nativeElement || this.popperContent.elemRef.nativeElement.contains($event.srcElement)) { return; } this.scheduledHide($event, this.hideTimeout); } hideOnScrollHandler($event: MouseEvent): void { if (this.disabled || !this.hideOnScroll) { return; } this.scheduledHide($event, this.hideTimeout); } applyTriggerListeners() { switch (this.showTrigger) { case Triggers.CLICK: this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'click', this.toggle.bind(this))); break; case Triggers.MOUSEDOWN: this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'mousedown', this.toggle.bind(this))); break; case Triggers.HOVER: this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'mouseenter', this.scheduledShow.bind(this, this.showDelay))); this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'touchend', this.scheduledHide.bind(this, null, this.hideTimeout))); this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'touchcancel', this.scheduledHide.bind(this, null, this.hideTimeout))); this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'mouseleave', this.scheduledHide.bind(this, null, this.hideTimeout))); break; } if (this.showTrigger !== Triggers.HOVER && this.hideOnMouseLeave) { this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'touchend', this.scheduledHide.bind(this, null, this.hideTimeout))); this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'touchcancel', this.scheduledHide.bind(this, null, this.hideTimeout))); this.eventListeners.push(this.renderer.listen(this.elementRef.nativeElement, 'mouseleave', this.scheduledHide.bind(this, null, this.hideTimeout))); } } static assignDefined(target: any, ...sources: any[]) { for (const source of sources) { for (const key of Object.keys(source)) { const val = source[key]; if (val !== undefined) { target[key] = val; } } } return target; } ngOnInit() { //Support legacy prop this.hideOnClickOutside = typeof this.hideOnClickOutside === 'undefined' ? this.closeOnClickOutside : this.hideOnClickOutside; if (typeof this.content === 'string') { const text = this.content; this.popperContent = this.constructContent(); this.popperContent.text = text; } else { this.popperContent = this.content; } const popperRef = this.popperContent; popperRef.referenceObject = this.getRefElement(); this.setContentProperties(popperRef); this.setDefaults(); this.applyTriggerListeners(); if (this.showOnStart) { this.scheduledShow(); } } ngOnChanges(changes: { [propertyName: string]: SimpleChange }) { if (changes['popperDisabled'] && changes['popperDisabled'].currentValue) { this.hide(); } if (changes['content'] && !changes['content'].firstChange && typeof changes['content'].currentValue === 'string') { this.popperContent.text = changes['content'].currentValue; } if (changes['applyClass'] && !changes['applyClass'].firstChange && typeof changes['applyClass'].currentValue === 'string') { this.popperContent.popperOptions.applyClass = changes['applyClass'].currentValue; } if (changes['applyArrowClass'] && !changes['applyArrowClass'].firstChange && typeof changes['applyArrowClass'].currentValue === 'string') { this.popperContent.popperOptions.applyArrowClass = changes['applyArrowClass'].currentValue; } } ngOnDestroy() { this.subscriptions.forEach(sub => sub.unsubscribe && sub.unsubscribe()); this.subscriptions.length = 0; this.clearEventListeners(); this.clearGlobalEventListeners(); clearTimeout(this.scheduledShowTimeout); clearTimeout(this.scheduledHideTimeout); this.popperContent && this.popperContent.clean(); } toggle() { if (this.disabled) { return; } this.shown ? this.scheduledHide(null, this.hideTimeout) : this.scheduledShow(); } show() { if (this.shown) { this.overrideHideTimeout(); return; } this.shown = true; const popperRef = this.popperContent; const element = this.getRefElement(); if (popperRef.referenceObject !== element) { popperRef.referenceObject = element; } this.setContentProperties(popperRef); popperRef.show(); this.popperOnShown.emit(this); if (this.timeoutAfterShow > 0) { this.scheduledHide(null, this.timeoutAfterShow); } this.globalEventListeners.push(this.renderer.listen('document', 'touchend', this.hideOnClickOutsideHandler.bind(this))); this.globalEventListeners.push(this.renderer.listen('document', 'click', this.hideOnClickOutsideHandler.bind(this))); this.globalEventListeners.push(this.renderer.listen(this.getScrollParent(this.getRefElement()), 'scroll', this.hideOnScrollHandler.bind(this))); } hide() { if (this.disabled) { return; } if (!this.shown) { this.overrideShowTimeout(); return; } this.shown = false; if (this.popperContentRef) { this.popperContentRef.instance.hide(); } else { this.popperContent.hide(); } this.popperOnHidden.emit(this); this.clearGlobalEventListeners(); } scheduledShow(delay: number | undefined = this.showDelay) { if (this.disabled) { return; } this.overrideHideTimeout(); this.scheduledShowTimeout = setTimeout(() => { this.show(); this.applyChanges(); }, delay) } scheduledHide($event: any = null, delay: number = this.hideTimeout) { if (this.disabled) { return; } this.overrideShowTimeout(); this.scheduledHideTimeout = setTimeout(() => { const toElement = $event ? $event.toElement : null; const popperContentView = this.popperContent.popperViewRef ? this.popperContent.popperViewRef.nativeElement : false; if (!popperContentView || popperContentView === toElement || popperContentView.contains(toElement) || (this.content as PopperContent).isMouseOver) { return; } this.hide(); this.applyChanges(); }, delay); } getRefElement() { return this.targetElement || this.viewContainerRef.element.nativeElement; } private applyChanges() { this.changeDetectorRef.markForCheck(); this.changeDetectorRef.detectChanges(); } private setDefaults() { this.showDelay = typeof this.showDelay === 'undefined' ? PopperController.baseOptions.showDelay : this.showDelay; this.showTrigger = typeof this.showTrigger === 'undefined' ? PopperController.baseOptions.trigger : this.showTrigger; this.hideOnClickOutside = typeof this.hideOnClickOutside === 'undefined' ? PopperController.baseOptions.hideOnClickOutside : this.hideOnClickOutside; this.hideOnScroll = typeof this.hideOnScroll === 'undefined' ? PopperController.baseOptions.hideOnScroll : this.hideOnScroll; this.hideOnMouseLeave = typeof this.hideOnMouseLeave === 'undefined' ? PopperController.baseOptions.hideOnMouseLeave : this.hideOnMouseLeave; this.ariaRole = typeof this.ariaRole === 'undefined' ? PopperController.baseOptions.ariaRole : this.ariaRole; this.ariaDescribe = typeof this.ariaDescribe === 'undefined' ? PopperController.baseOptions.ariaDescribe : this.ariaDescribe; this.styles = typeof this.styles === 'undefined' ? Object.assign({}, PopperController.baseOptions.styles) : this.styles; } private clearEventListeners() { this.eventListeners.forEach(evt => { evt && typeof evt === 'function' && evt(); }); this.eventListeners.length = 0; } private clearGlobalEventListeners() { this.globalEventListeners.forEach(evt => { evt && typeof evt === 'function' && evt(); }); this.globalEventListeners.length = 0; } private overrideShowTimeout() { if (this.scheduledShowTimeout) { clearTimeout(this.scheduledShowTimeout); this.scheduledHideTimeout = 0; } } private overrideHideTimeout() { if (this.scheduledHideTimeout) { clearTimeout(this.scheduledHideTimeout); this.scheduledHideTimeout = 0; } } private constructContent(): PopperContent { const factory = this.resolver.resolveComponentFactory(this.popperContentClass); this.popperContentRef = this.viewContainerRef.createComponent(factory); return this.popperContentRef.instance as PopperContent; } private setContentProperties(popperRef: PopperContent) { popperRef.popperOptions = PopperController.assignDefined(popperRef.popperOptions, PopperController.baseOptions, { showDelay: this.showDelay, disableAnimation: this.disableAnimation, disableDefaultStyling: this.disableStyle, placement: this.placement, boundariesElement: this.boundariesElement, trigger: this.showTrigger, positionFixed: this.positionFixed, popperModifiers: this.popperModifiers, ariaDescribe: this.ariaDescribe, ariaRole: this.ariaRole, applyClass: this.applyClass, applyArrowClass: this.applyArrowClass, hideOnMouseLeave: this.hideOnMouseLeave, styles: this.styles, appendTo: this.appendTo, preventOverflow: this.preventOverflow, }); popperRef.onUpdate = this.onPopperUpdate.bind(this); this.subscriptions.push(popperRef.onHidden.subscribe(this.hide.bind(this))); } private getScrollParent(node) { const isElement = node instanceof HTMLElement; const overflowY = isElement && window.getComputedStyle(node).overflowY; const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden'; if (!node) { return null; } else if (isScrollable && node.scrollHeight >= node.clientHeight) { return node; } return this.getScrollParent(node.parentNode) || document; } private onPopperUpdate(event) { this.popperOnUpdate.emit(event); } } ================================================ FILE: src/popper-model.ts ================================================ export type Trigger = | 'click' | 'mousedown' | 'hover' | 'none' ; export class Triggers { static CLICK: Trigger = 'click'; static HOVER: Trigger = 'hover'; static MOUSEDOWN: Trigger = 'mousedown'; static NONE: Trigger = 'none'; } export type Placement = | 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'bottom-start' | 'left-start' | 'right-start' | 'top-end' | 'bottom-end' | 'left-end' | 'right-end' | 'auto' | 'auto-start' | 'auto-end' | Function export class Placements { static Top: Placement = 'top'; static Bottom: Placement = 'bottom'; static Left: Placement = 'left'; static Right: Placement = 'right'; static TopStart: Placement = 'top-start'; static BottomStart: Placement = 'bottom-start'; static LeftStart: Placement = 'left-start'; static RightStart: Placement = 'right-start'; static TopEnd: Placement = 'top-end'; static BottomEnd: Placement = 'bottom-end'; static LeftEnd: Placement = 'left-end'; static RightEnd: Placement = 'right-end'; static Auto: Placement = 'auto'; static AutoStart: Placement = 'auto-start'; static AutoEnd: Placement = 'auto-end'; } export interface PopperContentOptions { showDelay?: number; disableAnimation?: boolean; disableDefaultStyling?: boolean; placement?: Placement; boundariesElement?: string; trigger?: Trigger; positionFixed?: boolean; hideOnClickOutside?: boolean; hideOnMouseLeave?: boolean; hideOnScroll?: boolean; popperModifiers?: {}; ariaRole?: string; ariaDescribe?: string; applyClass?: string; applyArrowClass?: string; styles?: Object; appendTo?: string; preventOverflow?: boolean; } ================================================ FILE: src/popper.module.ts ================================================ import {CommonModule} from "@angular/common"; import {ModuleWithProviders, NgModule} from "@angular/core"; import {PopperContentOptions} from './popper-model'; import {PopperController} from './popper-directive'; import {PopperContent} from './popper-content'; @NgModule({ imports: [ CommonModule ], declarations: [ PopperController, PopperContent ], exports: [ PopperController, PopperContent ], entryComponents: [ PopperContent ] }) export class NgxPopperModule { ngDoBootstrap() { } public static forRoot(popperBaseOptions: PopperContentOptions = {}): ModuleWithProviders { return {ngModule: NgxPopperModule, providers: [{provide: 'popperDefaults', useValue: popperBaseOptions}]}; } } ================================================ FILE: teardown.js ================================================ const chalk = require('chalk'); const puppeteer = require('puppeteer'); const rimraf = require('rimraf'); const os = require('os'); const path = require('path'); const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); module.exports = async function() { console.log(chalk.green('Teardown Puppeteer')); await global.__BROWSER__.close(); rimraf.sync(DIR); }; ================================================ FILE: test/__tests__/basic.test.js ================================================ const utils = require('../utils'); const timeout = 20000; let testCounter = 0; jest.setTimeout(timeout); describe( '/basic tests', () => { let page; beforeEach(async () => { testCounter++; await page.evaluate((count) => { const elm = document.querySelector('test-app > div'); elm.id = `popper${count}`; elm.style.display = 'block'; const elm2 = document.querySelector('body > .ngxp__container'); elm2 && elm2.parentNode.removeChild(elm2); }, testCounter - 1) }); afterEach(async () => { await page.evaluate(() => { const elm = document.querySelector('test-app > div'); elm.parentNode.removeChild(elm); const elm2 = document.querySelector('body > .ngxp__container'); elm2 && elm2.parentNode.removeChild(elm2); }); }); beforeAll(async () => { page = await global.__BROWSER__.newPage(); await page.goto('http://localhost:8888'); await page.waitForSelector('popper-content'); console.log('page ready'); }, timeout); afterAll(async () => { await page.close() }); it('should show popper on start', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page); expect(popperText.trim()).toEqual('testing'); // let popperBox = await popper.boundingBox(); // let targetBox = await popperTarget.boundingBox(); // await page.evaluate(() => {debugger}); // expect((targetBox.y + targetBox.height) - popperBox.y).toEqual(18); }); it('should show popper on click', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.click(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page); const popperContainerBox = await utils.getPopperBoundingBox(page); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); expect(popperContainerBox.y - (targetBox.y + targetBox.height)).toEqual(5); }); it('should on right', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.click(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page); const popperContainerBox = await utils.getPopperBoundingBox(page); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); }); it('should open to top', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.click(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page); const popperContainerBox = await utils.getPopperBoundingBox(page); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); //await utils.pause(page); expect(targetBox.y - (popperContainerBox.y + popperContainerBox.height)).toEqual(5); }); it('left placement should flip to right', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.click(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page); const popperContainerBox = await utils.getPopperBoundingBox(page); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); }); it('should show/hover in hover', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.hover(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page); const popperContainerBox = await utils.getPopperBoundingBox(page); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); await page.hover('p'); await page.waitForSelector(`.ngxp__container`, { visible: false }); }); it('should not show popper with trigger none', async () => { await page.click(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.hover(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: false }); }); it('should show popper-content component on right', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.click(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page); const popperContainerBox = await utils.getPopperBoundingBox(page); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); }); it('should show popper-content component on right after delay', async () => { await page.waitForSelector(`.ngxp__container`, { visible: false }); await page.click(`.popperTarget`); await page.waitFor(4500); //utils.pause(page); await page.waitForSelector(`.ngxp__container`, { visible: true, timeout: 1000 }); const popperText = await utils.getPopperText(page); const popperContainerBox = await utils.getPopperBoundingBox(page); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); }); it('should show popper on the right and attached to body', async () => { // utils.pause(page); await page.waitForSelector(`popper-content`, { visible: false }); await page.hover(`.popperTarget`); await page.waitForSelector(`.ngxp__container`, { visible: true }); const popperText = await utils.getPopperText(page, 'body'); const popperContainerBox = await utils.getPopperBoundingBox(page, 'body'); const targetBox = await utils.getTargetBoundingBox(page); expect(popperText.trim()).toEqual('testing'); expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); }); // it('should show popper on the right with position fixed and attached to body', async () => { // await page.waitForSelector(`.ngxp__container`, { // visible: false // }); // await page.hover(`.popperTarget`); // await page.waitForSelector(`.ngxp__container`, { // visible: true // }); // const popper = await page.$('.ngxp__container'); // const popperText = await utils.getPopperText(page, 'body'); // const popperContainerBox = await utils.getPopperBoundingBox(page, 'body'); // const targetBox = await utils.getTargetBoundingBox(page); // expect(popperText.trim()).toEqual('testing'); // expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); // }) // // it('should show popper on the right with show on start and attached to body', async () => { // // await page.waitForSelector(`.ngxp__container`, { // visible: true // }); // const popperText = await utils.getPopperText(page, 'body'); // const popperContainerBox = await utils.getPopperBoundingBox(page, 'body'); // const targetBox = await utils.getTargetBoundingBox(page); // // expect(popperText.trim()).toEqual('testing'); // expect(popperContainerBox.x - (targetBox.x + targetBox.width)).toEqual(5); // }) } ); ================================================ FILE: test/app/app.component.css ================================================ test-app > div { display: none; } .popperTarget { display: inline-block; height: 50px; width: 50px; background: green; } popper-content .ngxp__container { background-color: blue; color: white; } ================================================ FILE: test/app/app.component.html ================================================

ngx-popper testing started...

popper
click
click
click
click
click
click
click
testing
click
click
================================================ FILE: test/app/app.component.ts ================================================ import {Component, ViewEncapsulation} from '@angular/core'; /** * This class represents the main application component. */ @Component({ selector: 'test-app', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], encapsulation: ViewEncapsulation.None }) export class AppComponent { constructor() { } } ================================================ FILE: test/app/app.module.ts ================================================ import {NgModule} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {BrowserModule} from '@angular/platform-browser'; import {AppComponent} from './app.component'; import {NgxPopperModule, Triggers} from '../../dist'; @NgModule({ imports: [ BrowserModule, FormsModule, NgxPopperModule.forRoot({ trigger: Triggers.CLICK, hideOnClickOutside: false })], declarations: [ AppComponent], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ================================================ FILE: test/app/index.ts ================================================ import 'zone.js'; import 'reflect-metadata'; import '@angular/common'; import '@angular/compiler'; import '@angular/core'; import '@angular/forms'; import '@angular/platform-browser-dynamic'; import '@angular/platform-browser'; import 'rxjs'; import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; // The app module import { AppModule } from './app.module'; enableProdMode(); platformBrowserDynamic().bootstrapModule(AppModule); ================================================ FILE: test/index.html ================================================ ngx-popper ================================================ FILE: test/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es5", "sourceMap": true, "declaration": true, "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "strictNullChecks": true, "skipLibCheck": true, "lib": [ "es7", "dom", "es2017.object", "es2015.iterable", "ScriptHost" ] }, "typeRoots": [ "../node_modules/@types", "../node_modules" ], "types": [ "node" ], "include": [ "." ], "exclude": [ "../node_modules", "../dist", "../dist-tsc", "../example", "../example-cli" ], "angularCompilerOptions": { "preserveWhiteSpace": false } } ================================================ FILE: test/utils.js ================================================ module.exports = { curPage: {}, getPopperText: async (page, parent) => { let popper = await page.$(parent || 'popper-content'); let popperInner = await popper.$('.ngxp__inner'); let popperText = await popperInner.getProperty('innerText'); return await popperText.jsonValue(); }, getPopperBoundingBox: async (page, parent) => { let popper = await page.$(parent || 'popper-content'); let popperContainer = await popper.$('.ngxp__container'); return await popperContainer.boundingBox(); }, getTargetBoundingBox: async (page) => { let popperTarget = await page.$('.popperTarget'); return await popperTarget.boundingBox(); }, pause: async (page) => { jest.setTimeout(500000); return await page.evaluate(() => { debugger; }); } }; ================================================ FILE: test/webpack.config.js ================================================ const path = require('path'); const {CheckerPlugin} = require('awesome-typescript-loader'); //const OpenBrowserPlugin = require('open-browser-webpack-plugin'); const chalk = require('chalk'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); module.exports = { entry: './test/app/index.ts', output: { filename: '[name].js', path: path.resolve(__dirname, '../test_dist') }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.html', '.css'] }, devtool: 'source-map', module: { rules: [ { test: /\.ts?$/, use: ['awesome-typescript-loader?configFileName="./tsconfig.json"', 'angular2-template-loader'], exclude: [/__tests__/] }, { test: /\.(html|css)$/, use: 'raw-loader', exclude: [/__tests__/] } ] }, plugins: [ new CheckerPlugin(), new HtmlWebpackPlugin({ inject: true, template: './test/index.html' }), new ProgressBarPlugin({ format: ' build [' + chalk.blue.bold(':bar') + ']' + chalk.green.bold(':percent') + ' (:elapsed seconds) => :msg... ', clear: false }), // new OpenBrowserPlugin({ // url: `http://localhost:8888` // }) ], devServer: { // https: true, //hot: true, stats: 'errors-only', port: 8888, inline: true, historyApiFallback: { index: './test/' }, open: false } }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist-tsc", "module": "commonjs", "target": "es5", "sourceMap": true, "declaration": true, "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "strictNullChecks": true, "skipLibCheck": true, "lib": [ "es7", "dom", "es2017.object", "es2015.iterable", "ScriptHost" ] }, "typeRoots": [ "./node_modules/@types", "./node_modules" ], "types": [ "node" ], "include": [ "src" ], "exclude": [ "node_modules", "dist", "dist-tsc", "test", "example", "example-cli" ], "angularCompilerOptions": { "preserveWhiteSpace": false } }