Browse Source

Drag and drop support.

pull/167/merge
Sebastian Stehle 9 years ago
parent
commit
4d082978f7
  1. 9
      src/Squidex/app/shared/components/pipes.ts
  2. 7
      src/Squidex/app/shared/components/rich-editor.component.html
  3. 44
      src/Squidex/app/shared/components/rich-editor.component.scss
  4. 106
      src/Squidex/app/shared/components/rich-editor.component.ts
  5. 8
      src/Squidex/app/shared/services/assets.service.spec.ts
  6. 30
      src/Squidex/app/shared/services/assets.service.ts

9
src/Squidex/app/shared/components/pipes.ts

@ -199,13 +199,8 @@ export class UserPictureRefPipe extends UserAsyncPipe implements PipeTransform {
pure: true pure: true
}) })
export class AssetUrlPipe implements PipeTransform { export class AssetUrlPipe implements PipeTransform {
constructor( public transform(asset: { url: any }): string {
private readonly apiUrl: ApiUrlConfig return `${asset.url}?q=${MathHelper.guid()}`;
) {
}
public transform(asset: { id: any }): string {
return this.apiUrl.buildUrl(`api/assets/${asset.id}?q=${MathHelper.guid()}`);
} }
} }

7
src/Squidex/app/shared/components/rich-editor.component.html

@ -1,6 +1,7 @@
<div class="editor-container"> <div class="editor-container">
<div class="file-drop drag drop-zone" [class.active]="draggedOver" dnd-droppable (onDropSuccess)="onItemDropped($event)">
<h3>Drop asset in this zone to insert into content</h3>
</div>
<div class="editor" #editor></div> <div class="editor" #editor></div>
<div class="file-drop drag drop-area" [class.dragging]="draggedOver" dnd-droppable (onDropSuccess)="onItemDropped($event)">
<div class="drop-text">Drop files or assets here to add them.</div>
</div>
</div> </div>

44
src/Squidex/app/shared/components/rich-editor.component.scss

@ -6,29 +6,35 @@
border: 1px solid $color-input; border: 1px solid $color-input;
height: 30rem; height: 30rem;
} }
.editor-container { .editor-container {
position: relative; position: relative;
.drop-zone { .drop-area {
background: rgba(238, 241, 244, 0.89); & {
z-index: 5000; @include absolute(115px, 30px, 66px, 30px);
position: absolute; @include border-radius;
top: 92px; align-content: center;
left: 20px; align-items: center;
right: 30px; display: none;
border-color: #c8d2db; border: 2px dashed $color-border;
border-style: dashed; font-size: 1.2rem;
opacity: 0; font-weight: normal;
display: none; justify-content: center;
} color: darken($color-border, 30%);
}
h3 { &.dragging {
text-align: center; @include flex-box;
padding-top: 35%; }
}
.drop-zone.active { &.drag,
opacity: 1; &.dnd-drag-over,
display:block; &.dnd-drag-enter {
border-color: darken($color-border, 10%);
cursor: copy;
color: darken($color-border, 40%);
text-decoration: none;
}
} }
} }

106
src/Squidex/app/shared/components/rich-editor.component.ts

@ -5,10 +5,16 @@
* Copyright (c) Sebastian Stehle. All rights reserved * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core'; import { AfterViewInit, Component, forwardRef, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder } from '@angular/forms';
import { MessageBus, AssetDragged, AssetsService, Types, ResourceLoaderService } from './../declarations-base'; import {
AssetDto,
AssetDragged,
MessageBus,
ResourceLoaderService,
Types
} from './../declarations-base';
declare var tinymce: any; declare var tinymce: any;
@ -22,7 +28,7 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
templateUrl: './rich-editor.component.html', templateUrl: './rich-editor.component.html',
providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR]
}) })
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ }; private callTouched = () => { /* NOOP */ };
private tinyEditor: any; private tinyEditor: any;
@ -45,32 +51,63 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
constructor(private readonly resourceLoader: ResourceLoaderService, constructor(private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus
private readonly assetsService: AssetsService
) { ) {
this.assetDraggedSubscription = this.messageBus.of(AssetDragged).subscribe(message => { }
// only handle images for now
if (message.assetDto.isImage) { public ngOnDestroy() {
if (message.dragEvent === AssetDragged.DRAG_START) { clearTimeout(this.tinyInitTimer);
this.draggedOver = true;
} else { tinymce.remove(this.editor);
this.draggedOver = false;
this.assetDraggedSubscription.unsubscribe();
}
public ngOnInit() {
this.assetDraggedSubscription =
this.messageBus.of(AssetDragged).subscribe(message => {
if (message.assetDto.isImage) {
if (message.dragEvent === AssetDragged.DRAG_START) {
this.draggedOver = true;
} else {
this.draggedOver = false;
}
} }
} });
}
public ngAfterViewInit() {
const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.4/tinymce.min.js').then(() => {
tinymce.init(self.getEditorOptions());
}); });
} }
private getEditorOptions() { private getEditorOptions() {
const self = this; const self = this;
return { return {
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media assets', convert_fonts_to_spans: true,
plugins: 'code,image,media',
file_picker_types: 'image',
convert_urls: false, convert_urls: false,
plugins: 'code image media',
removed_menuitems: 'newdocument',
resize: true,
theme: 'modern',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media assets',
setup: (editor: any) => { setup: (editor: any) => {
self.tinyEditor = editor; self.tinyEditor = editor;
self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design'); self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design');
self.tinyEditor.addButton('assets', {
text: '',
icon: 'browse',
tooltip: 'Insert Assets',
onclick: (event: any) => {
self.assetPluginClicked.emit(event);
}
});
self.tinyEditor.on('change', () => { self.tinyEditor.on('change', () => {
const value = editor.getContent(); const value = editor.getContent();
@ -85,36 +122,14 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
self.callTouched(); self.callTouched();
}); });
editor.addButton('assets', {
text: '',
icon: 'browse',
tooltip: 'Insert Assets',
onclick: (event: any) => {
self.assetPluginClicked.emit(event);
}
});
self.tinyInitTimer = self.tinyInitTimer =
setTimeout(() => { setTimeout(() => {
self.tinyEditor.setContent(this.value || ''); self.tinyEditor.setContent(this.value || '');
}, 500); }, 500);
}, },
removed_menuitems: 'newdocument', target: this.editor.nativeElement
};
}
public ngOnDestroy() {
clearTimeout(this.tinyInitTimer);
tinymce.remove(this.editor); target: this.editor.nativeElement
this.assetDraggedSubscription.unsubscribe(); };
}
public ngAfterViewInit() {
const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.4/tinymce.min.js').then(() => {
tinymce.init(self.getEditorOptions());
});
} }
public writeValue(value: string) { public writeValue(value: string) {
@ -142,9 +157,14 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
} }
public onItemDropped(event: any) { public onItemDropped(event: any) {
let content = this.assetsService.buildDroppedAssetData(event.dragData, event.mouseEvent); const content = event.dragData;
if (content) {
this.tinyEditor.execCommand('mceInsertContent', false, content); if (content instanceof AssetDto) {
const img = `<img src="${content.url}" alt="${content.fileName}" />`;
this.tinyEditor.execCommand('mceInsertContent', false, img);
} }
this.draggedOver = false;
} }
} }

8
src/Squidex/app/shared/services/assets.service.spec.ts

@ -31,7 +31,7 @@ describe('AssetDto', () => {
const newVersion = new Version('2'); const newVersion = new Version('2');
it('should update name property and user info when renaming', () => { it('should update name property and user info when renaming', () => {
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version); const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'url', version);
const asset_2 = asset_1.rename('new-name.png', modifier, newVersion, modified); const asset_2 = asset_1.rename('new-name.png', modifier, newVersion, modified);
expect(asset_2.fileName).toEqual('new-name.png'); expect(asset_2.fileName).toEqual('new-name.png');
@ -43,7 +43,7 @@ describe('AssetDto', () => {
it('should update file properties when uploading', () => { it('should update file properties when uploading', () => {
const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2); const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2);
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version); const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'url', version);
const asset_2 = asset_1.update(update, modifier, newVersion, modified); const asset_2 = asset_1.update(update, modifier, newVersion, modified);
expect(asset_2.fileSize).toEqual(2); expect(asset_2.fileSize).toEqual(2);
@ -147,6 +147,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
'http://service/p/api/assets/id1',
new Version('11')), new Version('11')),
new AssetDto('id2', 'Created2', 'LastModifiedBy2', new AssetDto('id2', 'Created2', 'LastModifiedBy2',
DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2016-10-12T10:10'),
@ -159,6 +160,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
'http://service/p/api/assets/id2',
new Version('22')) new Version('22'))
])); ]));
})); }));
@ -210,6 +212,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
'http://service/p/api/assets/id1',
new Version('2'))); new Version('2')));
})); }));
@ -319,6 +322,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
'http://service/p/api/assets/id1',
new Version('2'))); new Version('2')));
})); }));

30
src/Squidex/app/shared/services/assets.service.ts

@ -19,8 +19,6 @@ import {
Versioned Versioned
} from 'framework'; } from 'framework';
import { AssetUrlPipe } from 'shared';
export class AssetsDto { export class AssetsDto {
constructor( constructor(
public readonly total: number, public readonly total: number,
@ -44,6 +42,7 @@ export class AssetDto {
public readonly isImage: boolean, public readonly isImage: boolean,
public readonly pixelWidth: number | null, public readonly pixelWidth: number | null,
public readonly pixelHeight: number | null, public readonly pixelHeight: number | null,
public readonly url: string,
public readonly version: Version public readonly version: Version
) { ) {
} }
@ -61,6 +60,7 @@ export class AssetDto {
update.isImage, update.isImage,
update.pixelWidth, update.pixelWidth,
update.pixelHeight, update.pixelHeight,
this.url,
version); version);
} }
@ -77,6 +77,7 @@ export class AssetDto {
this.isImage, this.isImage,
this.pixelWidth, this.pixelWidth,
this.pixelHeight, this.pixelHeight,
this.url,
version); version);
} }
} }
@ -102,16 +103,12 @@ export class AssetReplacedDto {
@Injectable() @Injectable()
export class AssetsService { export class AssetsService {
private assetUrlGenerator: AssetUrlPipe;
constructor( constructor(
private readonly http: HttpClient, private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig, private readonly apiUrl: ApiUrlConfig,
private readonly analytics: AnalyticsService, private readonly analytics: AnalyticsService,
private readonly localCache: LocalCacheService private readonly localCache: LocalCacheService
) { ) {
this.assetUrlGenerator = new AssetUrlPipe(this.apiUrl);
} }
public getAssets(appName: string, take: number, skip: number, query?: string, mimeTypes?: string[], ids?: string[]): Observable<AssetsDto> { public getAssets(appName: string, take: number, skip: number, query?: string, mimeTypes?: string[], ids?: string[]): Observable<AssetsDto> {
@ -143,6 +140,8 @@ export class AssetsService {
const items: any[] = body.items; const items: any[] = body.items;
return new AssetsDto(body.total, items.map(item => { return new AssetsDto(body.total, items.map(item => {
const assetUrl = this.apiUrl.buildUrl(`api/assets/${item.id}`);
return new AssetDto( return new AssetDto(
item.id, item.id,
item.createdBy, item.createdBy,
@ -157,6 +156,7 @@ export class AssetsService {
item.isImage, item.isImage,
item.pixelWidth, item.pixelWidth,
item.pixelHeight, item.pixelHeight,
assetUrl,
new Version(item.version.toString())); new Version(item.version.toString()));
})); }));
}) })
@ -181,6 +181,7 @@ export class AssetsService {
return percentDone; return percentDone;
} else if (event instanceof HttpResponse) { } else if (event instanceof HttpResponse) {
const response: any = event.body; const response: any = event.body;
const assetUrl = this.apiUrl.buildUrl(`api/assets/${response.id}`);
now = now || DateTime.now(); now = now || DateTime.now();
@ -198,6 +199,7 @@ export class AssetsService {
response.isImage, response.isImage,
response.pixelWidth, response.pixelWidth,
response.pixelHeight, response.pixelHeight,
assetUrl,
new Version(event.headers.get('etag'))); new Version(event.headers.get('etag')));
this.localCache.set(`asset.${dto.id}`, dto, 5000); this.localCache.set(`asset.${dto.id}`, dto, 5000);
@ -218,6 +220,8 @@ export class AssetsService {
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
const assetUrl = this.apiUrl.buildUrl(`api/assets/${body.id}`);
return new AssetDto( return new AssetDto(
body.id, body.id,
body.createdBy, body.createdBy,
@ -232,6 +236,7 @@ export class AssetsService {
body.isImage, body.isImage,
body.pixelWidth, body.pixelWidth,
body.pixelHeight, body.pixelHeight,
assetUrl,
response.version); response.version);
}) })
.catch(error => { .catch(error => {
@ -308,19 +313,6 @@ export class AssetsService {
}) })
.pretifyError('Failed to delete asset. Please reload.'); .pretifyError('Failed to delete asset. Please reload.');
} }
public buildDroppedAssetData(asset: AssetDto, dragEvent: DragEvent) {
if (asset.isImage) {
return this.handleImageAsset(asset, dragEvent);
}
return '';
}
private handleImageAsset(asset: AssetDto, dragEvent: DragEvent) {
let res = '<img src="' + this.assetUrlGenerator.transform(asset) + '" ';
res += 'width="' + asset.pixelWidth + '" height="' + asset.pixelHeight + '">';
return res;
}
} }
function getFormData(file: File) { function getFormData(file: File) {

Loading…
Cancel
Save