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. 12
      src/Squidex/app/shared/components/rich-editor.component.html
  9. 40
      src/Squidex/app/shared/components/rich-editor.component.scss
  10. 99
      src/Squidex/app/shared/components/rich-editor.component.ts

8
src/Squidex/Squidex.csproj

@ -22,6 +22,10 @@
<None Remove="Assets\**" />
</ItemGroup>
<ItemGroup>
<None Remove="app\shared\components\asset-drop.handler.ts" />
</ItemGroup>
<ItemGroup>
<None Update="dockerfile">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
@ -81,6 +85,10 @@
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="app\shared\components\asset-drop.handler.ts" />
</ItemGroup>
<Target Name="IncludeDocFile" BeforeTargets="PrepareForPublish">
<ItemGroup Condition=" '$(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>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControlName]="partition"></sqx-rich-editor>
<sqx-rich-editor [formControlName]="partition" [editorOptions]="richTextEditorOptions"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<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
*/
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppLanguageDto, FieldDto } from 'shared';
@ -32,6 +32,30 @@ export class ContentFieldComponent implements OnInit {
public fieldPartitions: 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) {
this.fieldPartition = language.iso2Code;
@ -39,6 +63,7 @@ export class ContentFieldComponent implements OnInit {
public ngOnInit() {
this.masterLanguageCode = this.languages.find(l => l.isMaster).iso2Code;
this.richTextEditorOptions = this.buildRichTextEditorOptions();
if (this.field.isDisabled) {
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">
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="assets" routerLinkActive="active">
<a class="panel-link" routerLink="assets" routerLinkActive="active" #assets>
<i class="icon-media"></i>
</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="file-preview" *ngIf="asset && progress == 0" @fade>
<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()
public deleting = new EventEmitter<AssetDto>();
@Output()
public clicked = new EventEmitter<AssetDto>();
@Output()
public failed = new EventEmitter();

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

@ -1,6 +1,14 @@
<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-dialog">
<div class="modal-content">
@ -37,4 +45,4 @@
</div>
</div>
</div>
</div>
</div>-->

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

@ -6,31 +6,29 @@
border: 1px solid $color-input;
height: 30rem;
}
.asset-selector {
z-index: 65560;
.editor-container {
position: relative;
.modal-header {
background: transparent;
border-bottom: 1px solid #c5c5c5;
.modal-title {
text-decoration: none;
color: #333;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
text-shadow: none;
}
.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;
}
.modal-content {
width: 100%;
border-radius: 0;
-webkit-border-radius: 0;
}
.modal-dialog {
max-width: 900px;
h3 {
text-align: center;
padding-top: 35%;
}
.btn {
border-radius: 0;
.drop-zone.active {
opacity: 1;
display:block;
}
}

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

@ -5,12 +5,13 @@
* 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 { AppComponentBase } from './app.component-base';
import { AssetUrlPipe } from './pipes';
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;
@ -36,9 +37,13 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
public assetsPager = new Pager(0, 0, 12);
private assetUrlGenerator: AssetUrlPipe;
private assetsMimeTypes: Array<string> = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
private assetDropHandler: AssetDropHandler;
public draggedOver = false;
@ViewChild('editor')
public editor: ElementRef;
@Input()
public editorOptions: any;
public assetsDialog = new ModalView();
public assetsForm = this.formBuilder.group({
@ -53,6 +58,7 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
) {
super(dialogs, apps, authService);
this.assetUrlGenerator = new AssetUrlPipe(this.apiUrlConfig);
this.assetDropHandler = new AssetDropHandler(this.apiUrlConfig);
}
private load() {
@ -66,29 +72,9 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
});
}
public goNext() {
this.assetsPager = this.assetsPager.goNext();
this.load();
}
public goPrev() {
this.assetsPager = this.assetsPager.goPrev();
this.load();
}
public ngOnDestroy() {
clearTimeout(this.tinyInitTimer);
tinymce.remove(this.editor);
}
public ngAfterViewInit() {
private editorDefaultOptions() {
const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.4/tinymce.min.js').then(() => {
tinymce.init({
return {
setup: (editor: any) => {
self.tinyEditor = editor;
self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design');
@ -107,20 +93,65 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
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', 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
removed_menuitems: 'newdocument', target: this.editor.nativeElement
};
}
});
public goNext() {
this.assetsPager = this.assetsPager.goNext();
this.load();
}
public goPrev() {
this.assetsPager = this.assetsPager.goPrev();
this.load();
}
public ngOnDestroy() {
clearTimeout(this.tinyInitTimer);
tinymce.remove(this.editor);
}
public ngAfterViewInit() {
const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.4/tinymce.min.js').then(() => {
let editorOptions = { ...self.editorDefaultOptions(), ...self.editorOptions };
console.log(editorOptions);
tinymce.init(editorOptions);
});
}
@ -163,4 +194,12 @@ export class RichEditorComponent extends AppComponentBase implements ControlValu
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