Browse Source

rich text editor assets drag and drop

pull/159/head
sowobm 8 years ago
parent
commit
bf68035f8a
  1. 8
      src/Squidex/Squidex.csproj
  2. 2
      src/Squidex/app/features/content/pages/content/content-field.component.html
  3. 27
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  4. 2
      src/Squidex/app/features/content/pages/content/content-page.component.html
  5. 37
      src/Squidex/app/shared/components/asset-drop.handler.ts
  6. 2
      src/Squidex/app/shared/components/asset.component.html
  7. 3
      src/Squidex/app/shared/components/asset.component.ts
  8. 14
      src/Squidex/app/shared/components/rich-editor.component.html
  9. 40
      src/Squidex/app/shared/components/rich-editor.component.scss
  10. 107
      src/Squidex/app/shared/components/rich-editor.component.ts

8
src/Squidex/Squidex.csproj

@ -22,6 +22,10 @@
<None Remove="Assets\**" /> <None Remove="Assets\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="app\shared\components\asset-drop.handler.ts" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="dockerfile"> <None Update="dockerfile">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
@ -81,6 +85,10 @@
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="app\shared\components\asset-drop.handler.ts" />
</ItemGroup>
<Target Name="IncludeDocFile" BeforeTargets="PrepareForPublish"> <Target Name="IncludeDocFile" BeforeTargets="PrepareForPublish">
<ItemGroup Condition=" '$(DocumentationFile)' != '' "> <ItemGroup Condition=" '$(DocumentationFile)' != '' ">
<_DocumentationFile Include="$(DocumentationFile)" /> <_DocumentationFile Include="$(DocumentationFile)" />

2
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -53,7 +53,7 @@
<textarea class="form-control" [formControlName]="partition" rows="5"></textarea> <textarea class="form-control" [formControlName]="partition" rows="5"></textarea>
</div> </div>
<div *ngSwitchCase="'RichText'"> <div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControlName]="partition"></sqx-rich-editor> <sqx-rich-editor [formControlName]="partition" [editorOptions]="richTextEditorOptions"></sqx-rich-editor>
</div> </div>
<div *ngSwitchCase="'Markdown'"> <div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControlName]="partition"></sqx-markdown-editor> <sqx-markdown-editor [formControlName]="partition"></sqx-markdown-editor>

