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