From 4d082978f705d63205708325963422c894e4e118 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 9 Nov 2017 17:55:40 +0100 Subject: [PATCH] Drag and drop support. --- src/Squidex/app/shared/components/pipes.ts | 9 +- .../components/rich-editor.component.html | 7 +- .../components/rich-editor.component.scss | 44 ++++---- .../components/rich-editor.component.ts | 106 +++++++++++------- .../shared/services/assets.service.spec.ts | 8 +- .../app/shared/services/assets.service.ts | 30 ++--- 6 files changed, 111 insertions(+), 93 deletions(-) diff --git a/src/Squidex/app/shared/components/pipes.ts b/src/Squidex/app/shared/components/pipes.ts index 934a95a39..30d5a3c8c 100644 --- a/src/Squidex/app/shared/components/pipes.ts +++ b/src/Squidex/app/shared/components/pipes.ts @@ -199,13 +199,8 @@ export class UserPictureRefPipe extends UserAsyncPipe implements PipeTransform { pure: true }) export class AssetUrlPipe implements PipeTransform { - constructor( - private readonly apiUrl: ApiUrlConfig - ) { - } - - public transform(asset: { id: any }): string { - return this.apiUrl.buildUrl(`api/assets/${asset.id}?q=${MathHelper.guid()}`); + public transform(asset: { url: any }): string { + return `${asset.url}?q=${MathHelper.guid()}`; } } diff --git a/src/Squidex/app/shared/components/rich-editor.component.html b/src/Squidex/app/shared/components/rich-editor.component.html index eb0a1fafb..76fb22df6 100644 --- a/src/Squidex/app/shared/components/rich-editor.component.html +++ b/src/Squidex/app/shared/components/rich-editor.component.html @@ -1,6 +1,7 @@
-
-

Drop asset in this zone to insert into content

-
+ +
+
Drop files or assets here to add them.
+
\ No newline at end of file diff --git a/src/Squidex/app/shared/components/rich-editor.component.scss b/src/Squidex/app/shared/components/rich-editor.component.scss index 24e175db7..a5fb04b83 100644 --- a/src/Squidex/app/shared/components/rich-editor.component.scss +++ b/src/Squidex/app/shared/components/rich-editor.component.scss @@ -6,29 +6,35 @@ border: 1px solid $color-input; height: 30rem; } + .editor-container { position: relative; - .drop-zone { - background: rgba(238, 241, 244, 0.89); - z-index: 5000; - position: absolute; - top: 92px; - left: 20px; - right: 30px; - border-color: #c8d2db; - border-style: dashed; - opacity: 0; - display: none; - } + .drop-area { + & { + @include absolute(115px, 30px, 66px, 30px); + @include border-radius; + align-content: center; + align-items: center; + display: none; + border: 2px dashed $color-border; + font-size: 1.2rem; + font-weight: normal; + justify-content: center; + color: darken($color-border, 30%); + } - h3 { - text-align: center; - padding-top: 35%; - } + &.dragging { + @include flex-box; + } - .drop-zone.active { - opacity: 1; - display:block; + &.drag, + &.dnd-drag-over, + &.dnd-drag-enter { + border-color: darken($color-border, 10%); + cursor: copy; + color: darken($color-border, 40%); + text-decoration: none; + } } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/rich-editor.component.ts b/src/Squidex/app/shared/components/rich-editor.component.ts index 3104b0c84..ea61d29ae 100644 --- a/src/Squidex/app/shared/components/rich-editor.component.ts +++ b/src/Squidex/app/shared/components/rich-editor.component.ts @@ -5,10 +5,16 @@ * 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 { MessageBus, AssetDragged, AssetsService, Types, ResourceLoaderService } from './../declarations-base'; +import { + AssetDto, + AssetDragged, + MessageBus, + ResourceLoaderService, + Types +} from './../declarations-base'; declare var tinymce: any; @@ -22,7 +28,7 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { templateUrl: './rich-editor.component.html', 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 callTouched = () => { /* NOOP */ }; private tinyEditor: any; @@ -45,32 +51,63 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, constructor(private readonly resourceLoader: ResourceLoaderService, private readonly formBuilder: FormBuilder, - private readonly messageBus: MessageBus, - private readonly assetsService: AssetsService + private readonly messageBus: MessageBus ) { - this.assetDraggedSubscription = this.messageBus.of(AssetDragged).subscribe(message => { - // only handle images for now - if (message.assetDto.isImage) { - if (message.dragEvent === AssetDragged.DRAG_START) { - this.draggedOver = true; - } else { - this.draggedOver = false; + } + + public ngOnDestroy() { + clearTimeout(this.tinyInitTimer); + + tinymce.remove(this.editor); + + 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() { const self = this; + return { - toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media assets', - plugins: 'code,image,media', - file_picker_types: 'image', + convert_fonts_to_spans: true, 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) => { self.tinyEditor = editor; 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', () => { const value = editor.getContent(); @@ -85,36 +122,14 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, self.callTouched(); }); - editor.addButton('assets', { - text: '', - icon: 'browse', - tooltip: 'Insert Assets', - onclick: (event: any) => { - self.assetPluginClicked.emit(event); - } - }); - self.tinyInitTimer = setTimeout(() => { self.tinyEditor.setContent(this.value || ''); }, 500); }, - removed_menuitems: 'newdocument', target: this.editor.nativeElement - }; - } - - public ngOnDestroy() { - clearTimeout(this.tinyInitTimer); - tinymce.remove(this.editor); - 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()); - }); + target: this.editor.nativeElement + }; } public writeValue(value: string) { @@ -142,9 +157,14 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, } public onItemDropped(event: any) { - let content = this.assetsService.buildDroppedAssetData(event.dragData, event.mouseEvent); - if (content) { - this.tinyEditor.execCommand('mceInsertContent', false, content); + const content = event.dragData; + + if (content instanceof AssetDto) { + const img = `${content.fileName}`; + + this.tinyEditor.execCommand('mceInsertContent', false, img); } + + this.draggedOver = false; } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index cfda97cef..25a6bc090 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -31,7 +31,7 @@ describe('AssetDto', () => { const newVersion = new Version('2'); 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); expect(asset_2.fileName).toEqual('new-name.png'); @@ -43,7 +43,7 @@ describe('AssetDto', () => { it('should update file properties when uploading', () => { 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); expect(asset_2.fileSize).toEqual(2); @@ -147,6 +147,7 @@ describe('AssetsService', () => { true, 1024, 2048, + 'http://service/p/api/assets/id1', new Version('11')), new AssetDto('id2', 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), @@ -159,6 +160,7 @@ describe('AssetsService', () => { true, 1024, 2048, + 'http://service/p/api/assets/id2', new Version('22')) ])); })); @@ -210,6 +212,7 @@ describe('AssetsService', () => { true, 1024, 2048, + 'http://service/p/api/assets/id1', new Version('2'))); })); @@ -319,6 +322,7 @@ describe('AssetsService', () => { true, 1024, 2048, + 'http://service/p/api/assets/id1', new Version('2'))); })); diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 5fcda2bbb..5bf2850a7 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -19,8 +19,6 @@ import { Versioned } from 'framework'; -import { AssetUrlPipe } from 'shared'; - export class AssetsDto { constructor( public readonly total: number, @@ -44,6 +42,7 @@ export class AssetDto { public readonly isImage: boolean, public readonly pixelWidth: number | null, public readonly pixelHeight: number | null, + public readonly url: string, public readonly version: Version ) { } @@ -61,6 +60,7 @@ export class AssetDto { update.isImage, update.pixelWidth, update.pixelHeight, + this.url, version); } @@ -77,6 +77,7 @@ export class AssetDto { this.isImage, this.pixelWidth, this.pixelHeight, + this.url, version); } } @@ -102,16 +103,12 @@ export class AssetReplacedDto { @Injectable() export class AssetsService { - - private assetUrlGenerator: AssetUrlPipe; - constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, private readonly analytics: AnalyticsService, 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 { @@ -143,6 +140,8 @@ export class AssetsService { const items: any[] = body.items; return new AssetsDto(body.total, items.map(item => { + const assetUrl = this.apiUrl.buildUrl(`api/assets/${item.id}`); + return new AssetDto( item.id, item.createdBy, @@ -157,6 +156,7 @@ export class AssetsService { item.isImage, item.pixelWidth, item.pixelHeight, + assetUrl, new Version(item.version.toString())); })); }) @@ -181,6 +181,7 @@ export class AssetsService { return percentDone; } else if (event instanceof HttpResponse) { const response: any = event.body; + const assetUrl = this.apiUrl.buildUrl(`api/assets/${response.id}`); now = now || DateTime.now(); @@ -198,6 +199,7 @@ export class AssetsService { response.isImage, response.pixelWidth, response.pixelHeight, + assetUrl, new Version(event.headers.get('etag'))); this.localCache.set(`asset.${dto.id}`, dto, 5000); @@ -218,6 +220,8 @@ export class AssetsService { .map(response => { const body = response.payload.body; + const assetUrl = this.apiUrl.buildUrl(`api/assets/${body.id}`); + return new AssetDto( body.id, body.createdBy, @@ -232,6 +236,7 @@ export class AssetsService { body.isImage, body.pixelWidth, body.pixelHeight, + assetUrl, response.version); }) .catch(error => { @@ -308,19 +313,6 @@ export class AssetsService { }) .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 = ''; - return res; - } } function getFormData(file: File) {