Browse Source

Copy and paste images.

pull/346/head
Sebastian Stehle 7 years ago
parent
commit
50e6ca95ab
  1. 2
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  2. 4
      src/Squidex/app/features/content/shared/assets-editor.component.html
  3. 22
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  4. 2
      src/Squidex/app/features/content/shared/references-editor.component.ts
  5. 56
      src/Squidex/app/framework/angular/forms/file-drop.directive.ts
  6. 2
      src/Squidex/app/shared/components/assets-list.component.html
  7. 22
      src/Squidex/app/shared/components/assets-list.component.ts
  8. 2
      src/Squidex/app/shared/components/assets-selector.component.ts
  9. 6
      src/Squidex/app/shared/components/markdown-editor.component.html
  10. 47
      src/Squidex/app/shared/components/markdown-editor.component.ts
  11. 6
      src/Squidex/app/shared/components/rich-editor.component.html
  12. 57
      src/Squidex/app/shared/components/rich-editor.component.ts
  13. 1
      src/Squidex/app/shared/state/contents.forms.ts

2
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';

4
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -1,8 +1,8 @@
<div class="assets-container" [class.disabled]="isDisabled" (paste)="pasteFiles($event)" tabindex="1000"> <div class="assets-container" [class.disabled]="isDisabled" (sqxFileDrop)="addFiles($event)" tabindex="1000">
<div class="header list"> <div class="header list">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<div class="drop-area align-items-center" (click)="assetsDialog.show()"> <div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxFileDrop)="addFiles($event)">
Drop files or click here to add assets. Drop files or click here to add assets.
</div> </div>
</div> </div>

22
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -127,23 +125,9 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
this.callTouched = fn; this.callTouched = fn;
} }
public pasteFiles(event: ClipboardEvent) { public addFiles(files: File[]) {
for (let i = 0; i < event.clipboardData.items.length; i++) { for (let file of files) {
const file = event.clipboardData.items[i].getAsFile(); this.newAssets = this.newAssets.pushFront(file);
if (file) {
this.newAssets = this.newAssets.pushFront(file);
}
}
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file) {
this.newAssets = this.newAssets.pushFront(file);
}
} }
} }

2
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

56
src/Squidex/app/framework/angular/forms/file-drop.directive.ts

@ -5,18 +5,33 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Directive, ElementRef, EventEmitter, HostListener, Output, Renderer2 } from '@angular/core'; // tslint:disable:prefer-for-of
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2 } from '@angular/core';
import { Types } from './../../utils/types'; import { Types } from './../../utils/types';
const ImageTypes = [
'image/jpeg',
'image/png',
'image/jpg',
'image/gif'
];
@Directive({ @Directive({
selector: '[sqxFileDrop]' selector: '[sqxFileDrop]'
}) })
export class FileDropDirective { export class FileDropDirective {
private dragCounter = 0; private dragCounter = 0;
@Input()
public allowedFiles: string[];
@Input()
public onlyImages: boolean;
@Output('sqxFileDrop') @Output('sqxFileDrop')
public drop = new EventEmitter<FileList>(); public drop = new EventEmitter<File[]>();
constructor( constructor(
private readonly element: ElementRef, private readonly element: ElementRef,
@ -24,6 +39,25 @@ export class FileDropDirective {
) { ) {
} }
@HostListener('paste', ['$event'])
public onPaste(event: ClipboardEvent) {
const result: File[] = [];
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (this.isAllowedFile(file)) {
result.push(file!);
}
}
if (result.length > 0) {
this.drop.emit(result);
}
this.stopEvent(event);
}
@HostListener('dragend', ['$event']) @HostListener('dragend', ['$event'])
@HostListener('dragleave', ['$event']) @HostListener('dragleave', ['$event'])
public onDragEnd(event: DragDropEvent) { public onDragEnd(event: DragDropEvent) {
@ -57,7 +91,19 @@ export class FileDropDirective {
const hasFiles = this.hasFiles(event.dataTransfer.types); const hasFiles = this.hasFiles(event.dataTransfer.types);
if (hasFiles) { if (hasFiles) {
this.drop.emit(event.dataTransfer.files); const result: File[] = [];
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i);
if (this.isAllowedFile(file)) {
result.push(file!);
}
}
if (result.length > 0) {
this.drop.emit(result);
}
this.dragEnd(0); this.dragEnd(0);
this.stopEvent(event); this.stopEvent(event);
@ -85,6 +131,10 @@ export class FileDropDirective {
} }
} }
private isAllowedFile(file: File | null) {
return file && (!this.allowedFiles || this.allowedFiles.indexOf(file.type) >= 0) && (!this.onlyImages || ImageTypes.indexOf(file.type) >= 0);
}
private hasFiles(types: any): boolean { private hasFiles(types: any): boolean {
if (!types) { if (!types) {
return false; return false;

2
src/Squidex/app/shared/components/assets-list.component.html

@ -1,4 +1,4 @@
<div class="file-drop" (sqxFileDrop)="addFiles($event)" *ngIf="!isDisabled" (paste)="pasteFiles($event)"> <div class="file-drop" (sqxFileDrop)="addFiles($event)" *ngIf="!isDisabled">
<h3 class="file-drop-header">Drop files here to upload</h3> <h3 class="file-drop-header">Drop files here to upload</h3>
<div class="file-drop-or">or</div> <div class="file-drop-or">or</div>

22
src/Squidex/app/shared/components/assets-list.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
@ -78,23 +76,9 @@ export class AssetsListComponent {
this.newFiles = this.newFiles.remove(file); this.newFiles = this.newFiles.remove(file);
} }
public pasteFiles(event: ClipboardEvent) { public addFiles(files: File[]) {
for (let i = 0; i < event.clipboardData.items.length; i++) { for (let file of files) {
const file = event.clipboardData.items[i].getAsFile(); this.newFiles = this.newFiles.pushFront(file);
if (file) {
this.newFiles = this.newFiles.pushFront(file);
}
}
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file) {
this.newFiles = this.newFiles.pushFront(file);
}
} }
return true; return true;

2
src/Squidex/app/shared/components/assets-selector.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';

6
src/Squidex/app/shared/components/markdown-editor.component.html

@ -1,11 +1,7 @@
<div #container class="drop-container"> <div #container class="drop-container" (sqxFileDrop)="insertFiles($event)" [onlyImages]="true">
<div #inner [class.fullscreen]="isFullscreen"> <div #inner [class.fullscreen]="isFullscreen">
<textarea class="form-control" #editor></textarea> <textarea class="form-control" #editor></textarea>
</div> </div>
<div class="file-drop drag drop-area">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div> </div>
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">

47
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -9,7 +9,11 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppsState,
AssetDto, AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
Types Types
@ -49,6 +53,9 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
public isFullscreen = false; public isFullscreen = false;
constructor( constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly changeDetector: ChangeDetectorRef, private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2, private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
@ -214,4 +221,44 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
this.assetsDialog.hide(); this.assetsDialog.hide();
} }
public insertFiles(files: File[]) {
const doc = this.simplemde.codemirror.getDoc();
for (let file of files) {
this.uploadFile(doc, file);
}
}
private uploadFile(doc: any, file: File) {
const uploadCursor = doc.getCursor();
const uploadText = `![Uploading file...${new Date()}]()`;
doc.replaceSelection(uploadText);
const replaceText = (replacement: string) => {
const cursor = doc.getCursor();
const text = doc.getValue().replace(uploadText, replacement);
doc.setValue(text);
if (uploadCursor && uploadCursor.line === cursor.line) {
const offset = replacement.length - uploadText.length;
doc.setCursor({ line: cursor.line, ch: cursor.ch + offset });
} else {
doc.setCursor(cursor);
}
};
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
replaceText(`![${asset.fileName}](${asset.url} '${asset.fileName}')`);
}
}, () => {
replaceText('FAILED');
});
}
} }

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