27
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights reserved * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { AppLanguageDto, FieldDto } from 'shared'; import { AppLanguageDto, FieldDto } from 'shared';
@ -32,6 +32,30 @@ export class ContentFieldComponent implements OnInit {
public fieldPartitions: string[]; public fieldPartitions: string[];
public fieldPartition: string; public fieldPartition: string;
public richTextEditorOptions: any;
@ViewChild('assets')
public assetLink: ElementRef;
private buildRichTextEditorOptions() {
// const self = this;
return {
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image assets',
plugins: 'code,image',
file_picker_types: 'image',
convert_urls: false,
onSetup: (editor: any) => {
editor.addButton('assets', {
text: '',
icon: 'browse',
tooltip: 'Insert Assets',
onclick: () => {
console.log(this.assetLink);
}
});
}
};
}
public selectLanguage(language: AppLanguageDto) { public selectLanguage(language: AppLanguageDto) {
this.fieldPartition = language.iso2Code; this.fieldPartition = language.iso2Code;
@ -39,6 +63,7 @@ export class ContentFieldComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.masterLanguageCode = this.languages.find(l => l.isMaster).iso2Code; this.masterLanguageCode = this.languages.find(l => l.isMaster).iso2Code;
this.richTextEditorOptions = this.buildRichTextEditorOptions();
if (this.field.isDisabled) { if (this.field.isDisabled) {
this.fieldForm.disable(); this.fieldForm.disable();

2
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -49,7 +49,7 @@
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode"> <a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode">
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<a class="panel-link" routerLink="assets" routerLinkActive="active"> <a class="panel-link" routerLink="assets" routerLinkActive="active" #assets>
<i class="icon-media"></i> <i class="icon-media"></i>
</a> </a>

37
src/Squidex/app/shared/components/asset-drop.handler.ts

@ -0,0 +1,37 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { AssetUrlPipe } from './pipes';
import { ApiUrlConfig, AssetDto } from './../declarations-base';
export class AssetDropHandler {
private assetUrlGenerator: AssetUrlPipe;
constructor(private readonly apiUrlConfig: ApiUrlConfig) {
this.assetUrlGenerator = new AssetUrlPipe(this.apiUrlConfig);
}
public buildDroppedAssetData(asset: AssetDto, dragEvent: DragEvent) {
switch (asset.mimeType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
case 'image/gif':
return this.handleImageAsset(asset, dragEvent);
default:
return '';
}
}
private handleImageAsset(asset: AssetDto, dragEvent: DragEvent) {
let res = '<img src="' + this.assetUrlGenerator.transform(asset) + '" ';
res += 'width="' + asset.pixelWidth + '" height="' + asset.pixelHeight + '">';
return res;
}
}

2
src/Squidex/app/shared/components/asset.component.html

@ -1,4 +1,4 @@
<div class="card" (sqxFileDrop)="updateFile($event)" dnd-draggable [dragEnabled]="!!asset" [dragData]="asset" (click)="clicked.emit(asset)"> <div class="card" (sqxFileDrop)="updateFile($event)" dnd-draggable [dragEnabled]="!!asset" [dragData]="asset">
<div class="card-block"> <div class="card-block">
<div class="file-preview" *ngIf="asset && progress == 0" @fade> <div class="file-preview" *ngIf="asset && progress == 0" @fade>
<span class="file-type" *ngIf="asset.fileType"> <span class="file-type" *ngIf="asset.fileType">

3
src/Squidex/app/shared/components/asset.component.ts

@ -59,9 +59,6 @@ export class AssetComponent extends AppComponentBase implements OnInit {
@Output() @Output()
public deleting = new EventEmitter<AssetDto>(); public deleting = new EventEmitter<AssetDto>();
@Output()
public clicked = new EventEmitter<AssetDto>();
@Output() @Output()
public failed = new EventEmitter(); public failed = new EventEmitter();

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

@ -1,6 +1,14 @@
<div class="editor" #editor></div> <div class="editor-container">
<div class="file-drop drag drop-zone" [class.active]="draggedOver" dnd-droppable
(onDropSuccess)="onItemDropped($event)"
(onDragEnter)="draggedOver=true"
(onDragLeave)="draggedOver=false">
<h3>Drop asset in this zone to insert into content</h3>
</div>
<div class="editor" #editor></div>
</div>
<div class="modal asset-selector" *sqxModalView="assetsDialog;onRoot:true"> <!--<div class="modal asset-selector" *sqxModalView="assetsDialog;onRoot:true">
<div class="modal-backdrop"></div> <div class="modal-backdrop"></div>
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@ -37,4 +45,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>-->

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

@ -6,31 +6,29 @@
border: 1px solid $color-input; border: 1px solid $color-input;
height: 30rem; height: 30rem;
} }
.asset-selector { .editor-container {
z-index: 65560; position: relative;
.modal-header { .drop-zone {
background: transparent; background: rgba(238, 241, 244, 0.89);
border-bottom: 1px solid #c5c5c5; z-index: 5000;
position: absolute;
.modal-title { top: 92px;
text-decoration: none; left: 20px;
color: #333; right: 30px;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; border-color: #c8d2db;
text-shadow: none; border-style: dashed;
} opacity: 0;
display: none;
} }
.modal-content { h3 {
width: 100%; text-align: center;
border-radius: 0; padding-top: 35%;
-webkit-border-radius: 0;
}
.modal-dialog {
max-width: 900px;
} }
.btn { .drop-zone.active {
border-radius: 0; opacity: 1;
display:block;
} }
} }

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

@ -5,12 +5,13 @@
* Copyright (c) Sebastian Stehle. All rights reserved * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core'; import { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder } from '@angular/forms';
import { AppComponentBase } from './app.component-base'; import { AppComponentBase } from './app.component-base';
import { AssetUrlPipe } from './pipes'; import { AssetUrlPipe } from './pipes';
import { ApiUrlConfig, ModalView, AppsStoreService, AssetDto, AssetsService, ImmutableArray, DialogService, AuthService, Pager, Types, ResourceLoaderService } from './../declarations-base'; import { ApiUrlConfig, ModalView, AppsStoreService, AssetDto, AssetsService, ImmutableArray, DialogService, AuthService, Pager, Types, ResourceLoaderService } from './../declarations-base';
import { AssetDropHandler } from './asset-drop.handler';
declare var tinymce: any; declare var tinymce: any;
@ -36,9 +37,13 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
public assetsPager = new Pager(0, 0, 12); public assetsPager = new Pager(0, 0, 12);
private assetUrlGenerator: AssetUrlPipe; private assetUrlGenerator: AssetUrlPipe;
private assetsMimeTypes: Array<string> = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']; private assetsMimeTypes: Array<string> = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
private assetDropHandler: AssetDropHandler;
public draggedOver = false;
@ViewChild('editor') @ViewChild('editor')
public editor: ElementRef; public editor: ElementRef;
@Input()
public editorOptions: any;
public assetsDialog = new ModalView(); public assetsDialog = new ModalView();
public assetsForm = this.formBuilder.group({ public assetsForm = this.formBuilder.group({
@ -53,6 +58,7 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
) { ) {
super(dialogs, apps, authService); super(dialogs, apps, authService);
this.assetUrlGenerator = new AssetUrlPipe(this.apiUrlConfig); this.assetUrlGenerator = new AssetUrlPipe(this.apiUrlConfig);
this.assetDropHandler = new AssetDropHandler(this.apiUrlConfig);
} }
private load() { private load() {
@ -66,6 +72,61 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
}); });
} }
private editorDefaultOptions() {
const self = this;
return {
setup: (editor: any) => {
self.tinyEditor = editor;
self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design');
self.tinyEditor.on('change', () => {
const value = editor.getContent();
if (this.value !== value) {
this.value = value;
self.callChange(value);
}
});
self.tinyEditor.on('blur', () => {
self.callTouched();
});
self.tinyEditor.on('init', () => {
let editorDoc = self.tinyEditor.iframeElement;
let dragTarget: any = null;
editorDoc.ondragenter = (event: any) => {
console.log('dragenter');
self.draggedOver = true;
dragTarget = event.target;
};
editorDoc.ondragleave = (event: any) => {
if (event.target === dragTarget) {
self.draggedOver = false;
console.log('dragleave');
} else {
self.draggedOver = true;
}
};
});
// TODO: expose an observable to which we can subscribe to
if (Types.isFunction(self.editorOptions.onSetup)) {
self.editorOptions.onSetup(editor);
}
this.tinyInitTimer =
setTimeout(() => {
self.tinyEditor.setContent(this.value || '');
}, 500);
},
removed_menuitems: 'newdocument', target: this.editor.nativeElement
};
}
public goNext() { public goNext() {
this.assetsPager = this.assetsPager.goNext(); this.assetsPager = this.assetsPager.goNext();
@ -88,39 +149,9 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
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.5.4/tinymce.min.js').then(() => {
tinymce.init({ let editorOptions = { ...self.editorDefaultOptions(), ...self.editorOptions };
setup: (editor: any) => { console.log(editorOptions);
self.tinyEditor = editor; tinymce.init(editorOptions);
self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design');
self.tinyEditor.on('change', () => {
const value = editor.getContent();
if (this.value !== value) {
this.value = value;
self.callChange(value);
}
});
self.tinyEditor.on('blur', () => {
self.callTouched();
});
this.tinyInitTimer =
setTimeout(() => {
self.tinyEditor.setContent(this.value || '');
}, 500);
},
removed_menuitems: 'newdocument', plugins: 'code,image', target: this.editor.nativeElement, file_picker_types: 'image', convert_urls: false, file_picker_callback: (cb: any, value: any, meta: any) => {
self.load();
self.assetsDialog.show();
self.assetSelectorClickHandler = {
cb: cb,
meta: meta
};
}
});
}); });
} }
@ -163,4 +194,12 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
this.closeAssetDialog(); this.closeAssetDialog();
} }
} }
public onItemDropped(event: any) {
this.draggedOver = false;
let content = this.assetDropHandler.buildDroppedAssetData(event.dragData, event.mouseEvent);
if (content) {
this.tinyEditor.execCommand('mceInsertContent', false, content);
}
}
} }
Loading…
Cancel
Save