diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json
index 4506527ac..8ed9dafca 100644
--- a/backend/i18n/frontend_en.json
+++ b/backend/i18n/frontend_en.json
@@ -103,6 +103,7 @@
"assets.tabImage": "Image",
"assets.tabMetadata": "Metadata",
"assets.tabPreview": "Preview",
+ "assets.tabTextEditor": "Text Editor",
"assets.updated": "Asset has been updated.",
"assets.updateFailed": "Failed to update asset. Please reload.",
"assets.updateFolderFailed": "Failed to update asset folder. Please reload.",
diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json
index 20dff0ee9..a3c316938 100644
--- a/backend/i18n/frontend_it.json
+++ b/backend/i18n/frontend_it.json
@@ -103,6 +103,7 @@
"assets.tabImage": "Immagine",
"assets.tabMetadata": "Metadati",
"assets.tabPreview": "Preview",
+ "assets.tabTextEditor": "Text Editor",
"assets.updated": "La risorsa è stata aggiornata.",
"assets.updateFailed": "Non è stato possibile aggiornare la risorsa. Per favore ricarica.",
"assets.updateFolderFailed": "Non è stato possibile aggiornare la cartella delle risorse. Per favore ricarica.",
diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json
index 99ba50991..4c5f50f50 100644
--- a/backend/i18n/frontend_nl.json
+++ b/backend/i18n/frontend_nl.json
@@ -103,6 +103,7 @@
"assets.tabImage": "Afbeelding",
"assets.tabMetadata": "Metadata",
"assets.tabPreview": "Preview",
+ "assets.tabTextEditor": "Text Editor",
"assets.updated": "Asset is bijgewerkt.",
"assets.updateFailed": "Bijwerken van item is mislukt. Laad opnieuw.",
"assets.updateFolderFailed": "Bijwerken van de map is mislukt. Laad opnieuw.",
diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json
index 4506527ac..8ed9dafca 100644
--- a/backend/i18n/source/frontend_en.json
+++ b/backend/i18n/source/frontend_en.json
@@ -103,6 +103,7 @@
"assets.tabImage": "Image",
"assets.tabMetadata": "Metadata",
"assets.tabPreview": "Preview",
+ "assets.tabTextEditor": "Text Editor",
"assets.updated": "Asset has been updated.",
"assets.updateFailed": "Failed to update asset. Please reload.",
"assets.updateFolderFailed": "Failed to update asset folder. Please reload.",
diff --git a/frontend/app-config/webpack.config.js b/frontend/app-config/webpack.config.js
index ad3204b85..a2dd58b91 100644
--- a/frontend/app-config/webpack.config.js
+++ b/frontend/app-config/webpack.config.js
@@ -225,8 +225,9 @@ module.exports = function (env) {
{ from: './node_modules/tinymce/tinymce.min.js', to: 'dependencies/tinymce' },
{ from: './node_modules/ace-builds/src-min/ace.js', to: 'dependencies/ace/ace.js' },
- { from: './node_modules/ace-builds/src-min/mode-*.js', to: 'dependencies/ace' },
- { from: './node_modules/ace-builds/src-min/worker-*.js', to: 'dependencies/ace' },
+ { from: './node_modules/ace-builds/src-min/mode-*.js', to: 'dependencies/ace/[name].[ext]' },
+ { from: './node_modules/ace-builds/src-min/worker-*.js', to: 'dependencies/ace/[name].[ext]' },
+ { from: './node_modules/ace-builds/src-min/ext-modelist.js', to: 'dependencies/ace/ext/modelist.js' },
{ from: './node_modules/video.js/dist/video.min.js', to: 'dependencies/videojs' },
{ from: './node_modules/video.js/dist/video-js.min.css', to: 'dependencies/videojs' },
diff --git a/frontend/app/framework/angular/forms/editors/code-editor.component.ts b/frontend/app/framework/angular/forms/editors/code-editor.component.ts
index de1a5bab4..edc2b8fe2 100644
--- a/frontend/app/framework/angular/forms/editors/code-editor.component.ts
+++ b/frontend/app/framework/angular/forms/editors/code-editor.component.ts
@@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core';
+import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ResourceLoaderService, StatefulControlComponent, Types } from '@app/framework/internal';
import { Subject } from 'rxjs';
@@ -27,10 +27,12 @@ export const SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class CodeEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, FocusComponent {
+export class CodeEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, FocusComponent, OnChanges {
private valueChanged = new Subject();
private aceEditor: any;
- private value: string;
+ private value: any;
+ private valueString: string;
+ private modelist: any;
@ViewChild('editor', { static: false })
public editor: ElementRef;
@@ -41,6 +43,12 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
@Input()
public mode = 'ace/mode/javascript';
+ @Input()
+ public filePath: string;
+
+ @Input()
+ public valueMode: 'String' | 'Json' = 'String';
+
@Input()
public height = 0;
@@ -51,7 +59,20 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
}
public writeValue(obj: string) {
- this.value = Types.isString(obj) ? obj : '';
+ this.value = obj;
+
+ if (this.valueMode === 'Json') {
+ this.value = obj;
+
+ try {
+ this.valueString = JSON.stringify(obj);
+ } catch (e) {
+ this.valueString = '';
+ }
+ } else {
+ this.value = Types.isString(obj) ? obj : '';
+ this.valueString = this.value;
+ }
if (this.aceEditor) {
this.setValue(this.value);
@@ -72,6 +93,12 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
}
}
+ public ngOnChanges(changes: SimpleChanges) {
+ if (changes['filePath'] || changes['mode']) {
+ this.setMode();
+ }
+ }
+
public ngAfterViewInit() {
this.valueChanged.pipe(debounceTime(500))
.subscribe(() => {
@@ -82,14 +109,20 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
this.editor.nativeElement.style.height = `${this.height}px`;
}
- this.resourceLoader.loadLocalScript('dependencies/ace/ace.js').then(() => {
+ Promise.all([
+ this.resourceLoader.loadLocalScript('dependencies/ace/ace.js'),
+ this.resourceLoader.loadLocalScript('dependencies/ace/ext/modelist.js')
+ ]).then(() => {
this.aceEditor = ace.edit(this.editor.nativeElement);
- this.aceEditor.getSession().setMode(this.mode);
+ this.modelist = ace.require('ace/ext/modelist');
+
this.aceEditor.setReadOnly(this.snapshot.isDisabled);
this.aceEditor.setFontSize(14);
- this.setValue(this.value);
+ this.setDisabledState(this.snapshot.isDisabled);
+ this.setValue(this.valueString);
+ this.setMode();
this.aceEditor.on('blur', () => {
this.changeValue();
@@ -105,13 +138,43 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
}
private changeValue() {
- const newValue = this.aceEditor.getValue();
+ let newValue: any = null;
+ let newValueString: string;
+
+ if (this.valueMode === 'Json') {
+ const isValid = this.aceEditor.getSession().getAnnotations().length === 0;
+
+ if (isValid) {
+ try {
+ newValue = JSON.parse(this.aceEditor.getValue());
+ } catch (e) {
+ newValue = null;
+ }
+ }
+
+ newValueString = JSON.stringify(newValue);
+ } else {
+ newValueString = newValue = this.aceEditor.getValue();
+ }
- if (this.value !== newValue) {
+ if (this.valueString !== newValueString) {
this.callChange(newValue);
}
this.value = newValue;
+ this.valueString = newValueString;
+ }
+
+ private setMode() {
+ if (this.aceEditor) {
+ if (this.filePath && this.modelist) {
+ const mode = this.modelist.getModeForPath(this.filePath).mode;
+
+ this.aceEditor.getSession().setMode(mode);
+ } else {
+ this.aceEditor.getSession().setMode(this.mode);
+ }
+ }
}
private setValue(value: string) {
diff --git a/frontend/app/shared/components/assets/asset-dialog.component.html b/frontend/app/shared/components/assets/asset-dialog.component.html
index 5a6c14fb3..17d458cb5 100644
--- a/frontend/app/shared/components/assets/asset-dialog.component.html
+++ b/frontend/app/shared/components/assets/asset-dialog.component.html
@@ -18,12 +18,17 @@
-
+
+ {{ 'assets.tabTextEditor' | sqxTranslate }}
+
+
+
+
{{ 'assets.tabPreview' | sqxTranslate }}
-
+
{{ 'assets.tabHistory' | sqxTranslate }}
@@ -45,6 +50,11 @@
{{ 'common.save' | sqxTranslate }}
+
+
+
@@ -162,10 +172,10 @@
-
+
-
0">
+
0">
-
+
-
0">
+
0">
+
+
+
@@ -198,7 +222,7 @@
-
+
diff --git a/frontend/app/shared/components/assets/asset-dialog.component.scss b/frontend/app/shared/components/assets/asset-dialog.component.scss
index efd3600a9..16f0748a3 100644
--- a/frontend/app/shared/components/assets/asset-dialog.component.scss
+++ b/frontend/app/shared/components/assets/asset-dialog.component.scss
@@ -2,7 +2,7 @@
position: relative;
}
-.image {
+.editor {
& {
@include absolute(0, 0, 0, 0);
}
diff --git a/frontend/app/shared/components/assets/asset-dialog.component.ts b/frontend/app/shared/components/assets/asset-dialog.component.ts
index 8a6bcb004..723c04ebe 100644
--- a/frontend/app/shared/components/assets/asset-dialog.component.ts
+++ b/frontend/app/shared/components/assets/asset-dialog.component.ts
@@ -12,6 +12,7 @@ import { AssetsService } from '@app/shared/services/assets.service';
import { AssetPathItem, ROOT_ITEM } from '@app/shared/state/assets.state';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
+import { AssetTextEditorComponent } from './asset-text-editor.component';
import { ImageCropperComponent } from './image-cropper.component';
import { ImageFocusPointComponent } from './image-focus-point.component';
@@ -39,6 +40,9 @@ export class AssetDialogComponent implements OnChanges {
@ViewChildren(ImageFocusPointComponent)
public imageFocus: QueryList;
+ @ViewChildren(AssetTextEditorComponent)
+ public textEditor: QueryList;
+
public path: Observable>;
public isEditable = false;
@@ -59,6 +63,10 @@ export class AssetDialogComponent implements OnChanges {
return this.asset.type === 'Video';
}
+ public get isDocumentLikely() {
+ return this.asset.type === 'Unknown' && this.asset.fileSize < 100_000;
+ }
+
constructor(
private readonly appsState: AppsState,
private readonly assetsState: AssetsState,
@@ -106,7 +114,19 @@ export class AssetDialogComponent implements OnChanges {
return;
}
- this.imageCropper.first.toFile().then(file => {
+ this.uploadEdited(this.imageCropper.first.toFile());
+ }
+
+ public saveText() {
+ if (!this.isUploadable) {
+ return;
+ }
+
+ this.uploadEdited(this.textEditor.first.toFile());
+ }
+
+ public uploadEdited(fileChange: Promise) {
+ fileChange.then(file => {
if (file) {
this.setProgress(0);
diff --git a/frontend/app/shared/components/assets/asset-text-editor.component.html b/frontend/app/shared/components/assets/asset-text-editor.component.html
new file mode 100644
index 000000000..624a51022
--- /dev/null
+++ b/frontend/app/shared/components/assets/asset-text-editor.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/app/shared/components/assets/asset-text-editor.component.scss b/frontend/app/shared/components/assets/asset-text-editor.component.scss
new file mode 100644
index 000000000..2019841b3
--- /dev/null
+++ b/frontend/app/shared/components/assets/asset-text-editor.component.scss
@@ -0,0 +1,5 @@
+:host ::ng-deep {
+ .editor {
+ @include absolute(0, 0, 0, 0);
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/shared/components/assets/asset-text-editor.component.ts b/frontend/app/shared/components/assets/asset-text-editor.component.ts
new file mode 100644
index 000000000..17335ff23
--- /dev/null
+++ b/frontend/app/shared/components/assets/asset-text-editor.component.ts
@@ -0,0 +1,54 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { HttpClient } from '@angular/common/http';
+import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
+import { StatefulComponent } from '@app/framework';
+
+interface Snapshot {
+ // The text to edit.
+ text?: string;
+}
+
+@Component({
+ selector: 'sqx-asset-text-editor',
+ styleUrls: ['./asset-text-editor.component.scss'],
+ templateUrl: './asset-text-editor.component.html'
+})
+export class AssetTextEditorComponent extends StatefulComponent implements OnInit {
+ @Input()
+ public fileSource: string;
+
+ @Input()
+ public fileName: string;
+
+ @Input()
+ public mimeType: string;
+
+ constructor(changeDetector: ChangeDetectorRef,
+ private readonly httpClient: HttpClient
+ ) {
+ super(changeDetector, {});
+ }
+
+ public ngOnInit() {
+ this.httpClient.get(this.fileSource, { responseType: 'text' })
+ .subscribe(text => {
+ this.next({ text });
+ });
+ }
+
+ public toFile(): Promise {
+ return new Promise(resolve => {
+ const blob = new Blob([this.snapshot.text || ''], {
+ type: this.mimeType
+ });
+
+ resolve(blob);
+ });
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/shared/declarations.ts b/frontend/app/shared/declarations.ts
index 9d1e0bf76..b2fbf79c3 100644
--- a/frontend/app/shared/declarations.ts
+++ b/frontend/app/shared/declarations.ts
@@ -11,6 +11,7 @@ export * from './components/assets/asset-folder-dialog.component';
export * from './components/assets/asset-folder.component';
export * from './components/assets/asset-history.component';
export * from './components/assets/asset-path.component';
+export * from './components/assets/asset-text-editor.component';
export * from './components/assets/asset-uploader.component';
export * from './components/assets/asset.component';
export * from './components/assets/assets-list.component';
diff --git a/frontend/app/shared/module.ts b/frontend/app/shared/module.ts
index a1ba61c34..2917c3670 100644
--- a/frontend/app/shared/module.ts
+++ b/frontend/app/shared/module.ts
@@ -15,7 +15,7 @@ import { SqxFrameworkModule } from '@app/framework';
import { MentionModule } from 'angular-mentions';
import { NgxDocViewerModule } from 'ngx-doc-viewer';
import { PreviewableType } from './components/assets/pipes';
-import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetsDialogState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentMustExistGuard, ContentsService, ContentsState, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PatternsService, PatternsState, PlansService, PlansState, QueryComponent, QueryListComponent, QueryPathComponent, ReferencesCheckboxesComponent, ReferencesDropdownComponent, ReferencesTagsComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UnsetContentGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WorkflowsService, WorkflowsState } from './declarations';
+import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetsDialogState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentMustExistGuard, ContentsService, ContentsState, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PatternsService, PatternsState, PlansService, PlansState, QueryComponent, QueryListComponent, QueryPathComponent, ReferencesCheckboxesComponent, ReferencesDropdownComponent, ReferencesTagsComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UnsetContentGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WorkflowsService, WorkflowsState } from './declarations';
import { SearchService } from './services/search.service';
@NgModule({
@@ -37,6 +37,7 @@ import { SearchService } from './services/search.service';
AssetPreviewUrlPipe,
AssetsListComponent,
AssetsSelectorComponent,
+ AssetTextEditorComponent,
AssetUploaderComponent,
AssetUrlPipe,
CommentComponent,