Browse Source

Update Rich text and improve caching.

pull/493/head
Sebastian 7 years ago
parent
commit
de84bf0737
  1. 2
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  2. 26
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  3. 3
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs
  4. 2
      frontend/app/declarations.d.ts
  5. 36
      frontend/app/features/content/pages/content/content-page.component.html
  6. 2
      frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html
  7. 2
      frontend/app/shared/components/forms/rich-editor.component.scss
  8. 71
      frontend/app/shared/components/forms/rich-editor.component.ts
  9. 21
      frontend/package-lock.json
  10. 1
      frontend/package.json

2
backend/src/Squidex.Web/Pipeline/CachingFilter.cs

@ -68,7 +68,7 @@ namespace Squidex.Web.Pipeline
}
}
cachingManager.Finish(httpContext, cachingOptions.MaxSurrogateKeysSize);
cachingManager.Finish(httpContext);
}
}
}

26
backend/src/Squidex.Web/Pipeline/CachingManager.cs

@ -13,6 +13,7 @@ using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
@ -23,13 +24,12 @@ namespace Squidex.Web.Pipeline
{
public sealed class CachingManager : IRequestCache
{
private readonly ObjectPool<StringBuilder> stringBuilderPool;
private readonly CachingOptions cachingOptions;
private readonly IHttpContextAccessor httpContextAccessor;
internal sealed class CacheContext : IRequestCache, IDisposable
{
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());
private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
private readonly HashSet<string> keys = new HashSet<string>();
private readonly HashSet<string> headers = new HashSet<string>();
@ -87,7 +87,7 @@ namespace Squidex.Web.Pipeline
}
}
public void Finish(HttpResponse response, int maxSurrogateKeySize)
public void Finish(HttpResponse response, int maxSurrogateKeySize, ObjectPool<StringBuilder> stringBuilderPool)
{
if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag))
{
@ -104,7 +104,7 @@ namespace Squidex.Web.Pipeline
{
const int GuidLength = 36;
var stringBuilder = StringBuilderPool.Get();
var stringBuilder = stringBuilderPool.Get();
try
{
foreach (var key in keys)
@ -136,7 +136,7 @@ namespace Squidex.Web.Pipeline
}
finally
{
StringBuilderPool.Return(stringBuilder);
stringBuilderPool.Return(stringBuilder);
}
}
@ -164,11 +164,19 @@ namespace Squidex.Web.Pipeline
}
}
public CachingManager(IHttpContextAccessor httpContextAccessor)
public CachingManager(IHttpContextAccessor httpContextAccessor, IOptions<CachingOptions> cachingOptions)
{
Guard.NotNull(httpContextAccessor);
Guard.NotNull(cachingOptions);
this.httpContextAccessor = httpContextAccessor;
this.cachingOptions = cachingOptions.Value;
stringBuilderPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy
{
MaximumRetainedCapacity = cachingOptions.Value.MaxSurrogateKeysSize
});
}
public void Start(HttpContext httpContext)
@ -217,7 +225,7 @@ namespace Squidex.Web.Pipeline
}
}
public void Finish(HttpContext httpContext, int maxSurrogateKeySize)
public void Finish(HttpContext httpContext)
{
Guard.NotNull(httpContext);
@ -225,7 +233,7 @@ namespace Squidex.Web.Pipeline
if (cacheContext != null)
{
cacheContext.Finish(httpContext.Response, maxSurrogateKeySize);
cacheContext.Finish(httpContext.Response, cachingOptions.MaxSurrogateKeysSize, stringBuilderPool);
}
}
}

3
backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.AspNetCore.Http;
@ -38,7 +39,7 @@ namespace Squidex.Web.Pipeline
A.CallTo(() => httpContextAccessor.HttpContext)
.Returns(httpContext);
cachingManager = new CachingManager(httpContextAccessor);
cachingManager = new CachingManager(httpContextAccessor, Options.Create(new CachingOptions()));
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var actionFilters = new List<IFilterMetadata>();

2
frontend/app/declarations.d.ts

@ -17,4 +17,4 @@ declare module 'sortablejs' {
}
export function create(element: any, options: any): Ref;
}
}

36
frontend/app/features/content/pages/content/content-page.component.html

@ -21,26 +21,28 @@
<ng-container menu>
<ng-container *ngIf="content; else noContent">
<div class="dropdown dropdown-options ml-1" *ngIf="!schema.isSingleton">
<ng-container *ngIf="!schema.isSingleton">
<sqx-preview-button [schema]="schema" [content]="content"></sqx-preview-button>
<ng-container *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-more"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
<div class="dropdown dropdown-options ml-1">
<ng-container *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-more"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</ng-container>
</ng-container>
</div>
</div>
</ng-container>
<ng-container *ngIf="content?.canUpdate">
<button type="submit" class="btn btn-primary ml-1" title="CTRL + S">

2
frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html

@ -1,5 +1,5 @@
<div [formGroup]="editForm">
<div class="form-group row" [class.hidden]="!showUnique">
<div class="form-group row" *ngIf="showUnique">
<div class="col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldUnique" formControlName="isUnique" />

