Browse Source

Image editing

pull/479/head
Sebastian 7 years ago
parent
commit
df46cc134c
  1. 2
      frontend/app/features/apps/pages/news-dialog.component.html
  2. 2
      frontend/app/features/content/shared/references/content-selector.component.html
  3. 2
      frontend/app/features/rules/pages/rules/rule-wizard.component.html
  4. 2
      frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html
  5. 2
      frontend/app/features/schemas/pages/schemas/schema-form.component.html
  6. 2
      frontend/app/features/settings/pages/clients/client-connect-form.component.html
  7. 2
      frontend/app/framework/angular/modals/modal-dialog.component.html
  8. 2
      frontend/app/framework/angular/modals/modal-dialog.component.ts
  9. 11
      frontend/app/framework/services/resource-loader.service.ts
  10. 158
      frontend/app/shared/components/assets/asset-dialog.component.html
  11. 12
      frontend/app/shared/components/assets/asset-dialog.component.scss
  12. 7
      frontend/app/shared/components/assets/asset-dialog.component.ts
  13. 2
      frontend/app/shared/components/assets/assets-selector.component.html
  14. 1
      frontend/app/shared/components/assets/image-editor.component.html
  15. 29
      frontend/app/shared/components/assets/image-editor.component.scss
  16. 159
      frontend/app/shared/components/assets/image-editor.component.ts
  17. 2
      frontend/app/shared/components/search/search-form.component.html
  18. 1
      frontend/app/shared/declarations.ts
  19. 2
      frontend/app/shared/module.ts
  20. 4
      frontend/app/theme/_bootstrap.scss
  21. 4
      frontend/app/theme/_mixins.scss

2
frontend/app/features/apps/pages/news-dialog.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog large="true" (close)="close.emit()"> <sqx-modal-dialog size="lg" (close)="close.emit()">
<ng-container title> <ng-container title>
New Features New Features
</ng-container> </ng-container>

2
frontend/app/features/content/shared/references/content-selector.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog (close)="emitComplete()" large="true" fullHeight="true" grid="true" flexBody="true"> <sqx-modal-dialog (close)="emitComplete()" size="lg" fullHeight="true" grid="true" flexBody="true">
<ng-container title> <ng-container title>
<div class="row"> <div class="row">
<div class="col-selector"> <div class="col-selector">

2
frontend/app/features/rules/pages/rules/rule-wizard.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog large="true" fullHeight="true" (close)="emitComplete()" [showFooter]="step === 2 || step === 4"> <sqx-modal-dialog size="lg" fullHeight="true" (close)="emitComplete()" [showFooter]="step === 2 || step === 4">
<ng-container title> <ng-container title>
<ng-container *ngIf="mode === 'EditTrigger'"> <ng-container *ngIf="mode === 'EditTrigger'">
Edit Trigger Edit Trigger

2
frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog (close)="emitComplete()" large="true"> <sqx-modal-dialog (close)="emitComplete()" size="lg">
<ng-container title> <ng-container title>
<ng-container *ngIf="parent; else noParent"> <ng-container *ngIf="parent; else noParent">
Add Nested Field Add Nested Field

2
frontend/app/features/schemas/pages/schemas/schema-form.component.html

@ -1,5 +1,5 @@
<form [formGroup]="createForm.form" (ngSubmit)="createSchema()"> <form [formGroup]="createForm.form" (ngSubmit)="createSchema()">
<sqx-modal-dialog (close)="cancel.emit()" [large]="false"> <sqx-modal-dialog (close)="cancel.emit()">
<ng-container title> <ng-container title>
<ng-container *ngIf="import; else noImport"> <ng-container *ngIf="import; else noImport">
Clone Schema Clone Schema

2
frontend/app/features/settings/pages/clients/client-connect-form.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog (close)="complete.emit()" large="true"> <sqx-modal-dialog (close)="complete.emit()" size="lg" >
<ng-container title> <ng-container title>
Connect Connect
</ng-container> </ng-container>

2
frontend/app/framework/angular/modals/modal-dialog.component.html

