mirror of https://github.com/Squidex/squidex.git
28 changed files with 600 additions and 156 deletions
@ -0,0 +1,51 @@ |
|||||
|
FROM microsoft/aspnetcore-build:1.1.2 |
||||
|
|
||||
|
# Install runtime dependencies |
||||
|
RUN apt-get update \ |
||||
|
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \ |
||||
|
&& apt-get clean \ |
||||
|
&& rm -rf /var/lib/apt/lists/* |
||||
|
|
||||
|
# Install official PhantomJS release |
||||
|
RUN set -x \ |
||||
|
&& apt-get update \ |
||||
|
&& apt-get install -y --no-install-recommends \ |
||||
|
&& mkdir /srv/var \ |
||||
|
&& mkdir /tmp/phantomjs \ |
||||
|
# Download Phantom JS |
||||
|
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \ |
||||
|
# Copy binaries only |
||||
|
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \ |
||||
|
# Create symbol link |
||||
|
# Clean up |
||||
|
&& apt-get autoremove -y \ |
||||
|
&& apt-get clean all \ |
||||
|
&& rm -rf /tmp/* /var/lib/apt/lists/* |
||||
|
|
||||
|
RUN phantomjs --version |
||||
|
|
||||
|
COPY src/Squidex/package.json /tmp/package.json |
||||
|
RUN cd /tmp \ |
||||
|
&& npm install \ |
||||
|
&& npm rebuild node-sass |
||||
|
|
||||
|
COPY . . |
||||
|
|
||||
|
WORKDIR / |
||||
|
|
||||
|
# Build Frontend |
||||
|
RUN cp -a /tmp/node_modules /src/Squidex/ \ |
||||
|
&& cd /src/Squidex \ |
||||
|
&& npm run test:coverage \ |
||||
|
&& npm run build:copy \ |
||||
|
&& npm run build |
||||
|
|
||||
|
# Test Backend |
||||
|
RUN dotnet restore \ |
||||
|
&& dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ |
||||
|
&& dotnet test tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj \ |
||||
|
&& dotnet test tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj \ |
||||
|
&& dotnet test tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj |
||||
|
|
||||
|
# Publish |
||||
|
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release |
||||
@ -0,0 +1,11 @@ |
|||||
|
# Build the image |
||||
|
docker build . -t squidex-build-image -f dockerfile.build |
||||
|
|
||||
|
# Open the image |
||||
|
docker create --name squidex-build-container squidex-build-image |
||||
|
|
||||
|
# Copy the output to the host file system |
||||
|
docker cp squidex-build-container:/out ./publish |
||||
|
|
||||
|
# Cleanup |
||||
|
docker rm squidex-build-container |
||||
@ -1,2 +1,15 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@import '_mixins'; |
||||
|
|
||||
|
.autocomplete-user { |
||||
|
&-picture { |
||||
|
float: left; |
||||
|
margin-top: .4rem; |
||||
|
} |
||||
|
|
||||
|
&-name, |
||||
|
&-email { |
||||
|
@include truncate; |
||||
|
margin-left: 3rem; |
||||
|
} |
||||
|
} |
||||
@ -1,18 +1,15 @@ |
|||||
<span> |
<span> |
||||
<input type="text" class="form-control" (blur)="blur()" [attr.name]="inputName" (keydown)="onKeyDown($event)" (blur)="markTouched()" [attr.placeholder]="placeholder" |
<input type="text" class="form-control" (blur)="blur()" [attr.name]="inputName" (keydown)="onKeyDown($event)" [attr.placeholder]="placeholder" #input |
||||
[formControl]="queryInput" |
[formControl]="queryInput" |
||||
autocomplete="off" |
autocomplete="off" |
||||
autocorrect="off" |
autocorrect="off" |
||||
autocapitalize="off"> |
autocapitalize="off"> |
||||
|
|
||||
<div class="items-container" *ngIf="items.length > 0"> |
<div *ngIf="items.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container> |
||||
<div class="items" #container> |
<div *ngFor="let item of items; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" [class.active]="i === selectedIndex" (mousedown)="selectItem(item)" (mouseover)="selectIndex(i)" [sqxScrollActive]="i === itemSelection" [container]="container"> |
||||
<div *ngFor="let item of items; let i = index;" class="item" [class.active]="i === itemSelection" (mousedown)="chooseItem(item)" (mouseover)="selectIndex(i)" [sqxScrollActive]="i === itemSelection" [container]="container"> |
<span *ngIf="!itemTemplate">{{item}}</span> |
||||
<img class="item-image" [attr.src]="item.image" /> |
|
||||
|
|
||||
<span class="item-title">{{item.title}}</span> |
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template> |
||||
<span class="item-description">{{item.description}}</span> |
|
||||
</div> |
|
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
</span> |
</span> |
||||
@ -1,51 +1,6 @@ |
|||||
@import '_mixins'; |
@import '_mixins'; |
||||
@import '_vars'; |
@import '_vars'; |
||||
|
|
||||
$color-input-border: rgba(0, 0, 0, .15); |
.control-dropdown { |
||||
|
width: 18rem; |
||||
.items { |
|
||||
&-container { |
|
||||
position: relative; |
|
||||
} |
|
||||
|
|
||||
& { |
|
||||
@include absolute(2px, auto, auto, 0); |
|
||||
@include border-radius(.25em); |
|
||||
@include box-shadow; |
|
||||
width: 18rem; |
|
||||
max-height: 12rem; |
|
||||
border: 1px solid $color-input-border; |
|
||||
background: $color-dark-foreground; |
|
||||
padding: .3rem 0; |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.item { |
|
||||
& { |
|
||||
padding: .3rem .8rem; |
|
||||
background: transparent; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
&.active { |
|
||||
color: $color-dark-foreground; |
|
||||
background: $color-theme-blue; |
|
||||
} |
|
||||
|
|
||||
&-image { |
|
||||
@include circle(2.5rem); |
|
||||
float: left; |
|
||||
} |
|
||||
|
|
||||
&-title, |
|
||||
&-description { |
|
||||
@include truncate; |
|
||||
margin-left: 3rem; |
|
||||
} |
|
||||
|
|
||||
&-description { |
|
||||
font-size: .8rem; |
|
||||
font-style: italic; |
|
||||
} |
|
||||
} |
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
<span> |
||||
|
<div class="selection"> |
||||
|
<input type="text" class="form-control" [disabled]="isDisabled" (click)="open()" readonly (keydown)="onKeyDown($event)" #input |
||||
|
autocomplete="off" |
||||
|
autocorrect="off" |
||||
|
autocapitalize="off"> |
||||
|
|
||||
|
<div class="control-dropdown-item" *ngIf="selectedItem"> |
||||
|
<span *ngIf="!selectionTemplate">{{selectedItem}}</span> |
||||
|
|
||||
|
<ng-template *ngIf="selectionTemplate" [sqxTemplateWrapper]="selectionTemplate" [item]="selectedItem"></ng-template> |
||||
|
</div> |
||||
|
|
||||
|
<i class="icon-caret-down"></i> |
||||
|
</div> |
||||
|
|
||||
|
<div class="items-container"> |
||||
|
<div class="control-dropdown" #container *sqxModalView="dropdown" [sqxModalTarget]="input"> |
||||
|
<div *ngFor="let item of items; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" [class.active]="i === selectedIndex" (mousedown)="selectIndexAndClose(i)" [sqxScrollActive]="i === selectedIndex" [container]="container"> |
||||
|
<span *ngIf="!itemTemplate">{{item}}</span> |
||||
|
|
||||
|
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</span> |
||||
@ -0,0 +1,36 @@ |
|||||
|
@import '_mixins'; |
||||
|
@import '_vars'; |
||||
|
|
||||
|
$color-input-disabled: #eef1f4; |
||||
|
|
||||
|
.form-control { |
||||
|
& { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
&[readonly] { |
||||
|
background: $color-input-background; |
||||
|
} |
||||
|
|
||||
|
&:disabled { |
||||
|
background: $color-input-disabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.selection { |
||||
|
& { |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.control-dropdown-item { |
||||
|
@include absolute(0, 1rem, 0, 0); |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.icon-caret-down { |
||||
|
@include absolute(30%, .4rem, auto, auto); |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { AfterContentInit, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
|
||||
|
const KEY_ENTER = 13; |
||||
|
const KEY_ESCAPE = 27; |
||||
|
const KEY_UP = 38; |
||||
|
const KEY_DOWN = 40; |
||||
|
const NOOP = () => { /* NOOP */ }; |
||||
|
|
||||
|
import { ModalView } from './../utils/modal-view'; |
||||
|
|
||||
|
export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { |
||||
|
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true |
||||
|
}; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-dropdown', |
||||
|
styleUrls: ['./dropdown.component.scss'], |
||||
|
templateUrl: './dropdown.component.html', |
||||
|
providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR] |
||||
|
}) |
||||
|
export class DropdownComponent implements AfterContentInit, ControlValueAccessor { |
||||
|
private changeCallback: (value: any) => void = NOOP; |
||||
|
private touchedCallback: () => void = NOOP; |
||||
|
|
||||
|
@Input() |
||||
|
public items: any[] = []; |
||||
|
|
||||
|
@ContentChildren(TemplateRef) |
||||
|
public templates: QueryList<any>; |
||||
|
|
||||
|
public dropdown = new ModalView(); |
||||
|
|
||||
|
public selectedItem: any; |
||||
|
public selectedIndex = -1; |
||||
|
public selectionTemplate: TemplateRef<any>; |
||||
|
|
||||
|
public itemTemplate: TemplateRef<any>; |
||||
|
|
||||
|
public isDisabled = false; |
||||
|
|
||||
|
public ngAfterContentInit() { |
||||
|
if (this.templates.length === 1) { |
||||
|
this.itemTemplate = this.selectionTemplate = this.templates.first; |
||||
|
} else { |
||||
|
this.templates.forEach(template => { |
||||
|
if (template.name === 'selection') { |
||||
|
this.selectionTemplate = template; |
||||
|
} else { |
||||
|
this.itemTemplate = template; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public writeValue(value: any) { |
||||
|
this.selectIndex(this.items && value ? this.items.indexOf(value) : 0); |
||||
|
} |
||||
|
|
||||
|
public setDisabledState(isDisabled: boolean): void { |
||||
|
this.isDisabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
public registerOnChange(fn: any) { |
||||
|
this.changeCallback = fn; |
||||
|
} |
||||
|
|
||||
|
public registerOnTouched(fn: any) { |
||||
|
this.touchedCallback = fn; |
||||
|
} |
||||
|
|
||||
|
public onKeyDown(event: KeyboardEvent) { |
||||
|
switch (event.keyCode) { |
||||
|
case KEY_UP: |
||||
|
this.up(); |
||||
|
return false; |
||||
|
case KEY_DOWN: |
||||
|
this.down(); |
||||
|
return false; |
||||
|
case KEY_ESCAPE: |
||||
|
case KEY_ENTER: |
||||
|
this.close(); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public open() { |
||||
|
this.dropdown.show(); |
||||
|
this.touchedCallback(); |
||||
|
} |
||||
|
|
||||
|
public selectIndexAndClose(selectedIndex: number) { |
||||
|
this.selectIndex(selectedIndex); |
||||
|
this.close(); |
||||
|
} |
||||
|
|
||||
|
private close() { |
||||
|
this.dropdown.hide(); |
||||
|
} |
||||
|
|
||||
|
private up() { |
||||
|
this.selectIndex(this.selectedIndex - 1); |
||||
|
} |
||||
|
|
||||
|
private down() { |
||||
|
this.selectIndex(this.selectedIndex + 1); |
||||
|
} |
||||
|
|
||||
|
private selectIndex(selectedIndex: number) { |
||||
|
if (selectedIndex < 0) { |
||||
|
selectedIndex = 0; |
||||
|
} |
||||
|
|
||||
|
const items = this.items || []; |
||||
|
|
||||
|
if (selectedIndex >= items.length) { |
||||
|
selectedIndex = items.length - 1; |
||||
|
} |
||||
|
|
||||
|
const value = items[selectedIndex]; |
||||
|
|
||||
|
if (value !== this.selectedItem) { |
||||
|
this.selectedIndex = selectedIndex; |
||||
|
this.selectedItem = value; |
||||
|
|
||||
|
this.changeCallback(value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer } from '@angular/core'; |
||||
|
import { Observable, Subscription } from 'rxjs'; |
||||
|
|
||||
|
@Directive({ |
||||
|
selector: '[sqxModalTarget]' |
||||
|
}) |
||||
|
export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit { |
||||
|
private elementResizeListener: Function; |
||||
|
private targetResizeListener: Function; |
||||
|
private timer: Subscription; |
||||
|
private targetElement: any; |
||||
|
|
||||
|
@Input('sqxModalTarget') |
||||
|
public target: any; |
||||
|
|
||||
|
@Input() |
||||
|
public offset = 2; |
||||
|
|
||||
|
@Input() |
||||
|
public position = 'left'; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly renderer: Renderer, |
||||
|
private readonly element: ElementRef |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
if (this.target) { |
||||
|
this.targetElement = this.target; |
||||
|
|
||||
|
this.targetResizeListener = |
||||
|
this.renderer.listen(this.targetElement, 'resize', () => { |
||||
|
this.updatePosition(); |
||||
|
}); |
||||
|
|
||||
|
this.elementResizeListener = |
||||
|
this.renderer.listen(this.element.nativeElement, 'resize', () => { |
||||
|
this.updatePosition(); |
||||
|
}); |
||||
|
|
||||
|
this.timer = |
||||
|
Observable.timer(100, 100).subscribe(() => { |
||||
|
this.updatePosition(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
if (this.targetResizeListener) { |
||||
|
this.targetResizeListener(); |
||||
|
} |
||||
|
|
||||
|
if (this.elementResizeListener) { |
||||
|
this.elementResizeListener(); |
||||
|
} |
||||
|
|
||||
|
if (this.timer) { |
||||
|
this.timer.unsubscribe(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngAfterViewInit() { |
||||
|
const modalRef = this.element.nativeElement; |
||||
|
|
||||
|
this.renderer.setElementStyle(modalRef, 'position', 'fixed'); |
||||
|
this.renderer.setElementStyle(modalRef, 'z-index', '1000000'); |
||||
|
|
||||
|
this.updatePosition(); |
||||
|
} |
||||
|
|
||||
|
private updatePosition() { |
||||
|
const viewportHeight = document.documentElement.clientHeight; |
||||
|
|
||||
|
const modalRef = this.element.nativeElement; |
||||
|
const modalRect = this.element.nativeElement.getBoundingClientRect(); |
||||
|
|
||||
|
const targetRect: ClientRect = this.targetElement.getBoundingClientRect(); |
||||
|
|
||||
|
const left = this.position === 'left' ? |
||||
|
targetRect.left : |
||||
|
targetRect.right - modalRect.width; |
||||
|
|
||||
|
let top = targetRect.bottom + this.offset; |
||||
|
|
||||
|
if (top + modalRect.height > viewportHeight) { |
||||
|
const potentialTop = targetRect.top - modalRect.height - this.offset; |
||||
|
|
||||
|
if (potentialTop > 0) { |
||||
|
top = potentialTop; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.renderer.setElementStyle(modalRef, 'top', top + 'px'); |
||||
|
this.renderer.setElementStyle(modalRef, 'left', left + 'px'); |
||||
|
this.renderer.setElementStyle(modalRef, 'right', 'auto'); |
||||
|
this.renderer.setElementStyle(modalRef, 'bottom', 'auto'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Directive, Input, OnDestroy, OnInit, OnChanges, TemplateRef, ViewContainerRef, EmbeddedViewRef } from '@angular/core'; |
||||
|
|
||||
|
@Directive({ |
||||
|
selector: '[sqxTemplateWrapper]' |
||||
|
}) |
||||
|
export class TemplateWrapper implements OnInit, OnDestroy, OnChanges { |
||||
|
@Input() |
||||
|
public item: any; |
||||
|
|
||||
|
@Input() |
||||
|
public index: number; |
||||
|
|
||||
|
@Input('sqxTemplateWrapper') |
||||
|
public templateRef: TemplateRef<any>; |
||||
|
|
||||
|
public view: EmbeddedViewRef<any>; |
||||
|
|
||||
|
public constructor( |
||||
|
private viewContainer: ViewContainerRef |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.view = this.viewContainer.createEmbeddedView(this.templateRef, { |
||||
|
'\$implicit': this.item, |
||||
|
'index': this.index |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
if (this.view) { |
||||
|
this.view.context.$implicit = this.item; |
||||
|
this.view.context.index = this.index; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
if (this.view) { |
||||
|
this.view.destroy(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue