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()"> |
<sqx-modal [model]="dueTimeDialog"> |
||||
<ng-container title> |
<sqx-modal-dialog (close)="cancelStatusChange()"> |
||||
{{dueTimeAction}} content item(s) |
<ng-container title> |
||||
</ng-container> |
{{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> |
</sqx-modal> |
||||
<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> |
|
||||
|
|||||
@ -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