mirror of https://github.com/Squidex/squidex.git
40 changed files with 516 additions and 447 deletions
@ -0,0 +1,9 @@ |
|||||
|
<sqx-modal-dialog (close)="emitComplete()" large="true"> |
||||
|
<ng-container title> |
||||
|
Export Schema |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<sqx-json-editor disabled [ngModel]="export"></sqx-json-editor> |
||||
|
</ng-container> |
||||
|
</sqx-modal-dialog> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
@ -0,0 +1,33 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
||||
|
|
||||
|
import { SchemaDetailsDto } from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-schema-export-form', |
||||
|
styleUrls: ['./schema-export-form.component.scss'], |
||||
|
templateUrl: './schema-export-form.component.html' |
||||
|
}) |
||||
|
export class SchemaExportFormComponent implements OnInit { |
||||
|
@Output() |
||||
|
public complete = new EventEmitter(); |
||||
|
|
||||
|
@Input() |
||||
|
public schema: SchemaDetailsDto; |
||||
|
|
||||
|
public export: any; |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.export = this.schema.export(); |
||||
|
} |
||||
|
|
||||
|
public emitComplete() { |
||||
|
this.complete.emit(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
/* |
||||
|
* 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: '[sqxAnchoredTo]' |
||||
|
}) |
||||
|
export class ModalPlacementDirective extends ResourceOwner implements AfterViewInit, OnDestroy { |
||||
|
private targetElement: Element; |
||||
|
|
||||
|
@Input('sqxAnchoredTo') |
||||
|
public set target(element: Element) { |
||||
|
if (element !== this.targetElement) { |
||||
|
this.unsubscribeAll(); |
||||
|
|
||||
|
this.targetElement = element; |
||||
|
|
||||
|
if (element) { |
||||
|
this.listenToElement(element); |
||||
|
this.updatePosition(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
public offset = 2; |
||||
|
|
||||
|
@Input() |
||||
|
public position = 'bottom-right'; |
||||
|
|
||||
|
@Input() |
||||
|
public update = true; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly renderer: Renderer2, |
||||
|
private readonly element: ElementRef<Element> |
||||
|
) { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
private listenToElement(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.renderer.setStyle(modalRef, 'right', 'auto'); |
||||
|
this.renderer.setStyle(modalRef, 'bottom', 'auto'); |
||||
|
this.renderer.setStyle(modalRef, 'margin', '0'); |
||||
|
|
||||
|
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.update, viewW, viewH); |
||||
|
|
||||
|
x = position.x; |
||||
|
y = position.y; |
||||
|
} |
||||
|
|
||||
|
this.renderer.setStyle(modalRef, 'top', `${y}px`); |
||||
|
this.renderer.setStyle(modalRef, 'left', `${x}px`); |
||||
|
} |
||||
|
} |
||||
@ -1,254 +0,0 @@ |
|||||
/* |
|
||||
* 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); |
|
||||
} |
|
||||
@ -0,0 +1,171 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectorRef, Directive, EmbeddedViewRef, Input, OnDestroy, Renderer2, TemplateRef } from '@angular/core'; |
||||
|
|
||||
|
import { |
||||
|
DialogModel, |
||||
|
ModalModel, |
||||
|
ResourceOwner, |
||||
|
Types |
||||
|
} from '@app/framework/internal'; |
||||
|
|
||||
|
import { RootViewComponent } from './root-view.component'; |
||||
|
|
||||
|
declare type Model = DialogModel | ModalModel | any; |
||||
|
|
||||
|
@Directive({ |
||||
|
selector: '[sqxModal]' |
||||
|
}) |
||||
|
export class ModalDirective implements OnDestroy { |
||||
|
private readonly eventsView = new ResourceOwner(); |
||||
|
private readonly eventsModel = new ResourceOwner(); |
||||
|
private currentModel: DialogModel | ModalModel | null = null; |
||||
|
private renderedView: EmbeddedViewRef<any> | null = null; |
||||
|
private renderRoots: HTMLElement[] | null; |
||||
|
private isOpen: boolean; |
||||
|
|
||||
|
@Input('sqxModal') |
||||
|
public set model(value: Model) { |
||||
|
if (this.currentModel !== value) { |
||||
|
this.currentModel = value; |
||||
|
|
||||
|
this.eventsModel.unsubscribeAll(); |
||||
|
|
||||
|
this.subscribeToModel(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input('sqxModalCloseAuto') |
||||
|
public closeAuto = true; |
||||
|
|
||||
|
@Input('sqxModalCloseAlways') |
||||
|
public closeAlways = false; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly changeDetector: ChangeDetectorRef, |
||||
|
private readonly renderer: Renderer2, |
||||
|
private readonly rootView: RootViewComponent, |
||||
|
private readonly templateRef: TemplateRef<any> |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
hideModal(this.currentModel); |
||||
|
|
||||
|
this.eventsView.unsubscribeAll(); |
||||
|
this.eventsModel.unsubscribeAll(); |
||||
|
} |
||||
|
|
||||
|
private update(isOpen: boolean) { |
||||
|
if (!this.templateRef || this.isOpen === isOpen) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.eventsView.unsubscribeAll(); |
||||
|
|
||||
|
if (isOpen) { |
||||
|
if (!this.renderedView) { |
||||
|
this.renderedView = this.rootView.viewContainer.createEmbeddedView(this.templateRef); |
||||
|
this.renderRoots = this.renderedView.rootNodes.filter(x => !!x.style); |
||||
|
|
||||
|
this.setupStyles(); |
||||
|
this.subscribeToView(); |
||||
|
|
||||
|
this.changeDetector.detectChanges(); |
||||
|
} |
||||
|
} else { |
||||
|
if (this.renderedView) { |
||||
|
this.renderedView.destroy(); |
||||
|
this.renderedView = null; |
||||
|
this.renderRoots = null; |
||||
|
|
||||
|
this.changeDetector.detectChanges(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.isOpen = isOpen; |
||||
|
} |
||||
|
|
||||
|
private setupStyles() { |
||||
|
if (this.renderRoots) { |
||||
|
for (let node of this.renderRoots) { |
||||
|
this.renderer.setStyle(node, 'display', 'block'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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.closeAuto) { |
||||
|
document.addEventListener('click', this.documentClickListener, true); |
||||
|
|
||||
|
this.eventsView.own(() => { |
||||
|
document.removeEventListener('click', this.documentClickListener); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (this.closeAlways && this.renderRoots) { |
||||
|
for (let node of this.renderRoots) { |
||||
|
this.eventsView.own(this.renderer.listen(node, 'click', this.elementListener)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private elementListener = (event: MouseEvent) => { |
||||
|
if (this.isClickedInside(event)) { |
||||
|
hideModal(this.currentModel); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private documentClickListener = (event: MouseEvent) => { |
||||
|
if (!this.isClickedInside(event)) { |
||||
|
hideModal(this.currentModel); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private isClickedInside(event: MouseEvent) { |
||||
|
try { |
||||
|
if (!this.renderRoots) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
for (let node of this.renderRoots) { |
||||
|
const bounds = node.getBoundingClientRect(); |
||||
|
|
||||
|
if (bounds.width > 0 && bounds.height > 0 && node.contains(<Node>event.target)) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} catch (ex) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
Loading…
Reference in new issue