mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
102 changed files with 3157 additions and 925 deletions
@ -0,0 +1,61 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Json.Objects; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
public static class RichTextExtensions |
|||
{ |
|||
public static bool TryGetEnum<T>(this JsonValue value, out T enumValue) where T : struct |
|||
{ |
|||
enumValue = default; |
|||
|
|||
return value.Value is string text && Enum.TryParse(text, true, out enumValue); |
|||
} |
|||
|
|||
public static int GetIntAttr(this JsonObject? attrs, string name, int defaultValue = 0) |
|||
{ |
|||
if (attrs?.TryGetValue(name, out var value) == true && value.Value is double attr) |
|||
{ |
|||
return (int)attr; |
|||
} |
|||
|
|||
return defaultValue; |
|||
} |
|||
|
|||
public static string GetStringAttr(this JsonObject? attrs, string name, string defaultValue = "") |
|||
{ |
|||
if (attrs?.TryGetValue(name, out var value) == true && value.Value is string attr) |
|||
{ |
|||
return attr; |
|||
} |
|||
|
|||
return defaultValue; |
|||
} |
|||
|
|||
public static bool TryGetArrayOfObject(this JsonValue value, out JsonArray array) |
|||
{ |
|||
array = default!; |
|||
|
|||
if (value.Value is not JsonArray temp) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
foreach (var item in temp) |
|||
{ |
|||
if (item.Value is not JsonObject) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
array = temp; |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Json.Objects; |
|||
using Squidex.Text.RichText.Model; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
internal class RichTextMark : IMark |
|||
{ |
|||
private JsonObject? attrs; |
|||
|
|||
public MarkType Type { get; private set; } |
|||
|
|||
public bool TryUse(JsonValue source) |
|||
{ |
|||
Type = MarkType.Undefined; |
|||
|
|||
attrs = null; |
|||
|
|||
if (source.Value is not JsonObject obj) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var isValid = true; |
|||
foreach (var (key, value) in obj) |
|||
{ |
|||
switch (key) |
|||
{ |
|||
case "type" when value.TryGetEnum<MarkType>(out var type): |
|||
Type = type; |
|||
break; |
|||
case "attrs" when value.Value is JsonObject attrs: |
|||
this.attrs = attrs; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
isValid &= Type != MarkType.Undefined; |
|||
|
|||
return isValid; |
|||
} |
|||
|
|||
public int GetIntAttr(string name, int defaultValue = 0) |
|||
{ |
|||
return attrs.GetIntAttr(name, defaultValue); |
|||
} |
|||
|
|||
public string GetStringAttr(string name, string defaultValue = "") |
|||
{ |
|||
return attrs.GetStringAttr(name, defaultValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,220 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Json.Objects; |
|||
using Squidex.Infrastructure.ObjectPool; |
|||
using Squidex.Text.RichText; |
|||
using Squidex.Text.RichText.Model; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
public sealed class RichTextNode : INode |
|||
{ |
|||
private readonly RichTextMark mark = new RichTextMark(); |
|||
private State currentState; |
|||
|
|||
internal struct State |
|||
{ |
|||
public JsonObject? Root; |
|||
public NodeType Type; |
|||
public JsonArray? Marks; |
|||
public JsonObject? Attrs; |
|||
public JsonArray? Content; |
|||
public string? Text; |
|||
public int MarkIndex; |
|||
} |
|||
|
|||
public NodeType Type |
|||
{ |
|||
get => currentState.Type; |
|||
} |
|||
|
|||
public JsonObject? Root |
|||
{ |
|||
get => currentState.Root; |
|||
} |
|||
|
|||
public string? Text |
|||
{ |
|||
get => currentState.Text; |
|||
} |
|||
|
|||
public static bool TryCreate(JsonValue source, out RichTextNode node) |
|||
{ |
|||
var candidate = new RichTextNode(); |
|||
|
|||
if (candidate.TryUse(source, true)) |
|||
{ |
|||
node = candidate; |
|||
return true; |
|||
} |
|||
|
|||
node = null!; |
|||
return false; |
|||
} |
|||
|
|||
public static RichTextNode Create(JsonValue source) |
|||
{ |
|||
var node = new RichTextNode(); |
|||
|
|||
// We assume that we have made the validation before.
|
|||
node.TryUse(source, false); |
|||
|
|||
return node; |
|||
} |
|||
|
|||
public bool TryUse(JsonValue source, bool recursive = false) |
|||
{ |
|||
State state = default; |
|||
|
|||
if (source.Value is not JsonObject obj) |
|||
{ |
|||
currentState = state; |
|||
return false; |
|||
} |
|||
|
|||
state.Root = obj; |
|||
|
|||
var isValid = true; |
|||
foreach (var (key, value) in obj) |
|||
{ |
|||
switch (key) |
|||
{ |
|||
case "type" when value.TryGetEnum<NodeType>(out var type): |
|||
state.Type = type; |
|||
break; |
|||
case "attrs" when value.Value is JsonObject attrs: |
|||
state.Attrs = attrs; |
|||
break; |
|||
case "marks" when value.TryGetArrayOfObject(out var marks): |
|||
state.Marks = marks; |
|||
break; |
|||
case "content" when value.TryGetArrayOfObject(out var content): |
|||
state.Content = content; |
|||
break; |
|||
case "text" when value.Value is string text: |
|||
state.Text = text; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
currentState = state; |
|||
|
|||
isValid &= Type != NodeType.Undefined; |
|||
|
|||
if (isValid && recursive) |
|||
{ |
|||
if (state.Content != null) |
|||
{ |
|||
foreach (var content in state.Content) |
|||
{ |
|||
// We have already validated this before.
|
|||
isValid &= TryUse((JsonObject)content.Value!, recursive); |
|||
} |
|||
} |
|||
|
|||
if (state.Marks != null) |
|||
{ |
|||
foreach (var markObj in state.Marks) |
|||
{ |
|||
// We have already validated this before.
|
|||
isValid &= mark.TryUse((JsonObject)markObj.Value!); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return isValid; |
|||
} |
|||
|
|||
public int GetIntAttr(string name, int defaultValue = 0) |
|||
{ |
|||
return currentState.Attrs.GetIntAttr(name, defaultValue); |
|||
} |
|||
|
|||
public string GetStringAttr(string name, string defaultValue = "") |
|||
{ |
|||
return currentState.Attrs.GetStringAttr(name, defaultValue); |
|||
} |
|||
|
|||
public IMark? GetNextMark() |
|||
{ |
|||
if (currentState.Marks == null || currentState.MarkIndex >= currentState.Marks.Count) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
// We have already validated this before.
|
|||
mark.TryUse((JsonObject)currentState.Marks[currentState.MarkIndex++].Value!); |
|||
return mark; |
|||
} |
|||
|
|||
public void IterateContent<T>(T state, Action<INode, T, bool, bool> action) |
|||
{ |
|||
var prevState = currentState; |
|||
|
|||
if (prevState.Content == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var i = 0; |
|||
foreach (var item in prevState.Content) |
|||
{ |
|||
var isFirst = i == 0; |
|||
var isLast = i == prevState.Content.Count - 1; |
|||
|
|||
// We have already validated this before.
|
|||
TryUse((JsonObject)item.Value!, false); |
|||
action(this, state, isFirst, isLast); |
|||
i++; |
|||
} |
|||
|
|||
currentState = prevState; |
|||
} |
|||
|
|||
public string ToMarkdown() |
|||
{ |
|||
var sb = DefaultPools.StringBuilder.Get(); |
|||
try |
|||
{ |
|||
MarkdownVisitor.Render(this, sb); |
|||
return sb.ToString(); |
|||
} |
|||
finally |
|||
{ |
|||
DefaultPools.StringBuilder.Return(sb); |
|||
} |
|||
} |
|||
|
|||
public string ToHtml(int indentation = 4) |
|||
{ |
|||
var sb = DefaultPools.StringBuilder.Get(); |
|||
try |
|||
{ |
|||
HtmlWriterVisitor.Render(this, sb, new HtmlWriterOptions { Indentation = indentation }); |
|||
return sb.ToString(); |
|||
} |
|||
finally |
|||
{ |
|||
DefaultPools.StringBuilder.Return(sb); |
|||
} |
|||
} |
|||
|
|||
public string ToText(int maxLength = int.MaxValue) |
|||
{ |
|||
var sb = DefaultPools.StringBuilder.Get(); |
|||
try |
|||
{ |
|||
TextVisitor.Render(this, sb, maxLength); |
|||
return sb.ToString(); |
|||
} |
|||
finally |
|||
{ |
|||
DefaultPools.StringBuilder.Return(sb); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Collections; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas; |
|||
|
|||
public sealed record RichTextFieldProperties : FieldProperties |
|||
{ |
|||
public string? FolderId { get; init; } |
|||
|
|||
public int? MinLength { get; init; } |
|||
|
|||
public int? MaxLength { get; init; } |
|||
|
|||
public int? MinCharacters { get; init; } |
|||
|
|||
public int? MaxCharacters { get; init; } |
|||
|
|||
public int? MinWords { get; init; } |
|||
|
|||
public int? MaxWords { get; init; } |
|||
|
|||
public ReadonlyList<string>? ClassNames { get; init; } |
|||
|
|||
public ReadonlyList<DomainId>? SchemaIds { get; init; } |
|||
|
|||
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) |
|||
{ |
|||
return visitor.Visit(this, args); |
|||
} |
|||
|
|||
public override T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, IField field, TArgs args) |
|||
{ |
|||
return visitor.Visit((IField<RichTextFieldProperties>)field, args); |
|||
} |
|||
|
|||
public override RootField CreateRootField(long id, string name, Partitioning partitioning) |
|||
{ |
|||
return Fields.RichText(id, name, partitioning, this); |
|||
} |
|||
|
|||
public override NestedField CreateNestedField(long id, string name) |
|||
{ |
|||
return Fields.RichText(id, name, this); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Collections; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; |
|||
|
|||
internal sealed class RichTextGraphType : ObjectGraphType<RichTextNode> |
|||
{ |
|||
public RichTextGraphType(Builder builder, FieldInfo fieldInfo, RichTextFieldProperties properties) |
|||
{ |
|||
// The name is used for equal comparison. Therefore it is important to treat it as readonly.
|
|||
Name = fieldInfo.RichTextType; |
|||
|
|||
AddField(ContentFields.RichTextFieldValue); |
|||
AddField(ContentFields.RichTextFieldHtml); |
|||
AddField(ContentFields.RichTextFieldMarkdown); |
|||
AddField(ContentFields.RichTextFieldText); |
|||
AddField(ContentFields.RichTextFieldAssets); |
|||
|
|||
var referenceType = ResolveReferences(builder, fieldInfo, properties.SchemaIds); |
|||
|
|||
if (referenceType != null) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = "contents", |
|||
ResolvedType = new NonNullGraphType(new ListGraphType(new NonNullGraphType(referenceType))), |
|||
Resolver = ContentFields.ResolveRichTextFieldContents, |
|||
Description = FieldDescriptions.RichTextFieldReferences |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private static IGraphType? ResolveReferences(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds) |
|||
{ |
|||
IGraphType? contentType = null; |
|||
|
|||
if (schemaIds?.Count == 1) |
|||
{ |
|||
contentType = builder.GetContentType(schemaIds[0]); |
|||
} |
|||
|
|||
if (contentType == null) |
|||
{ |
|||
var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, schemaIds); |
|||
|
|||
if (union.SchemaTypes.Count == 0) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
contentType = union; |
|||
} |
|||
|
|||
return contentType; |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps; |
|||
|
|||
public sealed class CalculatePreviewText : IContentEnricherStep |
|||
{ |
|||
public async Task EnrichAsync(Context context, IEnumerable<EnrichedContent> contents, ProvideSchema schemas, |
|||
CancellationToken ct) |
|||
{ |
|||
// Reuse the node for all contents.
|
|||
var node = new RichTextNode(); |
|||
|
|||
// Group by schema, so we only fetch the schema once.
|
|||
foreach (var group in contents.GroupBy(x => x.SchemaId.Id)) |
|||
{ |
|||
var (schema, components) = await schemas(group.Key); |
|||
|
|||
AddTexts(schema, node, group); |
|||
} |
|||
} |
|||
|
|||
private void AddTexts(Schema schema, RichTextNode node, IEnumerable<EnrichedContent> contents) |
|||
{ |
|||
foreach (var content in contents) |
|||
{ |
|||
foreach (var field in schema.Fields.Where(x => x.RawProperties is RichTextFieldProperties)) |
|||
{ |
|||
if (!content.Data.TryGetValue(field.Name, out var fieldData) || fieldData is not { Count: > 0 }) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
content.ReferenceData ??= []; |
|||
|
|||
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; |
|||
|
|||
foreach (var (partitionKey, partitionValue) in fieldData) |
|||
{ |
|||
// Only handle the content if the text is valid.
|
|||
if (node.TryUse(partitionValue)) |
|||
{ |
|||
fieldReference[partitionKey] = node.ToText(100); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Collections; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields; |
|||
|
|||
[OpenApiRequest] |
|||
public sealed class RichTextFieldPropertiesDto : FieldPropertiesDto |
|||
{ |
|||
/// <summary>
|
|||
/// The initial id to the folder when the control supports file uploads.
|
|||
/// </summary>
|
|||
public string? FolderId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The minimum allowed length for the field value.
|
|||
/// </summary>
|
|||
public int? MinLength { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The maximum allowed length for the field value.
|
|||
/// </summary>
|
|||
public int? MaxLength { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The minimum allowed of normal characters for the field value.
|
|||
/// </summary>
|
|||
public int? MinCharacters { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The maximum allowed of normal characters for the field value.
|
|||
/// </summary>
|
|||
public int? MaxCharacters { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The minimum allowed number of words for the field value.
|
|||
/// </summary>
|
|||
public int? MinWords { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The maximum allowed number of words for the field value.
|
|||
/// </summary>
|
|||
public int? MaxWords { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The class names for the editor.
|
|||
/// </summary>
|
|||
public ReadonlyList<string>? ClassNames { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The allowed schema ids that can be embedded.
|
|||
/// </summary>
|
|||
public ReadonlyList<DomainId>? SchemaIds { get; init; } |
|||
|
|||
public static RichTextFieldPropertiesDto FromDomain(RichTextFieldProperties fieldProperties) |
|||
{ |
|||
return SimpleMapper.Map(fieldProperties, new RichTextFieldPropertiesDto()); |
|||
} |
|||
|
|||
public override FieldProperties ToProperties() |
|||
{ |
|||
return SimpleMapper.Map(this, new RichTextFieldProperties()); |
|||
} |
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,28 @@ |
|||
<h1>Header</h1> |
|||
<p>Content with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <code>code</code> and <span class="__editor_text-left">a class</span>.</p> |
|||
<blockquote> |
|||
<p>Quote</p> |
|||
</blockquote> |
|||
<pre class="language-javascript"> |
|||
<code data-code-block-language="javascript">Code Block in Javascript</code> |
|||
</pre> |
|||
<p>Just another paragraph</p> |
|||
<hr> |
|||
<ul> |
|||
<li> |
|||
<p>Item 1</p> |
|||
</li> |
|||
<li> |
|||
<p>Item 2</p> |
|||
</li> |
|||
</ul> |
|||
<ol> |
|||
<li> |
|||
<p>Item A</p> |
|||
</li> |
|||
<li> |
|||
<p>Item B</p> |
|||
</li> |
|||
</ol> |
|||
<p><a href="Link Content" rel="noopener noreferrer nofollow">A link</a></p> |
|||
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Adams_The_Tetons_and_the_Snake_River.jpg/1280px-Adams_The_Tetons_and_the_Snake_River.jpg" title="My Image"></p> |
|||
@ -0,0 +1,256 @@ |
|||
{ |
|||
"type": "doc", |
|||
"content": [ |
|||
{ |
|||
"type": "heading", |
|||
"attrs": { |
|||
"level": 1 |
|||
}, |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Header" |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Content with " |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"marks": [ |
|||
{ |
|||
"type": "bold" |
|||
} |
|||
], |
|||
"text": "bold" |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"text": ", " |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"marks": [ |
|||
{ |
|||
"type": "italic" |
|||
} |
|||
], |
|||
"text": "italic" |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"text": ", " |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"marks": [ |
|||
{ |
|||
"type": "underline" |
|||
} |
|||
], |
|||
"text": "underline" |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"text": ", " |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"marks": [ |
|||
{ |
|||
"type": "code" |
|||
} |
|||
], |
|||
"text": "code" |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"text": " and " |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"marks": [ |
|||
{ |
|||
"type": "className", |
|||
"attrs": { |
|||
"className": "text-left" |
|||
} |
|||
} |
|||
], |
|||
"text": "a class" |
|||
}, |
|||
{ |
|||
"type": "text", |
|||
"text": "." |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "blockquote", |
|||
"content": [ |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Quote" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "codeBlock", |
|||
"attrs": { |
|||
"language": "javascript", |
|||
"wrap": false |
|||
}, |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Code Block in Javascript" |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Just another paragraph" |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "horizontalRule" |
|||
}, |
|||
{ |
|||
"type": "bulletList", |
|||
"content": [ |
|||
{ |
|||
"type": "listItem", |
|||
"attrs": { |
|||
"closed": false, |
|||
"nested": false |
|||
}, |
|||
"content": [ |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Item 1" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "listItem", |
|||
"attrs": { |
|||
"closed": false, |
|||
"nested": false |
|||
}, |
|||
"content": [ |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Item 2" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "orderedList", |
|||
"attrs": { |
|||
"order": 1 |
|||
}, |
|||
"content": [ |
|||
{ |
|||
"type": "listItem", |
|||
"attrs": { |
|||
"closed": false, |
|||
"nested": false |
|||
}, |
|||
"content": [ |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Item A" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "listItem", |
|||
"attrs": { |
|||
"closed": false, |
|||
"nested": false |
|||
}, |
|||
"content": [ |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Item B" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "text", |
|||
"marks": [ |
|||
{ |
|||
"type": "link", |
|||
"attrs": { |
|||
"href": "Link Content", |
|||
"target": null, |
|||
"auto": false |
|||
} |
|||
} |
|||
], |
|||
"text": "A link" |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
"type": "paragraph", |
|||
"content": [ |
|||
{ |
|||
"type": "image", |
|||
"attrs": { |
|||
"alt": "", |
|||
"crop": null, |
|||
"height": null, |
|||
"width": null, |
|||
"rotate": null, |
|||
"src": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Adams_The_Tetons_and_the_Snake_River.jpg/1280px-Adams_The_Tetons_and_the_Snake_River.jpg", |
|||
"title": "My Image", |
|||
"fileName": null, |
|||
"resizable": false |
|||
} |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
# Header |
|||
|
|||
Content with **bold**, *italic*, underline, `code` and a class. |
|||
|
|||
> Quote |
|||
|
|||
```javascript |
|||
Code Block in Javascript |
|||
``` |
|||
|
|||
Just another paragraph |
|||
|
|||
--- |
|||
|
|||
* Item 1 |
|||
* Item 2 |
|||
|
|||
1. Item A |
|||
2. Item B |
|||
|
|||
[A link](Link Content) |
|||
|
|||
 |
|||
@ -0,0 +1 @@ |
|||
<h1>Header</h1><p>Content with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <code>code</code> and <span class="__editor_text-left">a class</span>.</p><blockquote><p>Quote</p></blockquote><pre class="language-javascript"><code data-code-block-language="javascript">Code Block in Javascript</code></pre><p>Just another paragraph</p><hr><ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul><ol><li><p>Item A</p></li><li><p>Item B</p></li></ol><p><a href="Link Content" rel="noopener noreferrer nofollow">A link</a></p><p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Adams_The_Tetons_and_the_Snake_River.jpg/1280px-Adams_The_Tetons_and_the_Snake_River.jpg" title="My Image"></p> |
|||
@ -0,0 +1 @@ |
|||
Header Content with bold, italic, underline, code and a class. Quote Code Block in Javascript Just another paragraph Item 1 Item 2 Item A Item B A link |
|||
@ -0,0 +1,56 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Model.Contents; |
|||
|
|||
public class RichTextTests |
|||
{ |
|||
private readonly RichTextNode node = new RichTextNode(); |
|||
|
|||
public RichTextTests() |
|||
{ |
|||
var json = TestUtils.DefaultSerializer.Deserialize<JsonValue>(File.ReadAllText("Model/Contents/ComplexText.json")); |
|||
|
|||
node.TryUse(json); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_format_to_html() |
|||
{ |
|||
var expected = File.ReadAllText("Model/Contents/ComplexText.html"); |
|||
|
|||
Assert.Equal(expected.Trim(), node.ToHtml().Trim()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_format_to_minimized_html() |
|||
{ |
|||
var expected = File.ReadAllText("Model/Contents/ComplexText.min.html"); |
|||
|
|||
Assert.Equal(expected.Trim(), node.ToHtml(0).Trim()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_format_to_markdown() |
|||
{ |
|||
var expected = File.ReadAllText("Model/Contents/ComplexText.md"); |
|||
|
|||
Assert.Equal(expected.Trim(), node.ToMarkdown().Trim()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_format_to_text() |
|||
{ |
|||
var expected = File.ReadAllText("Model/Contents/ComplexText.txt"); |
|||
|
|||
Assert.Equal(expected.Trim(), node.ToText().Trim()); |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent; |
|||
|
|||
public class RichTextFieldTests : IClassFixture<TranslationsFixture> |
|||
{ |
|||
private readonly List<string> errors = []; |
|||
|
|||
[Fact] |
|||
public void Should_instantiate_field() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties()); |
|||
|
|||
Assert.Equal("myRichText", sut.Name); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_value_is_null() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties()); |
|||
|
|||
await sut.ValidateAsync(CreateValue(null), errors); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_rich_text_is_valid() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties()); |
|||
|
|||
await sut.ValidateAsync(CreateValue("text"), errors); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_add_error_if_rich_text_is_invalid() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties()); |
|||
|
|||
await sut.ValidateAsync(CreateValue(string.Empty, "unknown"), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Invalid rich text." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_required_but_null() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { IsRequired = true }); |
|||
|
|||
await sut.ValidateAsync(CreateValue(null), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Field is required." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_required_but_empty() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { IsRequired = true }); |
|||
|
|||
await sut.ValidateAsync(CreateValue(string.Empty), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Field is required." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_shorter_than_min_length() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { MinLength = 10 }); |
|||
|
|||
await sut.ValidateAsync(CreateValue("123"), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Must have at least 10 character(s)." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_longer_than_max_length() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { MaxLength = 5 }); |
|||
|
|||
await sut.ValidateAsync(CreateValue("12345678"), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Must not have more than 5 character(s)." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_shorter_than_min_characters() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { MinCharacters = 10 }); |
|||
|
|||
await sut.ValidateAsync(CreateValue("123"), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Must have at least 10 text character(s)." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_longer_than_max_characters() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { MaxCharacters = 5 }); |
|||
|
|||
await sut.ValidateAsync(CreateValue("12345678"), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Must not have more than 5 text character(s)." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_shorter_than_min_words() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { MinWords = 10 }); |
|||
|
|||
await sut.ValidateAsync(CreateValue("word1 word2 word3"), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Must have at least 10 word(s)." }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_add_error_if_string_is_longer_than_max_words() |
|||
{ |
|||
var sut = Field(new RichTextFieldProperties { MaxWords = 5 }); |
|||
|
|||
await sut.ValidateAsync(CreateValue("word1 word2 word3 word4 word5 word6"), errors); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new[] { "Must not have more than 5 word(s)." }); |
|||
} |
|||
|
|||
private static JsonValue CreateValue(string? v, string? type = null) |
|||
{ |
|||
if (v == null) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
return JsonValue.Object() |
|||
.Add("type", "doc") |
|||
.Add("content", JsonValue.Array( |
|||
JsonValue.Object() |
|||
.Add("type", "paragraph") |
|||
.Add("content", JsonValue.Array( |
|||
JsonValue.Object() |
|||
.Add("type", type ?? "text") |
|||
.Add("text", v))))); |
|||
} |
|||
|
|||
private static RootField<RichTextFieldProperties> Field(RichTextFieldProperties properties) |
|||
{ |
|||
return Fields.RichText(1, "myRichText", Partitioning.Invariant, properties); |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure.Json.Objects; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Queries; |
|||
|
|||
public class CalculatePreviewTextTests : GivenContext |
|||
{ |
|||
private readonly CalculatePreviewText sut; |
|||
|
|||
public CalculatePreviewTextTests() |
|||
{ |
|||
sut = new CalculatePreviewText(); |
|||
|
|||
Schema = Schema.AddRichText(1, "richText", Partitioning.Language); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_compute_texts() |
|||
{ |
|||
var content = CreateContentWithData(); |
|||
|
|||
await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken); |
|||
|
|||
Assert.Equal("Text1Text2", content.ReferenceData!["richText"]!["en"]); |
|||
Assert.Equal("Text3Text4", content.ReferenceData!["richText"]!["de"]); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_compute_texts_if_field_not_found() |
|||
{ |
|||
var content = CreateContent(); |
|||
|
|||
await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken); |
|||
|
|||
Assert.Null(content.ReferenceData); |
|||
} |
|||
|
|||
private EnrichedContent CreateContentWithData() |
|||
{ |
|||
var content = CreateContent() with |
|||
{ |
|||
Data = |
|||
new ContentData() |
|||
.AddField("richText", |
|||
new ContentFieldData() |
|||
.AddLocalized("en", |
|||
JsonValue.Object() |
|||
.Add("type", "paragraph") |
|||
.Add("content", JsonValue.Array( |
|||
JsonValue.Object() |
|||
.Add("type", "text") |
|||
.Add("text", "Text1"), |
|||
JsonValue.Object() |
|||
.Add("type", "text") |
|||
.Add("text", "Text2")))) |
|||
.AddLocalized("de", |
|||
JsonValue.Object() |
|||
.Add("type", "paragraph") |
|||
.Add("content", JsonValue.Array( |
|||
JsonValue.Object() |
|||
.Add("type", "text") |
|||
.Add("text", "Text3"), |
|||
JsonValue.Object() |
|||
.Add("type", "text") |
|||
.Add("text", "Text4"))))) |
|||
}; |
|||
|
|||
return content; |
|||
} |
|||
|
|||
private ProvideSchema SchemaProvider() |
|||
{ |
|||
return x => Task.FromResult((Schema, ResolvedComponents.Empty)); |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Infrastructure.Validation; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties; |
|||
|
|||
public class RichTextFieldPropertiesTests : IClassFixture<TranslationsFixture> |
|||
{ |
|||
[Fact] |
|||
public void Should_not_add_error() |
|||
{ |
|||
var sut = new RichTextFieldProperties(); |
|||
|
|||
var errors = FieldPropertiesValidator.Validate(sut).ToList(); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_min_length_greater_than_max() |
|||
{ |
|||
var sut = new RichTextFieldProperties { MinLength = 10, MaxLength = 5 }; |
|||
|
|||
var errors = FieldPropertiesValidator.Validate(sut).ToList(); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new List<ValidationError> |
|||
{ |
|||
new ValidationError("Max length must be greater or equal to min length.", "MinLength", "MaxLength") |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_add_error_if_min_length_equal_to_max_length() |
|||
{ |
|||
var sut = new RichTextFieldProperties { MinLength = 2, MaxLength = 2 }; |
|||
|
|||
var errors = FieldPropertiesValidator.Validate(sut).ToList(); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_min_characters_greater_than_max() |
|||
{ |
|||
var sut = new RichTextFieldProperties { MinCharacters = 10, MaxCharacters = 5 }; |
|||
|
|||
var errors = FieldPropertiesValidator.Validate(sut).ToList(); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new List<ValidationError> |
|||
{ |
|||
new ValidationError("Max characters must be greater or equal to min characters.", "MinCharacters", "MaxCharacters") |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_add_error_if_min_characters_equal_to_max_characters() |
|||
{ |
|||
var sut = new RichTextFieldProperties { MinCharacters = 2, MaxCharacters = 2 }; |
|||
|
|||
var errors = FieldPropertiesValidator.Validate(sut).ToList(); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_add_error_if_min_words_greater_than_max() |
|||
{ |
|||
var sut = new RichTextFieldProperties { MinWords = 10, MaxWords = 5 }; |
|||
|
|||
var errors = FieldPropertiesValidator.Validate(sut).ToList(); |
|||
|
|||
errors.Should().BeEquivalentTo( |
|||
new List<ValidationError> |
|||
{ |
|||
new ValidationError("Max words must be greater or equal to min words.", "MinWords", "MaxWords") |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_add_error_if_min_words_equal_to_max_words() |
|||
{ |
|||
var sut = new RichTextFieldProperties { MinWords = 2, MaxWords = 2 }; |
|||
|
|||
var errors = FieldPropertiesValidator.Validate(sut).ToList(); |
|||
|
|||
Assert.Empty(errors); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<div [formGroup]="fieldForm"> |
|||
<div class="form-group row"> |
|||
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.classNames' | sqxTranslate }}</label> |
|||
|
|||
<div class="col-9"> |
|||
<sqx-tag-editor formControlName="classNames"></sqx-tag-editor> |
|||
|
|||
<sqx-form-hint> |
|||
{{ 'schemas.fieldTypes.string.classNamesHint' | sqxTranslate }} |
|||
</sqx-form-hint> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col-3 col-form-label" for="{{field.fieldId}}_folderId">{{ 'schemas.fieldTypes.string.folderId' | sqxTranslate }}</label> |
|||
|
|||
<div class="col-9"> |
|||
<sqx-asset-folder-dropdown formControlName="folderId"></sqx-asset-folder-dropdown> |
|||
|
|||
<sqx-form-hint> |
|||
{{ 'schemas.fieldTypes.string.folderIdHint' | sqxTranslate }} |
|||
</sqx-form-hint> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldSchemaIds">{{ 'common.schemas' | sqxTranslate }}</label> |
|||
|
|||
<div class="col-9"> |
|||
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds" |
|||
[itemConverter]="(schemasSource.normalConverter | async)!" |
|||
[itemsSource]="(schemasSource.normalConverter | async)?.suggestions"> |
|||
</sqx-tag-editor> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,2 @@ |
|||
@import 'mixins'; |
|||
@import 'vars'; |
|||
@ -0,0 +1,41 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AsyncPipe } from '@angular/common'; |
|||
import { Component, Input } from '@angular/core'; |
|||
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; |
|||
import { FieldDto, FormHintComponent, RichTextFieldPropertiesDto, SchemaTagSource, TagEditorComponent, TranslatePipe } from '@app/shared'; |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: 'sqx-rich-text-ui', |
|||
styleUrls: ['rich-text-ui.component.scss'], |
|||
templateUrl: 'rich-text-ui.component.html', |
|||
imports: [ |
|||
AsyncPipe, |
|||
FormHintComponent, |
|||
FormsModule, |
|||
ReactiveFormsModule, |
|||
TagEditorComponent, |
|||
TranslatePipe, |
|||
], |
|||
}) |
|||
export class RichTextUIComponent { |
|||
@Input({ required: true }) |
|||
public fieldForm!: UntypedFormGroup; |
|||
|
|||
@Input({ required: true }) |
|||
public field!: FieldDto; |
|||
|
|||
@Input({ required: true }) |
|||
public properties!: RichTextFieldPropertiesDto; |
|||
|
|||
constructor( |
|||
public readonly schemasSource: SchemaTagSource, |
|||
) { |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
<div [formGroup]="fieldForm"> |
|||
<div class="form-group row"> |
|||
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.length' | sqxTranslate }}</label> |
|||
|
|||
<div class="col-9"> |
|||
<div class="row g-0"> |
|||
<div class="col"> |
|||
<input type="number" class="form-control" formControlName="minLength" placeholder="{{ 'schemas.fieldTypes.string.lengthMin' | sqxTranslate }}"> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<label class="col-form-label minmax">-</label> |
|||
</div> |
|||
<div class="col"> |
|||
<input type="number" class="form-control" formControlName="maxLength" placeholder="{{ 'schemas.fieldTypes.string.lengthMax' | sqxTranslate }}"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.characters' | sqxTranslate }}</label> |
|||
|
|||
<div class="col-9"> |
|||
<div class="row g-0"> |
|||
<div class="col"> |
|||
<input type="number" class="form-control" formControlName="minCharacters" placeholder="{{ 'schemas.fieldTypes.string.charactersMin' | sqxTranslate }}"> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<label class="col-form-label minmax">-</label> |
|||
</div> |
|||
<div class="col"> |
|||
<input type="number" class="form-control" formControlName="maxCharacters" placeholder="{{ 'schemas.fieldTypes.string.charactersMax' | sqxTranslate }}"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.words' | sqxTranslate }}</label> |
|||
|
|||
<div class="col-9"> |
|||
<div class="row g-0"> |
|||
<div class="col"> |
|||
<input type="number" class="form-control" formControlName="minWords" placeholder="{{ 'schemas.fieldTypes.string.wordsMin' | sqxTranslate }}"> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<label class="col-form-label minmax">-</label> |
|||
</div> |
|||
<div class="col"> |
|||
<input type="number" class="form-control" formControlName="maxWords" placeholder="{{ 'schemas.fieldTypes.string.wordsMax' | sqxTranslate }}"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,33 @@ |
|||
@import 'mixins'; |
|||
@import 'vars'; |
|||
|
|||
.minmax { |
|||
text-align: center; |
|||
text-decoration: none; |
|||
width: 2rem; |
|||
} |
|||
|
|||
.control-dropdown { |
|||
max-width: 285px; |
|||
min-height: 0; |
|||
min-width: 200px; |
|||
|
|||
h4 { |
|||
padding: .5rem 0 0 .5rem; |
|||
} |
|||
|
|||
&-item { |
|||
font-size: $font-smallest; |
|||
font-weight: normal; |
|||
|
|||
&:hover { |
|||
.text-muted { |
|||
color: $color-white !important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.truncate { |
|||
@include truncate; |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, Input } from '@angular/core'; |
|||
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; |
|||
import { FieldDto, RichTextFieldPropertiesDto, TranslatePipe } from '@app/shared'; |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: 'sqx-rich-text-validation', |
|||
styleUrls: ['rich-text-validation.component.scss'], |
|||
templateUrl: 'rich-text-validation.component.html', |
|||
imports: [ |
|||
FormsModule, |
|||
ReactiveFormsModule, |
|||
TranslatePipe, |
|||
], |
|||
}) |
|||
export class RichTextValidationComponent { |
|||
@Input({ required: true }) |
|||
public fieldForm!: UntypedFormGroup; |
|||
|
|||
@Input({ required: true }) |
|||
public field!: FieldDto; |
|||
|
|||
@Input({ required: true }) |
|||
public properties!: RichTextFieldPropertiesDto; |
|||
} |
|||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue