Browse Source

Merge branch 'master' into feature-references

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
bcddcb597e
  1. 6
      Dockerfile
  2. 51
      Dockerfile.build
  3. 11
      build.ps1
  4. 2
      src/Squidex/Config/Identity/IdentityUsage.cs
  5. 4
      src/Squidex/app/features/content/pages/contents/content-item.component.html
  6. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  7. 14
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  8. 8
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss
  9. 11
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  10. 15
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss
  11. 12
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  12. 13
      src/Squidex/app/framework/angular/autocomplete.component.html
  13. 49
      src/Squidex/app/framework/angular/autocomplete.component.scss
  14. 107
      src/Squidex/app/framework/angular/autocomplete.component.ts
  15. 18
      src/Squidex/app/framework/angular/cloak.directive.spec.ts
  16. 7
      src/Squidex/app/framework/angular/cloak.directive.ts
  17. 8
      src/Squidex/app/framework/angular/date-time-editor.component.scss
  18. 26
      src/Squidex/app/framework/angular/dropdown.component.html
  19. 36
      src/Squidex/app/framework/angular/dropdown.component.scss
  20. 136
      src/Squidex/app/framework/angular/dropdown.component.ts
  21. 106
      src/Squidex/app/framework/angular/modal-target.directive.ts
  22. 12
      src/Squidex/app/framework/angular/scroll-active.directive.ts
  23. 49
      src/Squidex/app/framework/angular/template-wrapper.directive.ts
  24. 3
      src/Squidex/app/framework/declarations.ts
  25. 9
      src/Squidex/app/framework/module.ts
  26. 4
      src/Squidex/app/shared/components/language-selector.component.html
  27. 33
      src/Squidex/app/theme/_forms.scss
  28. 2
      src/Squidex/app/theme/_vars.scss

6
dockerfile → Dockerfile

@ -28,9 +28,9 @@ RUN set -x \
RUN phantomjs --version
COPY src/Squidex/package.json /tmp/package.json
RUN cd /tmp \
&& npm install \
&& npm rebuild node-sass
# Install Node Packages
RUN cd /tmp && npm install
COPY . .

51
Dockerfile.build

@ -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

11
build.ps1

@ -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

2
src/Squidex/Config/Identity/IdentityUsage.cs

