ngx-simple-modal
Version:
A simple unopinionated framework to implement simple modal based behaviour in angular (v2+) projects.
723 lines (711 loc) • 27.6 kB
JavaScript
import { __decorate, __assign, __spread, __param } from 'tslib';
import { CommonModule } from '@angular/common';
import { InjectionToken, ReflectiveInjector, ComponentFactoryResolver, ViewChild, ViewContainerRef, ElementRef, Component, EventEmitter, NgZone, Renderer2, Input, Output, Directive, Inject, ApplicationRef, Injector, Optional, Injectable, NgModule, HostListener } from '@angular/core';
import { Subject, fromEvent, of, BehaviorSubject, Observable } from 'rxjs';
import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
var DefaultSimpleModalOptionConfig = new InjectionToken('default-simple-modal.config');
var defaultSimpleModalOptions = {
closeOnEscape: false,
closeOnClickOutside: false,
bodyClass: 'modal-open',
wrapperDefaultClasses: 'modal fade-anim',
wrapperClass: 'in',
draggableClass: 'draggable',
animationDuration: 300,
autoFocus: false,
draggable: false,
};
/**
* The modal backdrop wrapping wrapper to the modal
*/
var SimpleModalWrapperComponent = /** @class */ (function () {
/**
* Constructor
* @param {ComponentFactoryResolver} resolver
*/
function SimpleModalWrapperComponent(resolver) {
this.resolver = resolver;
/**
* Wrapper modal and fade classes
*/
this.modalClasses = 'modal fade-anim';
}
/**
* Adds content modal component to wrapper
* @param {Type<SimpleModalComponent>} component
* @return {SimpleModalComponent}
*/
SimpleModalWrapperComponent.prototype.addComponent = function (component) {
var factory = this.resolver.resolveComponentFactory(component);
var injector = ReflectiveInjector.fromResolvedProviders([], this.viewContainer.injector);
var componentRef = factory.create(injector);
this.viewContainer.insert(componentRef.hostView);
this.content = componentRef.instance;
this.content.wrapper = this.wrapper;
return { ref: componentRef, component: this.content };
};
/**
* Configures the function to call when you click on background of a modal but not the contents
* @param callback
*/
SimpleModalWrapperComponent.prototype.onClickOutsideModalContent = function (callback) {
var containerEl = this.wrapper.nativeElement;
this.clickOutsideCallback = function (event) {
if (event.target === containerEl) {
callback();
}
};
containerEl.addEventListener('click', this.clickOutsideCallback, false);
};
SimpleModalWrapperComponent.prototype.ngOnDestroy = function () {
if (this.clickOutsideCallback) {
var containerEl = this.wrapper.nativeElement;
containerEl.removeEventListener('click', this.clickOutsideCallback, false);
this.clickOutsideCallback = null;
}
};
SimpleModalWrapperComponent.ctorParameters = function () { return [
{ type: ComponentFactoryResolver }
]; };
__decorate([
ViewChild('viewContainer', { read: ViewContainerRef, static: true })
], SimpleModalWrapperComponent.prototype, "viewContainer", void 0);
__decorate([
ViewChild('wrapper', { read: ElementRef, static: true })
], SimpleModalWrapperComponent.prototype, "wrapper", void 0);
SimpleModalWrapperComponent = __decorate([
Component({
selector: 'simple-modal-wrapper',
template: "\n <div #wrapper [ngClass]=\"modalClasses\" [ngStyle]=\"{ display: 'block' }\" role=\"dialog\">\n <ng-template #viewContainer></ng-template>\n </div>\n "
})
], SimpleModalWrapperComponent);
return SimpleModalWrapperComponent;
}());
var DraggableDirective = /** @class */ (function () {
function DraggableDirective(host, zone, renderer) {
this.host = host;
this.zone = zone;
this.renderer = renderer;
this.dragEnabled = false;
this.dragged = new EventEmitter();
this.delta = { x: 0, y: 0 };
this.offset = { x: 0, y: 0 };
this.enabled = true;
this.destroy$ = new Subject();
}
Object.defineProperty(DraggableDirective.prototype, "dialogDragOffset", {
set: function (offset) {
this.reset(offset);
},
enumerable: true,
configurable: true
});
DraggableDirective.prototype.ngAfterViewInit = function () {
if (!this.enabled) {
return;
}
this.init();
};
DraggableDirective.prototype.ngOnChanges = function () {
if (!this.enabled && this.dragEnabled && this.dragTarget) {
this.enabled = true;
/** determine if the component has been init by the handle variable */
if (this.handle) {
this.renderer.setStyle(this.handle, 'cursor', 'move');
}
else if (this.enabled) {
this.init();
}
}
if (!this.dragEnabled) {
this.enabled = false;
if (this.handle) {
this.renderer.setStyle(this.handle, 'cursor', '');
}
}
};
DraggableDirective.prototype.ngOnDestroy = function () {
this.destroy$.next();
};
DraggableDirective.prototype.reset = function (offset) {
var defaultValues = { x: 0, y: 0 };
this.offset = __assign({}, defaultValues, offset);
this.delta = __assign({}, defaultValues);
this.translate();
};
DraggableDirective.prototype.setupEvents = function () {
var _this = this;
this.zone.runOutsideAngular(function () {
var mousedown$ = fromEvent(_this.handle, 'mousedown');
var mousemove$ = fromEvent(document, 'mousemove');
var mouseup$ = fromEvent(document, 'mouseup');
var mousedrag$ = mousedown$.pipe(filter(function () { return _this.enabled; }), map(function (event) { return ({
startX: event.clientX,
startY: event.clientY
}); }), switchMap(function (_a) {
var startX = _a.startX, startY = _a.startY;
return mousemove$.pipe(map(function (event) {
event.preventDefault();
_this.delta = {
x: event.clientX - startX,
y: event.clientY - startY
};
}), takeUntil(mouseup$));
}), takeUntil(_this.destroy$));
mousedrag$.subscribe(function () {
if (_this.delta.x === 0 && _this.delta.y === 0) {
return;
}
_this.translate();
});
mouseup$
.pipe(filter(function () { return _this.enabled; }),
/** Only emit change if the element has moved */
filter(function () { return _this.delta.x !== 0 || _this.delta.y !== 0; }), takeUntil(_this.destroy$))
.subscribe(function () {
_this.offset.x += _this.delta.x;
_this.offset.y += _this.delta.y;
_this.dragged.emit(_this.offset);
_this.delta = { x: 0, y: 0 };
});
});
};
DraggableDirective.prototype.translate = function () {
var _this = this;
if (this.target) {
this.zone.runOutsideAngular(function () {
requestAnimationFrame(function () {
var transform = "translate(" + (_this.offset.x + _this.delta.x) + "px, " + (_this.offset.y + _this.delta.y) + "px)";
_this.renderer.setStyle(_this.target, 'transform', transform);
});
});
}
};
/**
* Init the directive
*/
DraggableDirective.prototype.init = function () {
if (!this.dragTarget) {
throw new Error('You need to specify the drag target');
}
this.handle =
this.dragHandle instanceof Element
? this.dragHandle
: typeof this.dragHandle === 'string' && this.dragHandle
? document.querySelector(this.dragHandle)
: this.host.nativeElement;
/** add the move cursor */
if (this.handle && this.enabled) {
this.renderer.addClass(this.handle, 'handle');
}
this.target =
this.dragTarget instanceof HTMLElement
? this.dragTarget
: document.querySelector(this.dragTarget);
this.setupEvents();
this.translate();
};
DraggableDirective.ctorParameters = function () { return [
{ type: ElementRef },
{ type: NgZone },
{ type: Renderer2 }
]; };
__decorate([
Input()
], DraggableDirective.prototype, "dragHandle", void 0);
__decorate([
Input()
], DraggableDirective.prototype, "dragTarget", void 0);
__decorate([
Input()
], DraggableDirective.prototype, "dragEnabled", void 0);
__decorate([
Input()
], DraggableDirective.prototype, "dialogDragOffset", null);
__decorate([
Output()
], DraggableDirective.prototype, "dragged", void 0);
DraggableDirective = __decorate([
Directive({
selector: '[dialogDraggable]'
})
], DraggableDirective);
return DraggableDirective;
}());
/**
* View container manager which manages a list of modals currently active
* inside the viewvontainer
*/
var SimpleModalHolderComponent = /** @class */ (function () {
/**
* Constructor
* @param {ComponentFactoryResolver} resolver
* @param renderer
* @param ngZone
* @param defaultSimpleModalOptions
*/
function SimpleModalHolderComponent(resolver, renderer, ngZone, defaultSimpleModalOptions) {
this.resolver = resolver;
this.renderer = renderer;
this.ngZone = ngZone;
this.defaultSimpleModalOptions = defaultSimpleModalOptions;
/**
* modal collection, maintained by addModal and removeModal
* @type {Array<SimpleModalComponent> }
*/
this.modals = [];
/**
* if auto focus is on and no element focused, store it here to be restored back after close
*/
this.previousActiveElement = null;
}
/**
* Configures then adds modal to the modals array, and populates with data passed in
* @param {Type<SimpleModalComponent>} component
* @param {object?} data
* @param {SimpleModalOptionsOverrides?} options
* @return {Observable<*>}
*/
SimpleModalHolderComponent.prototype.addModal = function (component, data, options) {
var _this = this;
// create component
if (!this.viewContainer) {
return of(null);
}
var factory = this.resolver.resolveComponentFactory(SimpleModalWrapperComponent);
var componentRef = this.viewContainer.createComponent(factory);
var modalWrapper = (componentRef.instance);
var _a = modalWrapper.addComponent(component), _componentRef = _a.ref, _component = _a.component;
// assign options refs
_component.options = options = Object.assign({}, this.defaultSimpleModalOptions, options);
// set base classes for wrapper
modalWrapper.modalClasses = options.wrapperDefaultClasses;
// add to stack
this.modals.push(_component);
// wait a tick then setup the following while adding a modal
this.wait().then(function () {
_this.toggleWrapperClass(modalWrapper.wrapper, options.wrapperClass);
_this.toggleBodyClass(options.bodyClass);
if (options.draggable) {
_this.setDraggable(_componentRef, options);
}
_this.wait(options.animationDuration).then(function () {
_this.autoFocusFirstElement(_component.wrapper, options.autoFocus);
_component.markAsReady();
});
});
// when closing modal remove it
_component.onClosing(function (modal) { return _this.removeModal(modal); });
// if clicking on background closes modal
this.configureCloseOnClickOutside(modalWrapper);
// map and return observable
_component.mapDataObject(data);
return _component.setupObserver();
};
/**
* triggers components close function
* to take effect
* @returns {Promise<void>}
* @param closingModal
*/
SimpleModalHolderComponent.prototype.removeModal = function (closingModal) {
var _this = this;
var options = closingModal.options;
this.toggleWrapperClass(closingModal.wrapper, options.wrapperClass);
return this.wait(options.animationDuration).then(function () {
_this.removeModalFromArray(closingModal);
_this.toggleBodyClass(options.bodyClass);
_this.restorePreviousFocus();
});
};
/**
* Instructs all open modals to
*/
SimpleModalHolderComponent.prototype.removeAllModals = function () {
var _this = this;
return Promise.all(this.modals.map(function (modal) { return _this.removeModal(modal); }));
};
/**
* Bind a body class 'modal-open' to a condition of modals in pool > 0
* @param bodyClass - string to add and remove from body in document
*/
SimpleModalHolderComponent.prototype.toggleBodyClass = function (bodyClass) {
var _a, _b;
if (!bodyClass) {
return;
}
var body = document.getElementsByTagName('body')[0];
var bodyClassItems = bodyClass.split(' ');
if (!this.modals.length) {
(_a = body.classList).remove.apply(_a, __spread(bodyClassItems));
}
else {
(_b = body.classList).add.apply(_b, __spread(bodyClassItems));
}
};
/**
* if the option to close on background click is set, then hook up a callback
* @param modalWrapper
*/
SimpleModalHolderComponent.prototype.configureCloseOnClickOutside = function (modalWrapper) {
modalWrapper.onClickOutsideModalContent(function () {
if (modalWrapper.content.options.closeOnClickOutside) {
modalWrapper.content.close();
}
});
};
/**
* Auto focus o the first element if autofocus is on
* @param componentWrapper
* @param autoFocus
*/
SimpleModalHolderComponent.prototype.autoFocusFirstElement = function (componentWrapper, autoFocus) {
if (autoFocus) {
var focusable = componentWrapper.nativeElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable && focusable.length) {
this.previousActiveElement = document.activeElement;
focusable[0].focus();
}
}
};
/**
* Restores the last focus is there was one
*/
SimpleModalHolderComponent.prototype.restorePreviousFocus = function () {
if (this.previousActiveElement) {
this.previousActiveElement.focus();
this.previousActiveElement = null;
}
};
/**
* Configure the adding and removal of a wrapper class - predominantly animation focused
* @param modalWrapperEl
* @param wrapperClass
*/
SimpleModalHolderComponent.prototype.toggleWrapperClass = function (modalWrapperEl, wrapperClass) {
var wrapperClassList = modalWrapperEl.nativeElement.classList;
var wrapperClassItems = wrapperClass.split(' ');
if (wrapperClassList.toString().indexOf(wrapperClass) !== -1) {
wrapperClassList.remove.apply(wrapperClassList, __spread(wrapperClassItems));
}
else {
wrapperClassList.add.apply(wrapperClassList, __spread(wrapperClassItems));
}
};
/**
* Enables the drag option on the modal if the options have it enabled
* @param component
* @param options
* @private
*/
SimpleModalHolderComponent.prototype.setDraggable = function (component, options) {
var draggableDirective = new DraggableDirective(component.location, this.ngZone, this.renderer);
draggableDirective.dragTarget = component.location.nativeElement;
draggableDirective.dragHandle = component.instance.handle ? component.instance.handle.nativeElement : undefined;
draggableDirective.ngAfterViewInit();
component.location.nativeElement.classList.add(options.draggableClass);
};
/**
* Helper function for a more readable timeout
* @param ms
*/
SimpleModalHolderComponent.prototype.wait = function (ms) {
if (ms === void 0) { ms = 0; }
return new Promise(function (resolve, reject) {
setTimeout(function () { return resolve(); }, ms);
});
};
/**
* Instructs the holder to remove the modal and
* removes this component from the collection
* @param {SimpleModalComponent} component
*/
SimpleModalHolderComponent.prototype.removeModalFromArray = function (component) {
var index = this.modals.indexOf(component);
if (index > -1) {
this.viewContainer.remove(index);
this.modals.splice(index, 1);
}
};
SimpleModalHolderComponent.ctorParameters = function () { return [
{ type: ComponentFactoryResolver },
{ type: Renderer2 },
{ type: NgZone },
{ type: undefined, decorators: [{ type: Inject, args: [DefaultSimpleModalOptionConfig,] }] }
]; };
__decorate([
ViewChild('viewContainer', { read: ViewContainerRef, static: true })
], SimpleModalHolderComponent.prototype, "viewContainer", void 0);
SimpleModalHolderComponent = __decorate([
Component({
selector: 'simple-modal-holder',
template: '<ng-template #viewContainer></ng-template>'
}),
__param(3, Inject(DefaultSimpleModalOptionConfig))
], SimpleModalHolderComponent);
return SimpleModalHolderComponent;
}());
var SimpleModalServiceConfig = /** @class */ (function () {
function SimpleModalServiceConfig() {
this.container = null;
}
return SimpleModalServiceConfig;
}());
var SimpleModalService = /** @class */ (function () {
/**
* @param {ComponentFactoryResolver} resolver
* @param {ApplicationRef} applicationRef
* @param {Injector} injector
* @param {SimpleModalServiceConfig} config
*/
function SimpleModalService(resolver, applicationRef, injector, config) {
this.resolver = resolver;
this.applicationRef = applicationRef;
this.injector = injector;
if (config) {
this.container = config.container;
}
}
/**
* Adds modal
* @param {Type<SimpleModalComponent<T, T1>>} component
* @param {T?} data
* @param {SimpleModalOptionsOverrides?} options
* @return {Observable<T1>}
*/
SimpleModalService.prototype.addModal = function (component, data, options) {
if (!this.modalHolderComponent) {
this.modalHolderComponent = this.createSimpleModalHolder();
}
return this.modalHolderComponent.addModal(component, data, options);
};
/**
* Hides and removes modal from DOM, resolves promise when fully removed
* @param {SimpleModalComponent} component
* @return {Promise<{}>}
*/
SimpleModalService.prototype.removeModal = function (component) {
if (!this.modalHolderComponent) {
return Promise.resolve({});
}
return this.modalHolderComponent.removeModal(component);
};
/**
* Closes all modals, resolves promise when they're fully removed
* @return {Promise<{}>}
*/
SimpleModalService.prototype.removeAll = function () {
if (!this.modalHolderComponent) {
return Promise.resolve({});
}
return this.modalHolderComponent.removeAllModals();
};
Object.defineProperty(SimpleModalService.prototype, "container", {
get: function () {
if (typeof this._container === 'string') {
this._container = document.getElementById(this._container);
}
if (!this._container && this.applicationRef['components'].length) {
var componentRootViewContainer = this.applicationRef['components'][0];
this.container = componentRootViewContainer.hostView
.rootNodes[0];
}
// fallback
if (!this._container || typeof this._container === 'string') {
this._container = document.getElementsByTagName('body')[0];
}
return this._container;
},
/**
* Accessor for contain - will auto generate from string
* if needed or default to the root element if nothing was set
*/
set: function (c) {
this._container = c;
},
enumerable: true,
configurable: true
});
/**
* Creates and add to DOM modal holder component
* @return {SimpleModalHolderComponent}
*/
SimpleModalService.prototype.createSimpleModalHolder = function () {
var _this = this;
var componentFactory = this.resolver.resolveComponentFactory(SimpleModalHolderComponent);
var componentRef = componentFactory.create(this.injector);
var componentRootNode = componentRef.hostView
.rootNodes[0];
this.applicationRef.attachView(componentRef.hostView);
componentRef.onDestroy(function () {
_this.applicationRef.detachView(componentRef.hostView);
});
this.container.appendChild(componentRootNode);
return componentRef.instance;
};
SimpleModalService.ctorParameters = function () { return [
{ type: ComponentFactoryResolver },
{ type: ApplicationRef },
{ type: Injector },
{ type: SimpleModalServiceConfig, decorators: [{ type: Optional }] }
]; };
SimpleModalService = __decorate([
Injectable(),
__param(3, Optional())
], SimpleModalService);
return SimpleModalService;
}());
/**
* Modal service factory. Creates modal service with options
* @param { ComponentFactoryResolver } resolver
* @param { ApplicationRef } applicationRef
* @param { Injector } injector
* @param { SimpleModalServiceConfig } options
* @return { SimpleModalService }
*/
function SimpleModalServiceFactory(resolver, applicationRef, injector, options) {
return new SimpleModalService(resolver, applicationRef, injector, options);
}
var ɵ0 = defaultSimpleModalOptions;
var SimpleModalModule = /** @class */ (function () {
function SimpleModalModule() {
}
SimpleModalModule_1 = SimpleModalModule;
SimpleModalModule.forRoot = function (config, defaultModalOptions) {
return {
ngModule: SimpleModalModule_1,
providers: [
{ provide: SimpleModalServiceConfig, useValue: config },
{
provide: SimpleModalService,
useFactory: SimpleModalServiceFactory,
deps: [ComponentFactoryResolver, ApplicationRef, Injector, SimpleModalServiceConfig],
},
{
provide: DefaultSimpleModalOptionConfig,
useValue: defaultModalOptions || defaultSimpleModalOptions,
}
],
};
};
var SimpleModalModule_1;
SimpleModalModule = SimpleModalModule_1 = __decorate([
NgModule({
declarations: [SimpleModalHolderComponent, SimpleModalWrapperComponent, DraggableDirective],
providers: [
SimpleModalService,
{
provide: DefaultSimpleModalOptionConfig,
useValue: ɵ0,
},
],
imports: [CommonModule],
entryComponents: [SimpleModalHolderComponent, SimpleModalWrapperComponent],
})
], SimpleModalModule);
return SimpleModalModule;
}());
/**
* Abstract modal
* @template T - modal data;
* @template T1 - modal result
*/
var SimpleModalComponent = /** @class */ (function () {
/**
* Constructor
*/
function SimpleModalComponent() {
/**
* ready$ is when all animations and focusing have comleted
*/
this._ready$ = new BehaviorSubject(false);
/**
* Callback to the holders close function
*/
this.closerCallback = function () { return Promise.resolve(); };
}
/**
* Maps your object passed in the creation to fields in your own Dialog classes
* @param {T} data
*/
SimpleModalComponent.prototype.mapDataObject = function (data) {
data = data || {};
var keys = Object.keys(data);
for (var i = 0, length_1 = keys.length; i < length_1; i++) {
var key = keys[i];
if (data[key] &&
this[key] &&
typeof data[key] === 'object' &&
typeof this[key] === 'object') {
Object.assign(this[key], data[key]);
}
else {
this[key] = data[key];
}
}
};
/**
* Setup observer
* @return {Observable<T1>}
*/
SimpleModalComponent.prototype.setupObserver = function () {
var _this = this;
return Observable.create(function (observer) {
_this.observer = observer;
// called if observable is unsubscribed to
return function () {
_this.close();
};
});
};
/**
* Defines what happens when close is called - default this
* will just call the default remove modal process. If overridden
* must include
* @param callback
*/
SimpleModalComponent.prototype.onClosing = function (callback) {
this.closerCallback = callback;
};
/**
* Closes modal
*/
SimpleModalComponent.prototype.close = function () {
var _this = this;
return this.closerCallback(this).then(function (v) {
if (_this.observer) {
_this.observer.next(_this.result);
_this.observer.complete();
}
return v;
});
};
/**
* keypress binding ngx way
* @param evt
*/
SimpleModalComponent.prototype.onKeydownHandler = function (evt) {
if (this.options && this.options.closeOnEscape) {
this.close();
}
};
Object.defineProperty(SimpleModalComponent.prototype, "ready$", {
get: function () {
return this._ready$.asObservable();
},
enumerable: true,
configurable: true
});
SimpleModalComponent.prototype.markAsReady = function () {
this._ready$.next(true);
};
__decorate([
HostListener('document:keydown.escape', ['$event'])
], SimpleModalComponent.prototype, "onKeydownHandler", null);
return SimpleModalComponent;
}());
/**
* Generated bundle index. Do not edit.
*/
export { DefaultSimpleModalOptionConfig, DraggableDirective, SimpleModalComponent, SimpleModalModule, SimpleModalService, SimpleModalServiceConfig, defaultSimpleModalOptions, SimpleModalHolderComponent as ɵa, SimpleModalWrapperComponent as ɵb, SimpleModalServiceFactory as ɵc };
//# sourceMappingURL=ngx-simple-modal.js.map