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

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

@ -1,6 +1,7 @@
<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="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>

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

106
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 = `<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');
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')));
}));

30
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<AssetsDto> {
@ -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 = '<img src="' + this.assetUrlGenerator.transform(asset) + '" ';
res += 'width="' + asset.pixelWidth + '" height="' + asset.pixelHeight + '">';
return res;
}
}
function getFormData(file: File) {

Loading…
Cancel
Save