@ -59,7 +59,7 @@ namespace Squidex.Config.Identity
Task.Run(async () =>
{
var user = await userManager.FindByEmailAsync(adminPass);
var user = await userManager.FindByEmailAsync(adminEmail);
async Task userInitAsync(IUser theUser)
{

4
src/Squidex/app/features/content/pages/contents/content-item.component.html

@ -13,10 +13,10 @@
</td>
<td>
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-link btn-decent" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async">
<button type="button" class="btn btn-link btn-decent" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [@fade]>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="!content.isPublished">
Publish
</a>

4
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -22,10 +22,10 @@
</button>
<div class="dropdown dropdown-options">
<button type="button" class="btn btn-link btn-decent" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async">
<button type="button" class="btn btn-link btn-decent" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [@fade]>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item" (click)="enabling.emit()" *ngIf="field.isDisabled">
Enable
</a>

14
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -18,10 +18,10 @@
</div>
<div class="dropdown dropdown-options">
<button type="button" class="btn btn-link btn-decent btn-sm" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async">
<button type="button" class="btn btn-link btn-decent btn-sm" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown" closeAlways="true" [@fade]>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item dropdown-item-delete" (click)="confirmDeleteDialog.show()">
Delete
</a>
@ -54,10 +54,12 @@
<div class="table-items-footer">
<form [formGroup]="addFieldForm" (ngSubmit)="addField()">
<div class="form-inline">
<div class="form-group mr-1">
<select class="form-control" formControlName="type">
<option *ngFor="let type of fieldTypes" [ngValue]="type">{{type}}</option>
</select>
<div class="form-group types-group mr-1">
<sqx-dropdown formControlName="type" [items]="fieldTypes">
<ng-template let-type="$implicit">
<i class="field-icon icon-type-{{type}}"></i> <span>{{type}}</span>
</ng-template>
</sqx-dropdown>
</div>
<div class="form-group mr-1">

8
src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss

@ -20,6 +20,10 @@
margin-right: 1rem;
}
.types-group {
width: 10rem;
}
.schema {
&-edit {
color: $color-border-dark;
@ -32,6 +36,10 @@
}
}
.field-icon {
color: $color-border-dark;
}
.form-check {
margin-top: 1rem;
margin-bottom: -.2rem;

11
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -56,8 +56,15 @@
<div class="table-items-footer">
<form class="form-inline" [formGroup]="addContributorForm" (ngSubmit)="assignContributor()">
<div class="form-group mr-3">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user"></sqx-autocomplete>
<div class="form-group mr-1">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="email">
<ng-template let-user="$implicit">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | userDtoPicture" />
<span class="user-name autocomplete-user-name">{{user.displayName}}</span>
<span class="user-email autocomplete-user-email">{{user.email}}</span>
</ng-template>
</sqx-autocomplete>
</div>
<button type="submit" class="btn btn-success" [disabled]="!canAddContributor">Add Contributor</button>

15
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss

@ -1,2 +1,15 @@
@import '_vars';
@import '_mixins';
@import '_mixins';
.autocomplete-user {
&-picture {
float: left;
margin-top: .4rem;
}
&-name,
&-email {
@include truncate;
margin-left: 3rem;
}
}

12
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -15,7 +15,6 @@ import {
AppContributorsService,
AppsStoreService,
AuthService,
AutocompleteItem,
AutocompleteSource,
HistoryChannelUpdated,
ImmutableArray,
@ -32,19 +31,14 @@ export class UsersDataSource implements AutocompleteSource {
) {
}
public find(query: string): Observable<AutocompleteItem[]> {
public find(query: string): Observable<any[]> {
return this.usersService.getUsers(query)
.map(users => {
const results: AutocompleteItem[] = [];
const results: any[] = [];
for (let user of users) {
if (!this.component.appContributors || !this.component.appContributors.find(t => t.contributorId === user.id)) {
results.push(
new AutocompleteItem(
user.displayName,
user.email,
user.pictureUrl!,
user));
results.push(user);
}
}
return results;

13
src/Squidex/app/framework/angular/autocomplete.component.html

@ -1,18 +1,15 @@
<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"
autocomplete="off"
autocorrect="off"
autocapitalize="off">
<div class="items-container" *ngIf="items.length > 0">
<div class="items" #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">
<img class="item-image" [attr.src]="item.image" />
<div *ngIf="items.length > 0" [sqxModalTarget]="input" class="control-dropdown" #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">
<span *ngIf="!itemTemplate">{{item}}</span>
<span class="item-title">{{item.title}}</span>
<span class="item-description">{{item.description}}</span>
</div>
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template>
</div>
</div>
</span>

49
src/Squidex/app/framework/angular/autocomplete.component.scss

@ -1,51 +1,6 @@
@import '_mixins';
@import '_vars';
$color-input-border: rgba(0, 0, 0, .15);
.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;
}
.control-dropdown {
width: 18rem;
}

107
src/Squidex/app/framework/angular/autocomplete.component.ts

@ -5,25 +5,16 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, ContentChild, forwardRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
export interface AutocompleteSource {
find(query: string): Observable<AutocompleteItem[]>;
}
export class AutocompleteItem {
constructor(
public readonly title: string,
public readonly description: string,
public readonly image: string,
public readonly model: any
) {
}
find(query: string): Observable<any[]>;
}
const KEY_ENTER = 13;
const KEY_ESCAPE = 27;
const KEY_UP = 38;
const KEY_DOWN = 40;
const NOOP = () => { /* NOOP */ };
@ -49,25 +40,26 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
@Input()
public inputName = 'autocompletion';
@Input()
public displayProperty = '';
@Input()
public placeholder = '';
public items: AutocompleteItem[] = [];
public itemSelection = -1;
@ContentChild(TemplateRef)
public itemTemplate: TemplateRef<any>;
public items: any[] = [];
public selectedIndex = -1;
public queryInput = new FormControl();
public writeValue(value: any) {
if (!value) {
this.queryInput.setValue('');
this.resetValue();
} else {
let item: AutocompleteItem | null = null;
if (value instanceof AutocompleteItem) {
item = value;
} else {
item = this.items.find(i => i.model === value);
}
let item = this.items.find(i => i === value);
if (item) {
this.queryInput.setValue(value.title || '');
@ -101,27 +93,23 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public ngOnInit() {
this.subscription =
this.queryInput.valueChanges
.map(q => <string>q)
.map(q => q ? q.trim() : q)
.map(query => <string>query)
.map(query => query ? query.trim() : query)
.distinctUntilChanged()
.debounceTime(200)
.do(q => {
if (!q) {
.do(query => {
if (!query) {
this.reset();
}
})
.filter(q => !!q && !!this.source)
.switchMap(q => this.source.find(q)).catch(_ => Observable.of([]))
.subscribe(r => {
.filter(query => !!query && !!this.source)
.switchMap(query => this.source.find(query)).catch(_ => Observable.of([]))
.subscribe(items => {
this.reset();
this.items = r || [];
this.items = items || [];
});
}
public markTouched() {
this.touchedCallback();
}
public onKeyDown(event: KeyboardEvent) {
switch (event.keyCode) {
case KEY_UP:
@ -130,35 +118,27 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
case KEY_DOWN:
this.down();
return false;
case KEY_ESCAPE:
this.resetValue();
this.reset();
return false;
case KEY_ENTER:
if (this.items.length > 0) {
this.chooseItem();
this.selectItem();
return false;
}
break;
}
}
private reset() {
this.items = [];
this.itemSelection = -1;
}
public blur() {
this.reset();
this.touchedCallback();
}
public up() {
this.selectIndex(this.itemSelection - 1);
}
public down() {
this.selectIndex(this.itemSelection + 1);
}
public chooseItem(selection: AutocompleteItem | null = null) {
public selectItem(selection: any | null = null) {
if (!selection) {
selection = this.items[this.itemSelection];
selection = this.items[this.selectedIndex];
}
if (!selection && this.items.length === 1) {
@ -167,7 +147,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
if (selection) {
try {
this.queryInput.setValue(selection.title);
if (this.displayProperty && this.displayProperty.length > 0) {
this.queryInput.setValue(selection[this.displayProperty], { emitEvent: false });
} else {
this.queryInput.setValue(selection.toString(), { emitEvent: false });
}
this.changeCallback(selection);
} finally {
this.reset();
@ -175,7 +159,19 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
}
}
public selectIndex(selection: number) {
private up() {
this.selectIndex(this.selectedIndex - 1);
}
private down() {
this.selectIndex(this.selectedIndex + 1);
}
private resetValue() {
this.queryInput.setValue('');
}
private selectIndex(selection: number) {
if (selection < 0) {
selection = 0;
}
@ -184,6 +180,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
selection = this.items.length - 1;
}
this.itemSelection = selection;
this.selectedIndex = selection;
}
private reset() {
this.items = [];
this.selectedIndex = -1;
}
}

18
src/Squidex/app/framework/angular/cloak.directive.spec.ts

@ -12,16 +12,20 @@ describe('CloakDirective', () => {
let called = false;
const element = {
nativeElement: {
classList: {
remove: () => {
called = true;
}
}
nativeElement: {}
};
const renderer = {
setElementClass: (target: any, className: string, isAdd: boolean) => {
called = true;
expect(target).toBe(element.nativeElement);
expect(className).toBe('sqx-cloak');
expect(isAdd).toBeFalsy();
}
};
new CloakDirective(element).ngOnInit();
new CloakDirective(<any>element, <any>renderer).ngOnInit();
expect(called).toBeTruthy();
});

7
src/Squidex/app/framework/angular/cloak.directive.ts

@ -5,18 +5,19 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Directive, ElementRef, OnInit } from '@angular/core';
import { Directive, ElementRef, OnInit, Renderer } from '@angular/core';
@Directive({
selector: '.sqx-cloak'
})
export class CloakDirective implements OnInit {
constructor(
private readonly element: ElementRef
private readonly element: ElementRef,
private readonly renderer: Renderer
) {
}
public ngOnInit() {
this.element.nativeElement.classList.remove('sqx-cloak');
this.renderer.setElementClass(this.element.nativeElement, 'sqx-cloak', false);
}
}

8
src/Squidex/app/framework/angular/date-time-editor.component.scss

@ -1,8 +1,6 @@
@import '_mixins';
@import '_vars';
$form-color: #fff;
:host(.ng-invalid) {
&.ng-dirty {
.form-control {
@ -18,12 +16,6 @@ $form-color: #fff;
}
}
.form-control {
&[readonly] {
background: $form-color;
}
}
.date-group {
& {
padding-right: .25rem;

26
src/Squidex/app/framework/angular/dropdown.component.html

@ -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>

36
src/Squidex/app/framework/angular/dropdown.component.scss

@ -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;
}
}

136
src/Squidex/app/framework/angular/dropdown.component.ts

@ -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);
}
}
}

106
src/Squidex/app/framework/angular/modal-target.directive.ts

@ -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');
}
}

12
src/Squidex/app/framework/angular/scroll-active.directive.ts

@ -5,12 +5,12 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
import { AfterViewInit, Directive, ElementRef, Input, OnChanges } from '@angular/core';
@Directive({
selector: '[sqxScrollActive]'
})
export class ScrollActiveDirective implements OnChanges {
export class ScrollActiveDirective implements AfterViewInit, OnChanges {
@Input('sqxScrollActive')
public isActive = false;
@ -22,7 +22,15 @@ export class ScrollActiveDirective implements OnChanges {
) {
}
public ngAfterViewInit() {
this.check();
}
public ngOnChanges() {
this.check();
}
private check() {
if (this.isActive && this.container) {
this.scrollInView(this.container, this.element.nativeElement);
}

49
src/Squidex/app/framework/angular/template-wrapper.directive.ts

@ -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();
}
}
}

3
src/Squidex/app/framework/declarations.ts

@ -13,6 +13,7 @@ export * from './angular/control-errors.component';
export * from './angular/copy.directive';
export * from './angular/date-time-editor.component';
export * from './angular/date-time.pipes';
export * from './angular/dropdown.component';
export * from './angular/file-drop.directive';
export * from './angular/focus-on-change.directive';
export * from './angular/focus-on-init.directive';
@ -23,6 +24,7 @@ export * from './angular/indeterminate-value.directive';
export * from './angular/json-editor.component';
export * from './angular/lowercase-input.directive';
export * from './angular/markdown-editor.component';
export * from './angular/modal-target.directive';
export * from './angular/modal-view.directive';
export * from './angular/money.pipe';
export * from './angular/name.pipe';
@ -37,6 +39,7 @@ export * from './angular/shortcut.component';
export * from './angular/slider.component';
export * from './angular/stars.component';
export * from './angular/tag-editor.component';
export * from './angular/template-wrapper.directive';
export * from './angular/title.component';
export * from './angular/toggle.component';
export * from './angular/user-report.component';

9
src/Squidex/app/framework/module.ts

@ -22,6 +22,7 @@ import {
DayOfWeekPipe,
DayPipe,
DisplayNamePipe,
DropdownComponent,
DurationPipe,
FileDropDirective,
FocusOnChangeDirective,
@ -35,6 +36,7 @@ import {
LowerCaseInputDirective,
MarkdownEditorComponent,
MessageBus,
ModalTargetDirective,
ModalViewDirective,
MoneyPipe,
MonthPipe,
@ -55,6 +57,7 @@ import {
SliderComponent,
StarsComponent,
TagEditorComponent,
TemplateWrapper,
TitleService,
TitleComponent,
ToggleComponent,
@ -78,6 +81,7 @@ import {
DayOfWeekPipe,
DayPipe,
DisplayNamePipe,
DropdownComponent,
DurationPipe,
FileDropDirective,
FocusOnChangeDirective,
@ -89,6 +93,7 @@ import {
JsonEditorComponent,
LowerCaseInputDirective,
MarkdownEditorComponent,
ModalTargetDirective,
ModalViewDirective,
MoneyPipe,
MonthPipe,
@ -105,6 +110,7 @@ import {
SliderComponent,
StarsComponent,
TagEditorComponent,
TemplateWrapper,
TitleComponent,
ToggleComponent,
UserReportComponent
@ -118,6 +124,7 @@ import {
DayOfWeekPipe,
DayPipe,
DisplayNamePipe,
DropdownComponent,
DurationPipe,
FileDropDirective,
FocusOnChangeDirective,
@ -129,6 +136,7 @@ import {
JsonEditorComponent,
LowerCaseInputDirective,
MarkdownEditorComponent,
ModalTargetDirective,
ModalViewDirective,
MoneyPipe,
MonthPipe,
@ -145,6 +153,7 @@ import {
SliderComponent,
StarsComponent,
TagEditorComponent,
TemplateWrapper,
TitleComponent,
ToggleComponent,
UserReportComponent,

4
src/Squidex/app/shared/components/language-selector.component.html

@ -5,10 +5,10 @@
</div>
<div class="dropdown-options btn-group btn-group-{{size}}" *ngIf="isLargeMode">
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()">
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()" #button>
{{selectedLanguage.iso2Code}}
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [@fade]>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="button" position="right" [@fade]>
<div class="dropdown-item" *ngFor="let language of languages" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
<strong class="iso-code">{{language.iso2Code}}</strong> &nbsp; &nbsp; ({{language.englishName}})
</div>

33
src/Squidex/app/theme/_forms.scss

@ -84,6 +84,39 @@
}
}
//
// Control Dropdown item
//
.control-dropdown {
& {
@include absolute(2px, auto, auto, 0);
@include border-radius(.25em);
@include box-shadow;
max-height: 10rem;
border: 1px solid $color-input-border;
background: $color-dark-foreground;
padding: .3rem 0;
overflow-y: auto;
}
&-item {
padding: .5rem .75rem;
}
&-item-selectable {
& {
cursor: pointer;
}
&.active,
&:hover {
color: $color-dark-foreground;
border: 0;
background: $color-theme-blue;
}
}
}
//
// Form group error.
//

2
src/Squidex/app/theme/_vars.scss

@ -10,6 +10,8 @@ $color-subtext: $color-text-decent;
$color-empty: $color-text-decent;
$color-control: rgba(0, 0, 0, .15);
$color-input: #dbe4eb;
$color-input-border: rgba(0, 0, 0, .15);
$color-input-background: #fff;
$color-disabled: #eef1f4;
$color-extern-google: #d34836;

Loading…
Cancel
Save