Browse Source

Text editor integrated.

pull/614/head
Sebastian 5 years ago
parent
commit
0b8ac9ac1c
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_it.json
  3. 1
      backend/i18n/frontend_nl.json
  4. 1
      backend/i18n/source/frontend_en.json
  5. 5
      frontend/app-config/webpack.config.js
  6. 79
      frontend/app/framework/angular/forms/editors/code-editor.component.ts
  7. 38
      frontend/app/shared/components/assets/asset-dialog.component.html
  8. 2
      frontend/app/shared/components/assets/asset-dialog.component.scss
  9. 22
      frontend/app/shared/components/assets/asset-dialog.component.ts
  10. 1
      frontend/app/shared/components/assets/asset-text-editor.component.html
  11. 5
      frontend/app/shared/components/assets/asset-text-editor.component.scss
  12. 54
      frontend/app/shared/components/assets/asset-text-editor.component.ts
  13. 1
      frontend/app/shared/declarations.ts
  14. 3
      frontend/app/shared/module.ts

1
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.",

1
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.",

1
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.",

1
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.",

5
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' },

79
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 = 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.value !== newValue) {
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.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) {

38
frontend/app/shared/components/assets/asset-dialog.component.html

@ -18,12 +18,17 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link" (click)="selectTab(3)" [class.active]="selectedTab === 3" *ngIf="isVideo || (asset | sqxPreviewable)">
<a class="nav-link" (click)="selectTab(3)" [class.active]="selectedTab === 3" *ngIf="isDocumentLikely">
{{ 'assets.tabTextEditor' | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" (click)="selectTab(4)" [class.active]="selectedTab === 4" *ngIf="isVideo || (asset | sqxPreviewable)">
{{ 'assets.tabPreview' | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" (click)="selectTab(4)" [class.active]="selectedTab === 4">
<a class="nav-link" (click)="selectTab(5)" [class.active]="selectedTab === 5">
{{ 'assets.tabHistory' | sqxTranslate }}
</a>
</li>
@ -45,6 +50,11 @@
{{ 'common.save' | sqxTranslate }}
</button>
</ng-container>
<ng-container *ngSwitchCase="3">
<button type="button" class="btn btn-primary ml-auto mr-4" (click)="saveText()" [class.invisible]="!isEditable">
{{ 'common.save' | sqxTranslate }}
</button>
</ng-container>
</ng-container>
</ng-container>
@ -162,10 +172,10 @@
</div>
</ng-container>
<ng-container *ngSwitchCase="1">
<div class="image">
<div class="editor">
<sqx-image-editor [imageSource]="asset | sqxAssetPreviewUrl"></sqx-image-editor>
<div class="image-progress" *ngIf="progress > 0">
<div class="editor-progress" *ngIf="progress > 0">
<sqx-progress-bar
[value]="progress"
[strokeWidth]="2"
@ -176,10 +186,10 @@
</div>
</ng-container>
<ng-container *ngSwitchCase="2">
<div>
<div class="editor">
<sqx-image-focus-point [imageSource]="asset | sqxAssetPreviewUrl" [focusPoint]="asset.metadata"></sqx-image-focus-point>
<div class="image-progress" *ngIf="progress > 0">
<div class="editor-progress" *ngIf="progress > 0">
<sqx-progress-bar
[value]="progress"
[strokeWidth]="2"
@ -190,6 +200,20 @@
</div>
</ng-container>
<ng-container *ngSwitchCase="3">
<div class="editor">
<sqx-asset-text-editor [fileSource]="asset | sqxAssetPreviewUrl" [fileName]="asset.fileName" [mimeType]="asset.mimeType"></sqx-asset-text-editor>
<div class="editor-progress" *ngIf="progress > 0">
<sqx-progress-bar
[value]="progress"
[strokeWidth]="2"
[trailColor]="'transparent'"
[trailWidth]="0">
</sqx-progress-bar>
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="4">
<ng-container *ngIf="asset | sqxPreviewable; else video">
<ngx-doc-viewer [url]="asset | sqxAssetPreviewUrl" [style]="{}" viewer="google" style="width:100%;height:50vh;"></ngx-doc-viewer>
</ng-container>
@ -198,7 +222,7 @@
<sqx-video-player [source]="asset | sqxAssetPreviewUrl" [mimeType]="asset.mimeType"></sqx-video-player>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="4">
<ng-container *ngSwitchCase="5">
<sqx-asset-history [asset]="asset"></sqx-asset-history>
</ng-container>
</ng-container>

2
frontend/app/shared/components/assets/asset-dialog.component.scss

@ -2,7 +2,7 @@
position: relative;
}
.image {
.editor {
& {
@include absolute(0, 0, 0, 0);
}

22
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<ImageFocusPointComponent>;
@ViewChildren(AssetTextEditorComponent)
public textEditor: QueryList<AssetTextEditorComponent>;
public path: Observable<ReadonlyArray<AssetPathItem>>;
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<Blob | null>) {
fileChange.then(file => {
if (file) {
this.setProgress(0);

1
frontend/app/shared/components/assets/asset-text-editor.component.html

@ -0,0 +1 @@
<sqx-code-editor [(ngModel)]="snapshot.text" [filePath]="fileName"></sqx-code-editor>

5
frontend/app/shared/components/assets/asset-text-editor.component.scss

@ -0,0 +1,5 @@
:host ::ng-deep {
.editor {
@include absolute(0, 0, 0, 0);
}
}

54
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<Snapshot> 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<Blob | null> {
return new Promise<Blob | null>(resolve => {
const blob = new Blob([this.snapshot.text || ''], {
type: this.mimeType
});
resolve(blob);
});
}
}

1
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';

3
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,

Loading…
Cancel
Save