mirror of https://github.com/Squidex/squidex.git
39 changed files with 694 additions and 657 deletions
@ -1,28 +1,31 @@ |
|||
<sqx-modal-dialog *sqxModalView="dueTimeDialog;onRoot:true" (close)="cancelStatusChange()"> |
|||
<ng-container title> |
|||
{{dueTimeAction}} content item(s) |
|||
</ng-container> |
|||
<sqx-modal [model]="dueTimeDialog"> |
|||
<sqx-modal-dialog (close)="cancelStatusChange()"> |
|||
<ng-container title> |
|||
{{dueTimeAction}} content item(s) |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode"> |
|||
<label class="form-check-label" for="immediately"> |
|||
{{dueTimeAction}} content item(s) immediately. |
|||
</label> |
|||
</div> |
|||
|
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode"> |
|||
<label class="form-check-label" for="scheduled"> |
|||
{{dueTimeAction}} content item(s) at a later point date and time. |
|||
</label> |
|||
</div> |
|||
|
|||
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor> |
|||
</ng-container> |
|||
|
|||
<ng-container footer> |
|||
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> |
|||
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>Confirm</button> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
|
|||
<ng-container content> |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode"> |
|||
<label class="form-check-label" for="immediately"> |
|||
{{dueTimeAction}} content item(s) immediately. |
|||
</label> |
|||
</div> |
|||
|
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode"> |
|||
<label class="form-check-label" for="scheduled"> |
|||
{{dueTimeAction}} content item(s) at a later point date and time. |
|||
</label> |
|||
</div> |
|||
|
|||
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor> |
|||
</ng-container> |
|||
|
|||
<ng-container footer> |
|||
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> |
|||
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>Confirm</button> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
</sqx-modal> |
|||
|
|||
@ -1,114 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, Renderer2 } from '@angular/core'; |
|||
import { timer } from 'rxjs'; |
|||
|
|||
import { positionModal, ResourceOwner } from '@app/framework/internal'; |
|||
|
|||
@Directive({ |
|||
selector: '[sqxModalTarget]' |
|||
}) |
|||
export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy { |
|||
private targetElement: Element; |
|||
|
|||
@Input('sqxModalTarget') |
|||
public set target(element: Element) { |
|||
if (element !== this.targetElement) { |
|||
this.unsubscribeAll(); |
|||
|
|||
this.targetElement = element; |
|||
|
|||
if (element) { |
|||
this.subscribe(element); |
|||
this.updatePosition(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Input() |
|||
public offset = 2; |
|||
|
|||
@Input() |
|||
public position = 'bottom-right'; |
|||
|
|||
@Input() |
|||
public autoPosition = true; |
|||
|
|||
constructor( |
|||
private readonly renderer: Renderer2, |
|||
private readonly element: ElementRef<Element> |
|||
) { |
|||
super(); |
|||
} |
|||
|
|||
private subscribe(element: any) { |
|||
this.own( |
|||
this.renderer.listen(element, 'resize', () => { |
|||
this.updatePosition(); |
|||
})); |
|||
|
|||
this.own( |
|||
this.renderer.listen(this.element.nativeElement, 'resize', () => { |
|||
this.updatePosition(); |
|||
})); |
|||
|
|||
this.own(timer(100, 100).subscribe(() => this.updatePosition())); |
|||
} |
|||
|
|||
public ngAfterViewInit() { |
|||
const modalRef = this.element.nativeElement; |
|||
|
|||
this.renderer.setStyle(modalRef, 'position', 'fixed'); |
|||
this.renderer.setStyle(modalRef, 'z-index', '1000000'); |
|||
|
|||
this.updatePosition(); |
|||
} |
|||
|
|||
private updatePosition() { |
|||
if (!this.targetElement) { |
|||
return; |
|||
} |
|||
|
|||
const modalRef = this.element.nativeElement; |
|||
const modalRect = this.element.nativeElement.getBoundingClientRect(); |
|||
|
|||
if (modalRect.width === 0 || modalRect.height === 0) { |
|||
return; |
|||
} |
|||
|
|||
const targetRect = this.targetElement.getBoundingClientRect(); |
|||
|
|||
let y = 0; |
|||
let x = 0; |
|||
|
|||
if (this.position === 'full') { |
|||
x = -this.offset + targetRect.left; |
|||
y = -this.offset + targetRect.top; |
|||
|
|||
const w = 2 * this.offset + targetRect.width; |
|||
const h = 2 * this.offset + targetRect.height; |
|||
|
|||
this.renderer.setStyle(modalRef, 'width', `${w}px`); |
|||
this.renderer.setStyle(modalRef, 'height', `${h}px`); |
|||
} else { |
|||
const viewH = document.documentElement!.clientHeight; |
|||
const viewW = document.documentElement!.clientWidth; |
|||
|
|||
const position = positionModal(targetRect, modalRect, this.position, this.offset, this.autoPosition, viewW, viewH); |
|||
|
|||
x = position.x; |
|||
y = position.y; |
|||
} |
|||
|
|||
this.renderer.setStyle(modalRef, 'top', `${y}px`); |
|||
this.renderer.setStyle(modalRef, 'left', `${x}px`); |
|||
this.renderer.setStyle(modalRef, 'right', 'auto'); |
|||
this.renderer.setStyle(modalRef, 'bottom', 'auto'); |
|||
this.renderer.setStyle(modalRef, 'margin', '0'); |
|||
} |
|||
} |
|||
@ -1,157 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ChangeDetectorRef, Directive, EmbeddedViewRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core'; |
|||
import { Subscription } from 'rxjs'; |
|||
|
|||
import { |
|||
DialogModel, |
|||
ModalModel, |
|||
Types |
|||
} from '@app/framework/internal'; |
|||
|
|||
import { RootViewComponent } from './root-view.component'; |
|||
|
|||
@Directive({ |
|||
selector: '[sqxModalView]' |
|||
}) |
|||
export class ModalViewDirective implements OnChanges, OnDestroy { |
|||
private modalSubscription: Subscription | null = null; |
|||
private renderedView: EmbeddedViewRef<any> | null = null; |
|||
|
|||
@Input('sqxModalView') |
|||
public modalView: DialogModel | ModalModel | any; |
|||
|
|||
@Input('sqxModalViewOnRoot') |
|||
public placeOnRoot = false; |
|||
|
|||
@Input('sqxModalViewCloseAuto') |
|||
public closeAuto = true; |
|||
|
|||
@Input('sqxModalViewCloseAlways') |
|||
public closeAlways = false; |
|||
|
|||
constructor( |
|||
private readonly changeDetector: ChangeDetectorRef, |
|||
private readonly renderer: Renderer2, |
|||
private readonly rootView: RootViewComponent, |
|||
private readonly templateRef: TemplateRef<any>, |
|||
private readonly viewContainer: ViewContainerRef |
|||
) { |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeToModal(); |
|||
this.unsubscribeToClick(); |
|||
|
|||
if (Types.is(this.modalView, DialogModel) || Types.is(this.modalView, ModalModel)) { |
|||
this.modalView.hide(); |
|||
} |
|||
} |
|||
|
|||
public ngOnChanges(changes: SimpleChanges) { |
|||
if (!changes['modalView']) { |
|||
return; |
|||
} |
|||
|
|||
this.unsubscribeToModal(); |
|||
|
|||
if (Types.is(this.modalView, DialogModel) || Types.is(this.modalView, ModalModel)) { |
|||
this.modalSubscription = |
|||
this.modalView.isOpen.subscribe(isOpen => { |
|||
this.update(isOpen); |
|||
}); |
|||
} else { |
|||
this.update(!!this.modalView); |
|||
} |
|||
} |
|||
|
|||
private update(isOpen: boolean) { |
|||
if (isOpen === (!!this.renderedView)) { |
|||
return; |
|||
} |
|||
|
|||
this.unsubscribeToClick(); |
|||
|
|||
if (isOpen && !this.renderedView) { |
|||
const container = this.getContainer(); |
|||
|
|||
this.renderedView = container.createEmbeddedView(this.templateRef); |
|||
|
|||
if (this.renderedView.rootNodes[0].style) { |
|||
this.renderer.setStyle(this.renderedView.rootNodes[0], 'display', 'block'); |
|||
} |
|||
|
|||
this.startListening(); |
|||
|
|||
this.changeDetector.detectChanges(); |
|||
} else if (!isOpen && this.renderedView) { |
|||
const container = this.getContainer(); |
|||
const containerIndex = container.indexOf(this.renderedView); |
|||
|
|||
container.remove(containerIndex); |
|||
|
|||
this.renderedView = null; |
|||
|
|||
this.changeDetector.detectChanges(); |
|||
} |
|||
} |
|||
|
|||
private getContainer() { |
|||
return this.placeOnRoot ? this.rootView.viewContainer : this.viewContainer; |
|||
} |
|||
|
|||
private startListening() { |
|||
if (this.closeAuto) { |
|||
document.addEventListener('click', this.documentClickListener, true); |
|||
} |
|||
} |
|||
|
|||
private documentClickListener = (event: MouseEvent) => { |
|||
if (!event.target || this.renderedView === null) { |
|||
return; |
|||
} |
|||
|
|||
if (this.renderedView.rootNodes.length === 0) { |
|||
return; |
|||
} |
|||
|
|||
if (this.closeAlways) { |
|||
const modal = this.modalView; |
|||
|
|||
setTimeout(() => { |
|||
modal.hide(); |
|||
}, 100); |
|||
} else { |
|||
try { |
|||
const rootNode = this.renderedView.rootNodes[0]; |
|||
const rootBounds = rootNode.getBoundingClientRect(); |
|||
|
|||
if (rootBounds.width > 0 && rootBounds.height > 0) { |
|||
const clickedInside = rootNode.contains(event.target); |
|||
|
|||
if (!clickedInside && this.modalView) { |
|||
this.modalView.hide(); |
|||
} |
|||
} |
|||
} catch (ex) { |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private unsubscribeToModal() { |
|||
if (this.modalSubscription) { |
|||
this.modalSubscription.unsubscribe(); |
|||
this.modalSubscription = null; |
|||
} |
|||
} |
|||
|
|||
private unsubscribeToClick() { |
|||
document.removeEventListener('click', this.documentClickListener); |
|||
} |
|||
} |
|||
@ -0,0 +1,254 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AfterViewInit, ChangeDetectorRef, Component, EmbeddedViewRef, Input, OnDestroy, Renderer2, TemplateRef, ViewChild } from '@angular/core'; |
|||
import { timer } from 'rxjs'; |
|||
|
|||
import { |
|||
DialogModel, |
|||
ModalModel, |
|||
positionModal, |
|||
ResourceOwner, |
|||
Types |
|||
} from '@app/framework/internal'; |
|||
|
|||
import { RootViewComponent } from './root-view.component'; |
|||
|
|||
declare type Model = DialogModel | ModalModel | any; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-modal', |
|||
template: ` |
|||
<ng-template #templatePortalContent> |
|||
<ng-content></ng-content> |
|||
</ng-template> |
|||
` |
|||
}) |
|||
export class ModalComponent implements AfterViewInit, OnDestroy { |
|||
private readonly eventsView = new ResourceOwner(); |
|||
private readonly eventsModel = new ResourceOwner(); |
|||
private currentTarget: Element | null = null; |
|||
private currentModel: DialogModel | ModalModel | null = null; |
|||
private renderedView: EmbeddedViewRef<any> | null = null; |
|||
private renderRoot: HTMLElement | null = null; |
|||
private isOpen: boolean; |
|||
|
|||
@Input() |
|||
public target: Element; |
|||
|
|||
@Input() |
|||
public set model(value: Model) { |
|||
if (this.currentModel !== value) { |
|||
this.currentModel = value; |
|||
|
|||
this.eventsModel.unsubscribeAll(); |
|||
|
|||
this.subscribeToModel(value); |
|||
} |
|||
} |
|||
|
|||
@Input() |
|||
public offset = 2; |
|||
|
|||
@Input() |
|||
public position = 'bottom-right'; |
|||
|
|||
@Input() |
|||
public autoPosition = true; |
|||
|
|||
@Input() |
|||
public backdrop = true; |
|||
|
|||
@Input() |
|||
public closeAuto = true; |
|||
|
|||
@Input() |
|||
public closeAlways = false; |
|||
|
|||
@ViewChild('templatePortalContent', { static: false }) |
|||
public templateRef: TemplateRef<any>; |
|||
|
|||
constructor( |
|||
private readonly changeDetector: ChangeDetectorRef, |
|||
private readonly renderer: Renderer2, |
|||
private readonly rootView: RootViewComponent |
|||
) { |
|||
} |
|||
|
|||
public ngAfterViewInit() { |
|||
this.update(this.isOpen); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
hideModal(this.currentModel); |
|||
|
|||
this.eventsView.unsubscribeAll(); |
|||
this.eventsModel.unsubscribeAll(); |
|||
} |
|||
|
|||
public onClick() { |
|||
if (this.closeAlways) { |
|||
this.model.hide(); |
|||
} |
|||
} |
|||
|
|||
private update(isOpen: boolean) { |
|||
if (!this.templateRef || this.isOpen === isOpen) { |
|||
return; |
|||
} |
|||
|
|||
this.eventsView.unsubscribeAll(); |
|||
|
|||
if (isOpen) { |
|||
if (!this.renderedView) { |
|||
this.currentTarget = this.target; |
|||
|
|||
this.renderedView = this.rootView.viewContainer.createEmbeddedView(this.templateRef); |
|||
this.renderRoot = this.renderedView.rootNodes[0]; |
|||
|
|||
this.setupStyles(); |
|||
this.subscribeToView(); |
|||
|
|||
this.changeDetector.detectChanges(); |
|||
} |
|||
} else { |
|||
if (this.renderedView) { |
|||
this.renderedView.destroy(); |
|||
this.renderedView = null; |
|||
this.renderRoot = null; |
|||
|
|||
this.changeDetector.detectChanges(); |
|||
} |
|||
} |
|||
|
|||
this.isOpen = isOpen; |
|||
} |
|||
|
|||
private setupStyles() { |
|||
this.renderer.setStyle(this.renderRoot, 'display', 'block'); |
|||
this.renderer.setStyle(this.renderRoot, 'right', 'auto'); |
|||
this.renderer.setStyle(this.renderRoot, 'bottom', 'auto'); |
|||
this.renderer.setStyle(this.renderRoot, 'margin', '0'); |
|||
this.renderer.setStyle(this.renderRoot, 'position', 'fixed'); |
|||
this.renderer.setStyle(this.renderRoot, 'z-index', '1000000'); |
|||
} |
|||
|
|||
private subscribeToModel(value: Model) { |
|||
if (isModalModel(value)) { |
|||
this.currentModel = value; |
|||
|
|||
this.eventsModel.own(value.isOpen.subscribe(update => { |
|||
this.update(update); |
|||
})); |
|||
} else { |
|||
this.update(value === true); |
|||
} |
|||
} |
|||
|
|||
private subscribeToView() { |
|||
if (this.renderRoot) { |
|||
this.eventsView.own(this.renderer.listen(this.renderRoot, 'resize', () => { |
|||
this.updatePosition(); |
|||
})); |
|||
|
|||
if (this.currentTarget) { |
|||
this.eventsView.own(this.renderer.listen(this.currentTarget, 'resize', () => { |
|||
this.updatePosition(); |
|||
})); |
|||
|
|||
this.eventsView.own(timer(100, 100).subscribe(() => { |
|||
this.updatePosition(); |
|||
})); |
|||
} |
|||
} |
|||
|
|||
if (this.closeAuto) { |
|||
document.addEventListener('click', this.documentClickListener, true); |
|||
|
|||
this.eventsView.own(() => { |
|||
document.removeEventListener('click', this.documentClickListener); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private documentClickListener = (event: MouseEvent) => { |
|||
if (!event.target || this.renderRoot === null) { |
|||
return; |
|||
} |
|||
|
|||
const model = this.currentModel; |
|||
|
|||
if (this.closeAlways) { |
|||
setTimeout(() => { |
|||
hideModal(model); |
|||
}, 100); |
|||
} else { |
|||
try { |
|||
const rootBounds = this.renderRoot.getBoundingClientRect(); |
|||
|
|||
if (rootBounds.width > 0 && rootBounds.height > 0) { |
|||
const clickedInside = this.renderRoot.contains(<Node>event.target); |
|||
|
|||
if (!clickedInside && this.model) { |
|||
this.model.hide(); |
|||
} |
|||
} |
|||
} catch (ex) { |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private updatePosition() { |
|||
if (!this.renderRoot || !this.currentTarget) { |
|||
return; |
|||
} |
|||
|
|||
const modalRect = this.renderRoot.getBoundingClientRect(); |
|||
|
|||
if ((modalRect.width === 0 || modalRect.height === 0) && this.position !== 'full') { |
|||
return; |
|||
} |
|||
|
|||
const targetRect = this.currentTarget.getBoundingClientRect(); |
|||
|
|||
let y = 0; |
|||
let x = 0; |
|||
|
|||
if (this.position === 'full') { |
|||
x = -this.offset + targetRect.left; |
|||
y = -this.offset + targetRect.top; |
|||
|
|||
const w = 2 * this.offset + targetRect.width; |
|||
const h = 2 * this.offset + targetRect.height; |
|||
|
|||
this.renderer.setStyle(this.renderRoot, 'width', `${w}px`); |
|||
this.renderer.setStyle(this.renderRoot, 'height', `${h}px`); |
|||
} else { |
|||
const viewH = document.documentElement!.clientHeight; |
|||
const viewW = document.documentElement!.clientWidth; |
|||
|
|||
const position = positionModal(targetRect, modalRect, this.position, this.offset, this.autoPosition, viewW, viewH); |
|||
|
|||
x = position.x; |
|||
y = position.y; |
|||
} |
|||
|
|||
this.renderer.setStyle(this.renderRoot, 'top', `${y}px`); |
|||
this.renderer.setStyle(this.renderRoot, 'left', `${x}px`); |
|||
} |
|||
} |
|||
|
|||
function hideModal(model: Model) { |
|||
if (model && isModalModel(model)) { |
|||
model.hide(); |
|||
} |
|||
} |
|||
|
|||
function isModalModel(model: Model): model is DialogModel | ModalModel { |
|||
return Types.is(model, DialogModel) || Types.is(model, ModalModel); |
|||
} |
|||
@ -1,3 +0,0 @@ |
|||
<div #element></div> |
|||
|
|||
<ng-content></ng-content> |
|||
@ -1,2 +0,0 @@ |
|||
@import '_mixins'; |
|||
@import '_vars'; |
|||
Loading…
Reference in new issue