diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 113649272..226c6d008 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -53,7 +53,7 @@
- +
diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/src/Squidex/app/features/content/pages/content/content-field.component.ts index 4625524d7..97b8a6bf0 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-field.component.ts @@ -9,6 +9,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { AppLanguageDto, FieldDto } from 'shared'; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'sqx-content-field', @@ -16,6 +17,8 @@ import { AppLanguageDto, FieldDto } from 'shared'; templateUrl: './content-field.component.html' }) export class ContentFieldComponent implements OnInit { + constructor(private readonly router: Router, private readonly route: ActivatedRoute) { + } private masterLanguageCode: string; @Input() @@ -39,7 +42,6 @@ export class ContentFieldComponent implements OnInit { public ngOnInit() { this.masterLanguageCode = this.languages.find(l => l.isMaster).iso2Code; - if (this.field.isDisabled) { this.fieldForm.disable(); } @@ -53,6 +55,10 @@ export class ContentFieldComponent implements OnInit { } } + public richTextEditorAssetPluginClicked(event: any) { + this.router.navigate(['assets'], { relativeTo: this.route }); + } + public selectFieldLanguage(partition: string) { return partition === 'iv' ? this.masterLanguageCode : partition; } diff --git a/src/Squidex/app/framework/angular/rich-editor.component.html b/src/Squidex/app/framework/angular/rich-editor.component.html deleted file mode 100644 index bb84a4048..000000000 --- a/src/Squidex/app/framework/angular/rich-editor.component.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/rich-editor.component.scss b/src/Squidex/app/framework/angular/rich-editor.component.scss deleted file mode 100644 index 07b2c640b..000000000 --- a/src/Squidex/app/framework/angular/rich-editor.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import '_mixins'; -@import '_vars'; - -.editor { - background: $color-dark-foreground; - border: 1px solid $color-input; - height: 30rem; -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/rich-editor.component.ts b/src/Squidex/app/framework/angular/rich-editor.component.ts deleted file mode 100644 index 4e95a8152..000000000 --- a/src/Squidex/app/framework/angular/rich-editor.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { Types } from './../utils/types'; - -import { ResourceLoaderService } from './../services/resource-loader.service'; - -declare var tinymce: any; - -export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true -}; - -@Component({ - selector: 'sqx-rich-editor', - styleUrls: ['./rich-editor.component.scss'], - templateUrl: './rich-editor.component.html', - providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR] -}) -export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - private tinyEditor: any; - private tinyInitTimer: any; - private value: string; - private isDisabled = false; - - @ViewChild('editor') - public editor: ElementRef; - - constructor( - private readonly resourceLoader: ResourceLoaderService - ) { - } - - 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(() => { - tinymce.init({ - 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(); - }); - - this.tinyInitTimer = - setTimeout(() => { - self.tinyEditor.setContent(this.value || ''); - }, 500); - }, - removed_menuitems: 'newdocument', plugins: 'code', target: this.editor.nativeElement - }); - }); - } - - public writeValue(value: string) { - this.value = Types.isString(value) ? value : ''; - - if (this.tinyEditor) { - this.tinyEditor.setContent(this.value); - } - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - if (this.tinyEditor) { - this.tinyEditor.setMode(isDisabled ? 'readonly' : 'design'); - } - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } -} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index f2883f9b8..51fcb48ac 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -37,7 +37,6 @@ export * from './angular/panel-container.directive'; export * from './angular/parent-link.directive'; export * from './angular/popup-link.directive'; export * from './angular/progress-bar.component'; -export * from './angular/rich-editor.component'; export * from './angular/root-view.directive'; export * from './angular/router-utils'; export * from './angular/scroll-active.directive'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index ad8b62490..f61b8acfd 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -55,7 +55,6 @@ import { PopupLinkDirective, ProgressBarComponent, ResourceLoaderService, - RichEditorComponent, RootViewDirective, RootViewService, ScrollActiveDirective, @@ -117,7 +116,6 @@ import { ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, - RichEditorComponent, RootViewDirective, ScrollActiveDirective, ShortcutComponent, @@ -167,7 +165,6 @@ import { ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, - RichEditorComponent, RootViewDirective, ScrollActiveDirective, ShortcutComponent, diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 88fc97799..34a32ad1a 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -1,4 +1,7 @@ -
+
diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index 00735e278..6b7b82430 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -13,12 +13,14 @@ import { AppContext } from './app-context'; import { AssetDto, AssetsService, + AssetDragged, DateTime, fadeAnimation, ModalView, UpdateAssetDto, Version, - Versioned + Versioned, + MessageBus } from './../declarations-base'; @Component({ @@ -56,6 +58,9 @@ export class AssetComponent implements OnInit { @Output() public deleting = new EventEmitter(); + @Output() + public clicked = new EventEmitter(); + @Output() public failed = new EventEmitter(); @@ -73,7 +78,8 @@ export class AssetComponent implements OnInit { constructor(public readonly ctx: AppContext, private readonly formBuilder: FormBuilder, - private readonly assetsService: AssetsService + private readonly assetsService: AssetsService, + private readonly messageBus: MessageBus ) { } @@ -177,4 +183,12 @@ export class AssetComponent implements OnInit { this.resetRenameForm(); } + + public onAssetDragStart(event: any) { + this.messageBus.emit(new AssetDragged(event.dragData, AssetDragged.DRAG_START, this)); + } + + public onAssetDragEnd(event: any) { + this.messageBus.emit(new AssetDragged(event.dragData, AssetDragged.DRAG_END, this)); + } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/rich-editor.component.html b/src/Squidex/app/shared/components/rich-editor.component.html new file mode 100644 index 000000000..eb0a1fafb --- /dev/null +++ b/src/Squidex/app/shared/components/rich-editor.component.html @@ -0,0 +1,6 @@ +
+
+

Drop asset in this zone to insert into content

+
+
+
\ 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 new file mode 100644 index 000000000..24e175db7 --- /dev/null +++ b/src/Squidex/app/shared/components/rich-editor.component.scss @@ -0,0 +1,34 @@ +@import '_mixins'; +@import '_vars'; + +.editor { + background: $color-dark-foreground; + 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; + } + + h3 { + text-align: center; + padding-top: 35%; + } + + .drop-zone.active { + opacity: 1; + display:block; + } +} \ 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 new file mode 100644 index 000000000..3104b0c84 --- /dev/null +++ b/src/Squidex/app/shared/components/rich-editor.component.ts @@ -0,0 +1,150 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { AfterViewInit, Component, forwardRef, ElementRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder } from '@angular/forms'; + +import { MessageBus, AssetDragged, AssetsService, Types, ResourceLoaderService } from './../declarations-base'; + +declare var tinymce: any; + +export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true +}; + +@Component({ + selector: 'sqx-rich-editor', + styleUrls: ['./rich-editor.component.scss'], + templateUrl: './rich-editor.component.html', + providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR] +}) +export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; + private tinyEditor: any; + private tinyInitTimer: any; + private value: string; + private isDisabled = false; + + public draggedOver = false; + private assetDraggedSubscription: any; + + @ViewChild('editor') + public editor: ElementRef; + + @Output() + public assetPluginClicked = new EventEmitter(); + + public assetsForm = this.formBuilder.group({ + name: [''] + }); + + constructor(private readonly resourceLoader: ResourceLoaderService, + private readonly formBuilder: FormBuilder, + private readonly messageBus: MessageBus, + private readonly assetsService: AssetsService + ) { + 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; + } + } + }); + } + + 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_urls: false, + 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(); + }); + + 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()); + }); + } + + public writeValue(value: string) { + this.value = Types.isString(value) ? value : ''; + + if (this.tinyEditor) { + this.tinyEditor.setContent(this.value); + } + } + + public setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + + if (this.tinyEditor) { + this.tinyEditor.setMode(isDisabled ? 'readonly' : 'design'); + } + } + + public registerOnChange(fn: any) { + this.callChange = fn; + } + + public registerOnTouched(fn: any) { + this.callTouched = fn; + } + + public onItemDropped(event: any) { + let content = this.assetsService.buildDroppedAssetData(event.dragData, event.mouseEvent); + if (content) { + this.tinyEditor.execCommand('mceInsertContent', false, content); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/declarations.ts b/src/Squidex/app/shared/declarations.ts index 51ff12b5d..0d7371c00 100644 --- a/src/Squidex/app/shared/declarations.ts +++ b/src/Squidex/app/shared/declarations.ts @@ -12,5 +12,6 @@ export * from './components/help.component'; export * from './components/history.component'; export * from './components/language-selector.component'; export * from './components/pipes'; +export * from './components/rich-editor.component'; export * from './declarations-base'; \ No newline at end of file diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index eeec43d6b..3cc68672f 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -58,7 +58,8 @@ import { UserPictureRefPipe, UserManagementService, UsersProviderService, - UsersService + UsersService, + RichEditorComponent } from './declarations'; @NgModule({ @@ -82,7 +83,8 @@ import { UserNamePipe, UserNameRefPipe, UserPicturePipe, - UserPictureRefPipe + UserPictureRefPipe, + RichEditorComponent ], exports: [ AppFormComponent, @@ -100,7 +102,8 @@ import { UserNamePipe, UserNameRefPipe, UserPicturePipe, - UserPictureRefPipe + UserPictureRefPipe, + RichEditorComponent ] }) export class SqxSharedModule { diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 7cc74528d..5fcda2bbb 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -19,6 +19,8 @@ import { Versioned } from 'framework'; +import { AssetUrlPipe } from 'shared'; + export class AssetsDto { constructor( public readonly total: number, @@ -100,12 +102,16 @@ 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 { @@ -302,6 +308,19 @@ 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) { diff --git a/src/Squidex/app/shared/utils/messages.ts b/src/Squidex/app/shared/utils/messages.ts index 2f8c4aa83..543bd3caf 100644 --- a/src/Squidex/app/shared/utils/messages.ts +++ b/src/Squidex/app/shared/utils/messages.ts @@ -15,4 +15,17 @@ export class AssetUpdated { public readonly sender: any ) { } +} + +export class AssetDragged { + + public static readonly DRAG_START = 'Start'; + public static readonly DRAG_END = 'End'; + + constructor( + public readonly assetDto: AssetDto, + public readonly dragEvent: string, + public readonly sender: any + ) { + } } \ No newline at end of file diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index eec0956f2..0fc439776 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -81,7 +81,7 @@ /* * Allow to expose the url in graph ql url. */ - "exposeSourceUrl": false + "exposeSourceUrl": false }, "eventStore": {