@ -1,5 +1,5 @@
<div class="modal" @fade> <div class="modal" @fade>
<div class="modal-dialog" [class.modal-lg]="large" [class.modal-fh]="fullHeight"> <div class="modal-dialog modal-{{size}}" [class.modal-fh]="fullHeight">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header" *ngIf="showHeader"> <div class="modal-header" *ngIf="showHeader">
<h4 class="modal-title"> <h4 class="modal-title">

2
frontend/app/framework/angular/modals/modal-dialog.component.ts

@ -35,7 +35,7 @@ export class ModalDialogComponent implements AfterViewInit {
public showTabs = true; public showTabs = true;
@Input() @Input()
public large = false; public size: 'sm' | 'md' | 'lg' | 'xl' = 'md';
@Input() @Input()
public flexBody = false; public flexBody = false;

11
frontend/app/framework/services/resource-loader.service.ts

@ -44,13 +44,14 @@ export class ResourceLoaderService {
let result = this.cache[key]; let result = this.cache[key];
if (!result) { if (!result) {
result = new Promise((resolve, reject) => { const script = this.renderer.createElement('script');
const script = this.renderer.createElement('script');
this.renderer.setProperty(script, 'src', url);
this.renderer.setProperty(script, 'async', false);
this.renderer.appendChild(document.body, script);
result = new Promise((resolve) => {
this.renderer.listen(script, 'load', () => resolve()); this.renderer.listen(script, 'load', () => resolve());
this.renderer.setProperty(script, 'src', url);
this.renderer.setProperty(script, 'async', true);
this.renderer.appendChild(document.body, script);
}); });
this.cache[key] = result; this.cache[key] = result;

158
frontend/app/shared/components/assets/asset-dialog.component.html

@ -1,84 +1,100 @@
<form [formGroup]="annotateForm.form" (ngSubmit)="annotateAsset()"> <form [formGroup]="annotateForm.form" (ngSubmit)="annotateAsset()">
<sqx-modal-dialog (close)="emitComplete()"> <sqx-modal-dialog (close)="emitComplete()" size="xl" fullHeight="true" [showHeader]="true" [showFooter]="false">
<ng-container title> <ng-container title>
Update Asset Update Asset
</ng-container> </ng-container>
<ng-container tabs>
<ul class="nav nav-tabs2">
<li class="nav-item" *ngFor="let tab of selectableTabs">
<a class="nav-link" [class.active]="tab === selectedTab" (click)="selectTab(tab)">{{tab}}</a>
</li>
</ul>
<button type="submit" class="float-right btn btn-primary" [disabled]="!isEditable">Save</button>
</ng-container>
<ng-container content> <ng-container content>
<sqx-form-error [error]="annotateForm.error | async"></sqx-form-error> <ng-container [ngSwitch]="selectedTab">
<ng-container *ngSwitchCase="'Image'">
<div class="form-group"> <div class="image">
<label for="fileName">Name</label> <sqx-image-editor [imageUrl]="asset.contentUrl"></sqx-image-editor>
</div>
</ng-container>
<ng-container *ngSwitchCase="'Metadata'">
<div class="metadata">
<sqx-form-error [error]="annotateForm.error | async"></sqx-form-error>
<div class="form-group">
<label for="fileName">Name</label>
<sqx-control-errors for="fileName" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="fileName" formControlName="fileName" />
</div>
<div class="form-group">
<label for="slug">Slug</label>
<sqx-control-errors for="slug" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control slug" id="slug" formControlName="slug" sqxTransformInput="Slugify" />
<button type="button" class="btn btn-text-secondary btn-sm slug-generate" (click)="generateSlug()">
Generate
</button>
</div>
<div class="form-group">
<label>Tags</label>
<sqx-control-errors for="tags" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<sqx-tag-editor [suggestions]="allTags" [allowDuplicates]="false" [undefinedWhenEmpty]="false" formControlName="tags"></sqx-tag-editor>
</div>
<div class="form-group">
<label>Metadata</label>
<div class="form-group row no-gutters" *ngFor="let form of annotateForm.metadata.controls; let i = index">
<div class="col col-name pr-1">
<sqx-control-errors [for]="form.get('name')" fieldName="Name" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<sqx-control-errors for="fileName" [submitted]="annotateForm.submitted | async"></sqx-control-errors> <input type="text" class="form-control" maxlength="1000" [formControl]="form.get('name')" placeholder="Name" />
</div>
<input type="text" class="form-control" id="fileName" formControlName="fileName" />
</div>
<div class="form-group">
<label for="slug">Slug</label>
<sqx-control-errors for="slug" [submitted]="annotateForm.submitted | async"></sqx-control-errors> <div class="col pr-1">
<sqx-control-errors [for]="form.get('value')" fieldName="Value" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control slug" id="slug" formControlName="slug" sqxTransformInput="Slugify" />
<button type="button" class="btn btn-text-secondary btn-sm btn-generate" (click)="generateSlug()">
Generate
</button>
</div>
<div class="form-group">
<label>Tags</label>
<sqx-control-errors for="tags" [submitted]="annotateForm.submitted | async"></sqx-control-errors> <input type="text" class="form-control" maxlength="1000" [formControl]="form.get('value')" placeholder="Value" />
</div>
<sqx-tag-editor [suggestions]="allTags" [allowDuplicates]="false" [undefinedWhenEmpty]="false" formControlName="tags"></sqx-tag-editor>
</div> <div class="col-auto col-options">
<button type="button" class="btn btn-text-danger"
<div class="form-group"> [disabled]="!isEditable"
<label>Metadata</label> (sqxConfirmClick)="annotateForm.removeMetadata(i)"
confirmTitle="Remove url"
<div class="form-group row no-gutters" *ngFor="let form of annotateForm.metadata.controls; let i = index"> confirmText="Do you really want to remove this metadata?">
<div class="col col-name pr-1"> <i class="icon-bin2"></i>
<sqx-control-errors [for]="form.get('name')" fieldName="Name" [submitted]="annotateForm.submitted | async"></sqx-control-errors> </button>
</div>
<input type="text" class="form-control" maxlength="1000" [formControl]="form.get('name')" placeholder="Name" /> </div>
<div class="form-group">
<button type="button" class="btn btn-success" (click)="annotateForm.addMetadata()" [disabled]="!isEditable">
Add Metadata
</button>
</div>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="isProtected" formControlName="isProtected" />
<label class="form-check-label" for="isProtected">Protected</label>
</div>
</div> </div>
</ng-container>
<div class="col pr-1"> </ng-container>
<sqx-control-errors [for]="form.get('value')" fieldName="Value" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" [formControl]="form.get('value')" placeholder="Value" />
</div>
<div class="col-auto col-options">
<button type="button" class="btn btn-text-danger"
[disabled]="!isEditable"
(sqxConfirmClick)="annotateForm.removeMetadata(i)"
confirmTitle="Remove url"
confirmText="Do you really want to remove this metadata?">
<i class="icon-bin2"></i>
</button>
</div>
</div>
<div class="form-group">
<button type="button" class="btn btn-success" (click)="annotateForm.addMetadata()" [disabled]="!isEditable">
Add Metadata
</button>
</div>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="isProtected" formControlName="isProtected" />
<label class="form-check-label" for="isProtected">Protected</label>
</div>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" *ngIf="isEditable">Save</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>
</form> </form>

12
frontend/app/shared/components/assets/asset-dialog.component.scss

@ -2,11 +2,21 @@
position: relative; position: relative;
} }
.image {
@include absolute(0, 0, 0, 0);
}
.metadata {
margin: 1rem auto;
max-width: 500px;
min-width: 0;
}
.slug { .slug {
padding-right: 6rem; padding-right: 6rem;
} }
.btn-generate { .slug-generate {
& { & {
@include absolute(auto, 10px, 3px, auto); @include absolute(auto, 10px, 3px, auto);
} }

7
frontend/app/shared/components/assets/asset-dialog.component.ts

@ -32,6 +32,9 @@ export class AssetDialogComponent implements OnInit {
public isEditable = false; public isEditable = false;
public selectableTabs: ReadonlyArray<string> = ['Image', 'Metadata'];
public selectedTab = this.selectableTabs[0];
public annotateForm = new AnnotateAssetForm(this.formBuilder); public annotateForm = new AnnotateAssetForm(this.formBuilder);
constructor( constructor(
@ -47,6 +50,10 @@ export class AssetDialogComponent implements OnInit {
this.annotateForm.setEnabled(this.isEditable); this.annotateForm.setEnabled(this.isEditable);
} }
public selectTab(tab: string) {
this.selectedTab = tab;
}
public generateSlug() { public generateSlug() {
this.annotateForm.generateSlug(this.asset); this.annotateForm.generateSlug(this.asset);
} }

2
frontend/app/shared/components/assets/assets-selector.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog (close)="emitComplete()" large="true" fullHeight="true"> <sqx-modal-dialog (close)="emitComplete()" size="lg" fullHeight="true">
<ng-container title> <ng-container title>
Select assets Select assets
</ng-container> </ng-container>

1
frontend/app/shared/components/assets/image-editor.component.html

@ -0,0 +1 @@
<div #editor></div>

29
frontend/app/shared/components/assets/image-editor.component.scss

@ -0,0 +1,29 @@
:host {
height: 100%;
}
:host /deep/ {
.tui-image-editor-header {
display: none;
}
* {
box-sizing: content-box;
}
.tie-btn-delete {
display: none !important;
}
.tie-btn-delete-all {
display: none !important;
& + li {
display: none !important;
}
}
svg {
vertical-align: baseline;
}
}

159
frontend/app/shared/components/assets/image-editor.component.ts

@ -0,0 +1,159 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, ViewChild } from '@angular/core';
import { ResourceLoaderService } from '@app/shared/internal';
declare var tui: any;
const blackTheme = {
'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png',
'common.bisize.width': '251px',
'common.bisize.height': '21px',
'common.backgroundImage': 'none',
'common.backgroundColor': '#000',
'common.border': '0px',
// header
'header.backgroundImage': 'none',
'header.backgroundColor': 'transparent',
'header.border': '0px',
// load button
'loadButton.backgroundColor': '#fff',
'loadButton.border': '1px solid #ddd',
'loadButton.color': '#222',
'loadButton.fontFamily': '\'Noto Sans\', sans-serif',
'loadButton.fontSize': '12px',
// download button
'downloadButton.backgroundColor': '#fdba3b',
'downloadButton.border': '1px solid #fdba3b',
'downloadButton.color': '#fff',
'downloadButton.fontFamily': '\'Noto Sans\', sans-serif',
'downloadButton.fontSize': '12px',
// main icons
'menu.normalIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-d.svg',
'menu.normalIcon.name': 'icon-d',
'menu.activeIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-b.svg',
'menu.activeIcon.name': 'icon-b',
'menu.disabledIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-a.svg',
'menu.disabledIcon.name': 'icon-a',
'menu.hoverIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-c.svg',
'menu.hoverIcon.name': 'icon-c',
'menu.iconSize.width': '24px',
'menu.iconSize.height': '24px',
// submenu primary color
'submenu.backgroundColor': '#000',
'submenu.partition.color': '#3c3c3c',
// submenu icons
'submenu.normalIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-d.svg',
'submenu.normalIcon.name': 'icon-d',
'submenu.activeIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-c.svg',
'submenu.activeIcon.name': 'icon-c',
'submenu.iconSize.width': '32px',
'submenu.iconSize.height': '32px',
// submenu labels
'submenu.normalLabel.color': '#8a8a8a',
'submenu.normalLabel.fontWeight': 'normal',
'submenu.activeLabel.color': '#fff',
'submenu.activeLabel.fontWeight': 'normal',
// checkbox style
'checkbox.border': '0px',
'checkbox.backgroundColor': '#fff',
// range style
'range.pointer.color': '#fff',
'range.bar.color': '#666',
'range.subbar.color': '#d1d1d1',
'range.disabledPointer.color': '#414141',
'range.disabledBar.color': '#282828',
'range.disabledSubbar.color': '#414141',
'range.value.color': '#fff',
'range.value.fontWeight': 'normal',
'range.value.fontSize': '11px',
'range.value.border': '1px solid #353535',
'range.value.backgroundColor': '#151515',
'range.title.color': '#fff',
'range.title.fontWeight': 'normal',
// colorpicker style
'colorpicker.button.border': '1px solid #1e1e1e',
'colorpicker.title.color': '#fff'
};
@Component({
selector: 'sqx-image-editor',
styleUrls: ['./image-editor.component.scss'],
templateUrl: './image-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImageEditorComponent implements AfterViewInit, OnChanges {
private imageEditor: any;
@Input()
public imageUrl: string;
@ViewChild('editor', { static: false })
public editor: ElementRef;
constructor(
private readonly resourceLoader: ResourceLoaderService
) {
}
public ngOnChanges() {
if (this.imageEditor && this.imageUrl) {
this.imageEditor.loadImageFromURL(this.imageUrl);
}
}
public ngAfterViewInit() {
const styles = [
'https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.css',
'https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.css'
];
const scripts = [
'https://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.3.2/fabric.js',
'https://uicdn.toast.com/tui.code-snippet/latest/tui-code-snippet.min.js',
'https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.js',
'https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.js'
];
styles.forEach(style => this.resourceLoader.loadStyle(style));
const s = scripts.map(script => this.resourceLoader.loadScript(script));
Promise.all(s).then(() => {
this.imageEditor = new tui.ImageEditor(this.editor.nativeElement, {
includeUI: {
loadImage: {
path: this.imageUrl, name: 'image'
},
menu: [
'crop',
'flip',
'rotate',
'mask',
'filter'
],
theme: blackTheme
}
});
});
}
}

2
frontend/app/shared/components/search/search-form.component.html

@ -41,7 +41,7 @@
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
<ng-container *sqxModal="searchDialog"> <ng-container *sqxModal="searchDialog">
<sqx-modal-dialog (close)="searchDialog.hide()" large="true"> <sqx-modal-dialog (close)="searchDialog.hide()" size="lg">
<ng-container title> <ng-container title>
Custom Query Custom Query
</ng-container> </ng-container>

1
frontend/app/shared/declarations.ts

@ -18,6 +18,7 @@ export * from './components/assets/asset-uploader.component';
export * from './components/assets/asset.component'; export * from './components/assets/asset.component';
export * from './components/assets/assets-list.component'; export * from './components/assets/assets-list.component';
export * from './components/assets/assets-selector.component'; export * from './components/assets/assets-selector.component';
export * from './components/assets/image-editor.component';
export * from './components/assets/pipes'; export * from './components/assets/pipes';
export * from './components/comments/comment.component'; export * from './components/comments/comment.component';

2
frontend/app/shared/module.ts

@ -61,6 +61,7 @@ import {
HistoryListComponent, HistoryListComponent,
HistoryMessagePipe, HistoryMessagePipe,
HistoryService, HistoryService,
ImageEditorComponent,
LanguageSelectorComponent, LanguageSelectorComponent,
LanguagesService, LanguagesService,
LanguagesState, LanguagesState,
@ -146,6 +147,7 @@ import {
HistoryComponent, HistoryComponent,
HistoryListComponent, HistoryListComponent,
HistoryMessagePipe, HistoryMessagePipe,
ImageEditorComponent,
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
QueryComponent, QueryComponent,

4
frontend/app/theme/_bootstrap.scss

@ -675,9 +675,9 @@ $icon-size: 4.5rem;
} }
&-content { &-content {
@include box-shadow(0, 6px, 16px, .4); @include box-shadow2(0, 8px, 16px, .3);
border: 0; border: 0;
border-radius: .4rem, .35rem, .35rem, .4rem; border-radius: .4rem .35rem .35rem .4rem;
max-height: 100%; max-height: 100%;
} }

4
frontend/app/theme/_mixins.scss

@ -170,6 +170,10 @@
box-shadow: $x-axis $y-axis $blur rgba(0, 0, 0, $alpha); box-shadow: $x-axis $y-axis $blur rgba(0, 0, 0, $alpha);
} }
@mixin box-shadow2($x-axis: 0, $y-axis: 1px, $blur: 2px, $alpha: .1) {
box-shadow: $x-axis $y-axis $blur rgba(0, 0, 0, $alpha);
}
@mixin box-shadow-inner($x-axis: 0, $y-axis: 1px, $blur: 2px, $alpha: .1) { @mixin box-shadow-inner($x-axis: 0, $y-axis: 1px, $blur: 2px, $alpha: .1) {
box-shadow: inset $x-axis $y-axis $blur rgba(0, 0, 0, $alpha); box-shadow: inset $x-axis $y-axis $blur rgba(0, 0, 0, $alpha);
} }

Loading…
Cancel
Save