@ -1,9 +1,5 @@
<div class="drop-container"> <div class="drop-container" (sqxFileDrop)="insertFiles($event)" [onlyImages]="true">
<div class="editor" #editor></div> <div class="editor" #editor></div>
<div class="file-drop drag drop-area">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div> </div>
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">

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

@ -5,11 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppsState,
AssetDto, AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
Types Types
@ -21,6 +27,13 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true
}; };
const ImageTypes = [
'image/jpeg',
'image/png',
'image/jpg',
'image/gif'
];
@Component({ @Component({
selector: 'sqx-rich-editor', selector: 'sqx-rich-editor',
styleUrls: ['./rich-editor.component.scss'], styleUrls: ['./rich-editor.component.scss'],
@ -45,6 +58,9 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
public assetPluginClicked = new EventEmitter<any>(); public assetPluginClicked = new EventEmitter<any>();
constructor( constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly changeDetector: ChangeDetectorRef, private readonly changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
@ -59,7 +75,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
public ngAfterViewInit() { public ngAfterViewInit() {
const self = this; const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.4/tinymce.min.js').then(() => { this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.3/tinymce.min.js').then(() => {
tinymce.init(self.getEditorOptions()); tinymce.init(self.getEditorOptions());
}); });
} }
@ -76,10 +92,9 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
return { return {
convert_fonts_to_spans: true, convert_fonts_to_spans: true,
convert_urls: false, convert_urls: false,
plugins: 'code image media link lists advlist', plugins: 'code image media link lists advlist paste',
removed_menuitems: 'newdocument', removed_menuitems: 'newdocument',
resize: true, resize: true,
theme: 'modern',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets', toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets',
setup: (editor: any) => { setup: (editor: any) => {
self.tinyEditor = editor; self.tinyEditor = editor;
@ -102,6 +117,15 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
} }
}); });
self.tinyEditor.on('paste', (event: ClipboardEvent) => {
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (file && ImageTypes.indexOf(file.type) >= 0) {
self.uploadFile(file);
}
}
});
self.tinyEditor.on('blur', () => { self.tinyEditor.on('blur', () => {
self.callTouched(); self.callTouched();
}); });
@ -153,4 +177,31 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
this.assetsDialog.hide(); this.assetsDialog.hide();
} }
public insertFiles(files: File[]) {
for (let file of files) {
this.uploadFile(file);
}
}
private uploadFile(file: File) {
const uploadText = `[Uploading file...${new Date()}]`;
this.tinyEditor.execCommand('mceInsertContent', false, uploadText);
const replaceText = (replacement: string) => {
const content = this.tinyEditor.getContent().replace(uploadText, replacement);
this.tinyEditor.setContent(content);
};
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
replaceText(`<img src="${asset.url}" alt="${asset.fileName}" />`);
}
}, () => {
replaceText('FAILED');
});
}
} }

1
src/Squidex/app/shared/state/contents.forms.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of // tslint:disable:prefer-for-of
import { FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';

Loading…
Cancel
Save