2
frontend/app/shared/components/forms/rich-editor.component.scss

@ -3,5 +3,5 @@
}
.editor {
height: 300px;
height: 400px;
}

71
frontend/app/shared/components/forms/rich-editor.component.ts

@ -72,10 +72,16 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
}
public ngAfterViewInit() {
const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/5.2.0/tinymce.min.js').then(() => {
const timer = setInterval(() => {
const target = this.editor.nativeElement;
if (document.body.contains(target)) {
tinymce.init(this.getEditorOptions(target));
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.4/tinymce.min.js').then(() => {
tinymce.init(self.getEditorOptions());
clearInterval(timer);
}
}, 10);
});
}
@ -95,21 +101,13 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
this.assetsDialog.show();
}
private getEditorOptions() {
private getEditorOptions(target: any): any {
const self = this;
return {
convert_fonts_to_spans: true,
convert_urls: false,
paste_data_images: true,
plugins: 'code image media link lists advlist paste',
min_height: 300,
max_height: 800,
removed_menuitems: 'newdocument',
resize: true,
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets',
images_upload_handler: (blob: any, success: (url: string) => void, failed: () => void) => {
...DEFAULT_PROPS,
images_upload_handler: (blob: any, success: (url: string) => void, failure: (message: string) => void) => {
const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() });
self.assetUploader.uploadFile(file)
@ -119,26 +117,27 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
}
}, error => {
if (!Types.is(error, UploadCanceled)) {
failed();
failure('Failed');
}
});
},
setup: (editor: any) => {
self.tinyEditor = editor;
self.tinyEditor.addButton('assets', {
onclick: self.showSelector,
icon: 'assets',
editor.ui.registry.addButton('assets', {
onAction: self.showSelector,
icon: 'gallery',
text: '',
tooltip: 'Insert Assets'
});
self.tinyEditor.on('init', () => {
editor.on('init', () => {
self.tinyEditor = editor;
self.setContent();
self.setReadOnly();
});
self.tinyEditor.on('change', () => {
editor.on('change', () => {
const value = editor.getContent();
if (this.value !== value) {
@ -148,7 +147,7 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
}
});
self.tinyEditor.on('paste', (event: ClipboardEvent) => {
editor.on('paste', (event: ClipboardEvent) => {
if (event.clipboardData) {
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
@ -160,7 +159,7 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
}
});
self.tinyEditor.on('drop', (event: DragEvent) => {
editor.on('drop', (event: DragEvent) => {
if (event.dataTransfer) {
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i);
@ -174,21 +173,19 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
return false;
});
self.tinyEditor.on('blur', () => {
editor.on('blur', () => {
self.callTouched();
});
self.setReadOnly();
},
target: self.editor.nativeElement
target
};
}
public writeValue(obj: any) {
this.value = Types.isString(obj) ? obj : '';
if (this.tinyEditor) {
if (this.tinyEditor && this.tinyEditor.initialized) {
this.setContent();
}
}
@ -196,7 +193,7 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
if (this.tinyEditor) {
if (this.tinyEditor && this.tinyEditor.initialized) {
this.setReadOnly();
}
}
@ -265,4 +262,16 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
}
});
}
}
}
const DEFAULT_PROPS = {
convert_fonts_to_spans: true,
convert_urls: false,
paste_data_images: true,
plugins: 'code image media link lists advlist paste',
min_height: 400,
max_height: 800,
removed_menuitems: 'newdocument',
resize: true,
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets'
};

21
frontend/package-lock.json

@ -490,6 +490,14 @@
"integrity": "sha512-LlhwGivHkUV8ehNmaXjGGXopLm91G9ORIRcjw7Ya47jVAIGudewFZM2PdPXBvueZfRWwYzLt083wiPfKRXrSlg==",
"dev": true
},
"@types/jquery": {
"version": "3.3.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.33.tgz",
"integrity": "sha512-U6IdXYGkfUI42SR79vB2Spj+h1Ly3J3UZjpd8mi943lh126TK7CB+HZOxGh2nM3IySor7wqVQdemD/xtydsBKA==",
"requires": {
"@types/sizzle": "*"
}
},
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -557,6 +565,19 @@
"@types/react": "*"
}
},
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg=="
},
"@types/tinymce": {
"version": "4.5.24",
"resolved": "https://registry.npmjs.org/@types/tinymce/-/tinymce-4.5.24.tgz",
"integrity": "sha512-g2aFp+/GHTD6P2ZI2tBwZGIDi+64oocoDleCHtAWLYlFOR9fjPO+pdvmGjpJG3C7XeMZvugFx2NhRBL+Jb/wbQ==",
"requires": {
"@types/jquery": "*"
}
},
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",

1
frontend/package.json

@ -62,6 +62,7 @@
"@types/node": "13.7.4",
"@types/react": "16.9.21",
"@types/react-dom": "16.9.5",
"@types/tinymce": "^4.5.24",
"browserslist": "4.8.7",
"caniuse-lite": "1.0.30001028",
"circular-dependency-plugin": "5.2.0",

Loading…
Cancel
Save