diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs new file mode 100644 index 000000000..877e76d7a --- /dev/null +++ b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Json; + +namespace Squidex.Infrastructure.Translations +{ + public sealed class DeepLTranslator : ITranslator + { + private const string Url = "https://api.deepl.com/v2/translate"; + private readonly HttpClient httpClient = new HttpClient(); + private readonly string authKey; + private readonly IJsonSerializer jsonSerializer; + + private sealed class Response + { + public ResponseTranslation[] Translations { get; set; } + } + + private sealed class ResponseTranslation + { + public string Text { get; set; } + } + + public DeepLTranslator(string authKey, IJsonSerializer jsonSerializer) + { + Guard.NotNull(authKey, nameof(authKey)); + Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); + + this.authKey = authKey; + + this.jsonSerializer = jsonSerializer; + } + + public async Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(sourceText) || targetLanguage == null) + { + return new Translation(TranslationResult.NotTranslated, sourceText); + } + + var parameters = new Dictionary + { + ["auth_key"] = authKey, + ["text"] = sourceText, + ["target_lang"] = GetLanguageCode(targetLanguage) + }; + + if (sourceLanguage != null) + { + parameters["source_lang"] = GetLanguageCode(sourceLanguage); + } + + var response = await httpClient.PostAsync(Url, new FormUrlEncodedContent(parameters), ct); + var responseString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var result = jsonSerializer.Deserialize(responseString); + + if (result?.Translations?.Length == 1) + { + return new Translation(TranslationResult.Translated, result.Translations[0].Text); + } + } + + if (response.StatusCode == HttpStatusCode.BadRequest) + { + return new Translation(TranslationResult.LanguageNotSupported, resultText: responseString); + } + + return new Translation(TranslationResult.Failed, resultText: responseString); + } + + private string GetLanguageCode(Language language) + { + return language.Iso2Code.Substring(0, 2).ToUpperInvariant(); + } + } +} diff --git a/src/Squidex.Infrastructure/Translations/ITranslator.cs b/src/Squidex.Infrastructure/Translations/ITranslator.cs new file mode 100644 index 000000000..456923bb4 --- /dev/null +++ b/src/Squidex.Infrastructure/Translations/ITranslator.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Translations +{ + public interface ITranslator + { + Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default); + } +} diff --git a/src/Squidex.Infrastructure/Translations/NoopTranslator.cs b/src/Squidex.Infrastructure/Translations/NoopTranslator.cs new file mode 100644 index 000000000..e872675c9 --- /dev/null +++ b/src/Squidex.Infrastructure/Translations/NoopTranslator.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Translations +{ + public sealed class NoopTranslator : ITranslator + { + public Task Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default) + { + var result = new Translation(TranslationResult.NotImplemented); + + return Task.FromResult(result); + } + } +} diff --git a/src/Squidex.Infrastructure/Translations/Translation.cs b/src/Squidex.Infrastructure/Translations/Translation.cs new file mode 100644 index 000000000..7bcabad2a --- /dev/null +++ b/src/Squidex.Infrastructure/Translations/Translation.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Translations +{ + public sealed class Translation + { + public TranslationResult Result { get; } + + public string Text { get; } + + public string ResultText { get; set; } + + public Translation(TranslationResult result, string text = null, string resultText = null) + { + Text = text; + Result = result; + ResultText = resultText; + } + } +} diff --git a/src/Squidex.Infrastructure/Translations/TranslationResult.cs b/src/Squidex.Infrastructure/Translations/TranslationResult.cs new file mode 100644 index 000000000..f58f0ce45 --- /dev/null +++ b/src/Squidex.Infrastructure/Translations/TranslationResult.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Translations +{ + public enum TranslationResult + { + Translated, + LanguageNotSupported, + NotTranslated, + NotImplemented, + Failed + } +} diff --git a/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs b/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs index bcfd0f24e..0a7309d21 100644 --- a/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs +++ b/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.News.Service { public sealed class FeaturesService { - private const int FeatureVersion = 1; + private const int FeatureVersion = 2; private static readonly QueryContext Flatten = QueryContext.Default.Flatten(); private readonly SquidexClient client; diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 1da3dd5f3..777495ffd 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -37,7 +37,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// /// Get schemas. /// - /// The name of the app to get the schemas for. + /// The name of the app. /// /// 200 => Schemas returned. /// 404 => App not found. diff --git a/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs b/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs new file mode 100644 index 000000000..ee0c643de --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Infrastructure; + +namespace Squidex.Areas.Api.Controllers.Translations.Models +{ + public sealed class TranslateDto + { + /// + /// The text to translate. + /// + [Required] + public string Text { get; set; } + + /// + /// The target language. + /// + [Required] + public Language TargetLanguage { get; set; } + + /// + /// The optional source language. + /// + public Language SourceLanguage { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs b/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs new file mode 100644 index 000000000..a7582b3ea --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.Translations; + +namespace Squidex.Areas.Api.Controllers.Translations.Models +{ + public sealed class TranslationDto + { + /// + /// The result of the translation. + /// + public TranslationResult Result { get; set; } + + /// + /// The translated text. + /// + public string Text { get; set; } + + public static TranslationDto FromTranslation(Translation translation) + { + return SimpleMapper.Map(translation, new TranslationDto()); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs new file mode 100644 index 000000000..e5fbe040e --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Squidex.Areas.Api.Controllers.Translations.Models; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Translations; +using Squidex.Pipeline; +using Squidex.Shared; + +namespace Squidex.Areas.Api.Controllers.Translations +{ + /// + /// Manage translations. + /// + [ApiExplorerSettings(GroupName = nameof(Translations))] + public sealed class TranslationsController : ApiController + { + private readonly ITranslator translator; + + public TranslationsController(ICommandBus commandBus, ITranslator translator) + : base(commandBus) + { + this.translator = translator; + } + + /// + /// Translate a text. + /// + /// The name of the app. + /// The translation request. + /// + /// 200 => Text translated. + /// + [HttpPost] + [Route("apps/{app}/translations/")] + [ProducesResponseType(typeof(TranslationDto), 200)] + [ApiPermission(Permissions.AppCommon)] + [ApiCosts(0)] + public async Task GetLanguages(string app, [FromBody] TranslateDto request) + { + var result = await translator.Translate(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted); + var response = TranslationDto.FromTranslation(result); + + return Ok(response); + } + } +} diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index bdfedbaf5..33687222d 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NodaTime; using Squidex.Areas.Api.Controllers.News.Service; @@ -18,6 +19,8 @@ using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Diagnostics; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.UsageTracking; using Squidex.Shared.Users; @@ -27,8 +30,21 @@ namespace Squidex.Config.Domain { public static class InfrastructureServices { - public static void AddMyInfrastructureServices(this IServiceCollection services) + public static void AddMyInfrastructureServices(this IServiceCollection services, IConfiguration config) { + var deeplAuthKey = config.GetValue("translations:deeplAuthKey"); + + if (!string.IsNullOrWhiteSpace(deeplAuthKey)) + { + services.AddSingletonAs(c => new DeepLTranslator(deeplAuthKey, c.GetRequiredService())) + .As(); + } + else + { + services.AddSingletonAs() + .As(); + } + services.AddHealthChecks() .AddCheck("GC", tags: new[] { "node" }) .AddCheck("Orleans", tags: new[] { "cluster" }) diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index b657d4083..b68fffec1 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -58,7 +58,7 @@ namespace Squidex services.AddMyEventPublishersServices(config); services.AddMyEventStoreServices(config); services.AddMyIdentityServer(); - services.AddMyInfrastructureServices(); + services.AddMyInfrastructureServices(config); services.AddMyLoggingServices(config); services.AddMyMigrationServices(); services.AddMyMvc(); diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index 0c51c3304..61d0af7d5 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -51,7 +51,7 @@ - + {{userInfo.user.displayName}} diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.html b/src/Squidex/app/features/assets/pages/assets-page.component.html index 8c27b3e4c..2e6170d44 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-page.component.html @@ -53,9 +53,7 @@
- Filters - - +
diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 3ea27986d..9b8ed3a6a 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -2,6 +2,10 @@
+ + ; public isDifferent: Observable; + public isTranslateable: boolean; constructor( - private readonly localStore: LocalStoreService + private readonly appsState: AppsState, + private readonly localStore: LocalStoreService, + private readonly translations: TranslationsService ) { } public ngOnChanges(changes: SimpleChanges) { + if (changes['field']) { + this.showAllControls = this.localStore.getBoolean(this.configKey()); + } + if (changes['fieldForm']) { this.isInvalid = invalid$(this.fieldForm); } + if (changes['fieldForm'] || changes['field'] || changes['languages']) { + this.isTranslateable = this.field.isTranslateable; + } + if ((changes['fieldForm'] || changes['fieldFormCompare']) && this.fieldFormCompare) { this.isDifferent = value$(this.fieldForm).pipe( @@ -77,10 +91,6 @@ export class ContentFieldComponent implements OnChanges { (lhs, rhs) => !Types.jsJsonEquals(lhs, rhs))); } - if (changes['field']) { - this.showAllControls = this.localStore.getBoolean(this.configKey()); - } - const control = this.findControl(this.fieldForm); if (this.selectedFormControl !== control) { @@ -120,6 +130,46 @@ export class ContentFieldComponent implements OnChanges { } } + public translate() { + const master = this.languages.find(x => x.isMaster); + + if (master) { + const masterCode = master.iso2Code; + const masterValue = this.fieldForm.get(masterCode)!.value; + + if (masterValue) { + if (this.showAllControls) { + for (let language of this.languages) { + if (!language.isMaster) { + this.translateValue(masterValue, masterCode, language.iso2Code); + } + } + } else { + this.translateValue(masterValue, masterCode, this.language.iso2Code); + } + } + } + } + + private translateValue(text: string, sourceLanguage: string, targetLanguage: string) { + const control = this.fieldForm.get(targetLanguage); + + if (control) { + const value = control.value; + + if (!value) { + const request = new TranslateDto(text, sourceLanguage, targetLanguage); + + this.translations.translate(this.appsState.appName, request) + .subscribe(result => { + if (result.text) { + control.setValue(result.text); + } + }); + } + } + } + private findControl(form: FormGroup) { if (this.field.isLocalizable) { return form.controls[this.language.iso2Code]; @@ -129,7 +179,7 @@ export class ContentFieldComponent implements OnChanges { } public prefix(language: AppLanguageDto) { - return `(${language.iso2Code}`; + return `(${language.iso2Code})`; } public trackByLanguage(index: number, language: AppLanguageDto) { diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.html b/src/Squidex/app/features/content/pages/content/content-history-page.component.html index 7ca0c7449..a77ff3c0e 100644 --- a/src/Squidex/app/features/content/pages/content/content-history-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-history-page.component.html @@ -6,7 +6,7 @@
- +
diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index a0fb8001b..fd527a854 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -126,19 +126,15 @@
- commentsLink - - + - History - - + - + The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
diff --git a/src/Squidex/app/features/content/pages/content/field-languages.component.ts b/src/Squidex/app/features/content/pages/content/field-languages.component.ts index 08d6651d2..d878f1a51 100644 --- a/src/Squidex/app/features/content/pages/content/field-languages.component.ts +++ b/src/Squidex/app/features/content/pages/content/field-languages.component.ts @@ -24,7 +24,7 @@ import { AppLanguageDto, RootFieldDto } from '@app/shared'; [languages]="languages"> - + Please remember to check all languages when you see validation errors.
diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index 26f70f2c6..e4da7203b 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -126,9 +126,7 @@
- Filters - - +
diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 5fd8b28c0..925663fb5 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -32,7 +32,7 @@ - + diff --git a/src/Squidex/app/features/content/shared/content-status.component.html b/src/Squidex/app/features/content/shared/content-status.component.html index 33e7cff46..22de0d0b8 100644 --- a/src/Squidex/app/features/content/shared/content-status.component.html +++ b/src/Squidex/app/features/content/shared/content-status.component.html @@ -1,17 +1,13 @@ - + - - {{displayStatus}} - + - - Will be set to '{{scheduledTo}}' at {{scheduledAt | sqxFullDateTime}} {{displayStatus}} \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content-status.component.ts b/src/Squidex/app/features/content/shared/content-status.component.ts index 918a9f3e4..527002671 100644 --- a/src/Squidex/app/features/content/shared/content-status.component.ts +++ b/src/Squidex/app/features/content/shared/content-status.component.ts @@ -35,7 +35,13 @@ export class ContentStatusComponent { public alignMiddle = true; public get displayStatus() { - return !!this.isPending ? 'Pending' : this.status; + if (this.scheduledAt) { + return `Will be set to '${this.scheduledTo}' at ${this.scheduledAt.toStringFormat('LLLL')}`; + } else if (this.isPending) { + return 'Pending'; + } else { + return this.status; + } } } diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html index aff31a2ad..313951c46 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -77,19 +77,15 @@
- Events - - + - Help - - + - + Click the help icon to show a context specific help page. Go to https://docs.squidex.io for the full documentation.
diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.html b/src/Squidex/app/features/schemas/pages/schema/field.component.html index fce705986..03d3495c2 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.html @@ -7,7 +7,7 @@ - {{field.displayName}} + {{field.displayName}} localizable
diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 46983f919..3e1bbe227 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -48,11 +48,11 @@
- + Open the context menu to delete the schema or to create some scripts for content changes. - + Note, that you have to publish the schema before you can add content to it.
@@ -82,10 +82,8 @@ -
- Help - - + diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html index 5906e2444..8a04729f8 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -94,9 +94,7 @@
- Help - - +
diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html index b190aca78..b0fff5866 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html @@ -48,15 +48,11 @@
- History - - + - Help - - +
diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html index f4f9a428c..5019d2d82 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html @@ -26,7 +26,7 @@ - + {{contributorInfo.contributor.contributorId | sqxUserName}} @@ -74,15 +74,11 @@
- History - - + - Help - - +
diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html index 25d942fc8..ae44142e4 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html @@ -44,15 +44,11 @@
- History - - + - Help - - +
diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html index 9b428a6b9..b82d2553d 100644 --- a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html +++ b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html @@ -26,15 +26,11 @@
- History - - + - Help - - +
diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html index c4849ea5f..c1118dc6c 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html @@ -79,10 +79,8 @@
-
- History - - + diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html b/src/Squidex/app/features/settings/pages/roles/roles-page.component.html index 1d44a7fd6..4d756d031 100644 --- a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html +++ b/src/Squidex/app/features/settings/pages/roles/roles-page.component.html @@ -39,15 +39,11 @@
- History - - + - Help - - +
diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/src/Squidex/app/framework/angular/forms/autocomplete.component.html index ea2df131d..326206e1d 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.html +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.html @@ -5,7 +5,7 @@ autocorrect="off" autocapitalize="off"> -
+
-
+
-
+
-
\ No newline at end of file +
+ + +
+ {{tooltip.text}} +
+
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss index f86958242..76fde5578 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss @@ -1,6 +1,8 @@ @import '_mixins'; @import '_vars'; +// sass-lint:disable single-line-per-selector + .notification-container { & { margin: .625rem; @@ -14,19 +16,65 @@ max-height: 20rem; } - &-topright { + &-top-right { @include fixed(0, 0, auto, auto); } - &-topleft { + &-top-left { @include fixed(0, auto, auto, 0); } - &-bottomright { + &-bottom-right { @include fixed(auto, 0, 0, auto); } - &-bottomleft { + &-bottom-left { @include fixed(auto, auto, 0, 0); } +} + +$caret-size: 6px; + +.tooltip2 { + & { + color: $color-dark-foreground; + background: $color-tooltip; + border: 0; + font-size: .9rem; + font-weight: normal; + white-space: nowrap; + padding: .5rem; + } + + &-left { + &::after { + @include caret-right($color-tooltip, $caret-size); + @include absolute(50%, -$caret-size * 2, auto, auto); + margin-top: -$caret-size; + } + } + + &-right { + &::after { + @include caret-left($color-tooltip, $caret-size); + @include absolute(50%, auto, auto, -$caret-size * 2); + margin-top: -$caret-size; + } + } + + &-top { + &::after { + @include caret-bottom($color-tooltip, $caret-size); + @include absolute(auto, auto, -$caret-size * 2, 50%); + margin-left: -$caret-size; + } + } + + &-bottom { + &::after { + @include caret-top($color-tooltip, $caret-size); + @include absolute(-$caret-size * 2, auto, auto, 50%); + margin-left: -$caret-size; + } + } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts index 594c3fc9a..873f4ced7 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights r vbeserved */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { timer } from 'rxjs'; import { @@ -16,11 +16,14 @@ import { Notification, StatefulComponent } from '@app/framework/internal'; +import { Tooltip } from '@app/shared'; interface State { dialogRequest?: DialogRequest | null; notifications: Notification[]; + + tooltip?: Tooltip | null; } @Component({ @@ -33,9 +36,6 @@ interface State { changeDetection: ChangeDetectionStrategy.OnPush }) export class DialogRendererComponent extends StatefulComponent implements OnInit { - @Input() - public position = 'bottomright'; - public dialogView = new DialogModel(); constructor(changeDetector: ChangeDetectorRef, @@ -75,6 +75,16 @@ export class DialogRendererComponent extends StatefulComponent implements this.next(s => ({ ...s, dialogRequest })); })); + + this.own( + this.dialogs.tooltips + .subscribe(tooltip => { + if (tooltip.text) { + this.next(s => ({ ...s, tooltip })); + } else if (!this.snapshot.tooltip || tooltip.target === this.snapshot.tooltip.target) { + this.next(s => ({ ...s, tooltip: null })); + } + })); } public cancel() { diff --git a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts index 1cd5fd4c1..69111ca4e 100644 --- a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts @@ -5,35 +5,37 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, Renderer2 } from '@angular/core'; import { timer } from 'rxjs'; import { ResourceOwner } from '@app/framework/internal'; - -const POSITION_TOPLEFT = 'topLeft'; -const POSITION_TOPRIGHT = 'topRight'; -const POSITION_BOTTOMLEFT = 'bottomLeft'; -const POSITION_BOTTOMRIGHT = 'bottomRight'; -const POSITION_LEFTTOP = 'leftTop'; -const POSITION_LEFTBOTTOM = 'leftBottom'; -const POSITION_RIGHTTOP = 'rightTop'; -const POSITION_RIGHTBOTTOM = 'rightBottom'; -const POSITION_FULL = 'full'; +import { positionModal } from '@app/shared'; @Directive({ selector: '[sqxModalTarget]' }) -export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy, OnInit { +export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy { private targetElement: any; @Input('sqxModalTarget') - public target: any; + public set target(element: any) { + if (element !== this.targetElement) { + this.ngOnDestroy(); + + this.targetElement = element; + + if (element) { + this.subscribe(element); + this.updatePosition(); + } + } + } @Input() public offset = 2; @Input() - public position = POSITION_BOTTOMRIGHT; + public position = 'bottom-right'; @Input() public autoPosition = true; @@ -45,22 +47,18 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit super(); } - public ngOnInit() { - if (this.target) { - this.targetElement = this.target; + private subscribe(element: any) { + this.own( + this.renderer.listen(element, 'resize', () => { + this.updatePosition(); + })); - this.own( - this.renderer.listen(this.targetElement, 'resize', () => { - this.updatePosition(); - })); + this.own( + this.renderer.listen(this.element.nativeElement, 'resize', () => { + this.updatePosition(); + })); - this.own( - this.renderer.listen(this.element.nativeElement, 'resize', () => { - this.updatePosition(); - })); - - this.own(timer(100, 100).subscribe(() => this.updatePosition())); - } + this.own(timer(100, 100).subscribe(() => this.updatePosition())); } public ngAfterViewInit() { @@ -77,110 +75,35 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit return; } - const viewportHeight = document.documentElement!.clientHeight; - const viewportWidth = document.documentElement!.clientWidth; - const modalRef = this.element.nativeElement; const modalRect = this.element.nativeElement.getBoundingClientRect(); const targetRect: ClientRect = this.targetElement.getBoundingClientRect(); - const fix = this.autoPosition; - - let t = 0; - let l = 0; - - switch (this.position) { - case POSITION_LEFTTOP: - case POSITION_RIGHTTOP: { - t = targetRect.top; - break; - } - case POSITION_LEFTBOTTOM: - case POSITION_RIGHTBOTTOM: { - t = targetRect.bottom - modalRect.height; - break; - } - case POSITION_BOTTOMLEFT: - case POSITION_BOTTOMRIGHT: { - t = targetRect.bottom + this.offset; - - if (fix && t + modalRect.height > viewportHeight) { - const candidate = targetRect.top - modalRect.height - this.offset; - - if (candidate > 0) { - t = candidate; - } - } - break; - } - case POSITION_TOPLEFT: - case POSITION_TOPRIGHT: { - t = targetRect.top - modalRect.height - this.offset; - - if (fix && t < 0) { - const candidate = targetRect.bottom + this.offset; - - if (candidate + modalRect.height > viewportHeight) { - t = candidate; - } - } - break; - } - } - - switch (this.position) { - case POSITION_TOPLEFT: - case POSITION_BOTTOMLEFT: { - l = targetRect.left; - break; - } - case POSITION_TOPRIGHT: - case POSITION_BOTTOMRIGHT: { - l = targetRect.right - modalRect.width; - break; - } - case POSITION_RIGHTTOP: - case POSITION_RIGHTBOTTOM: { - l = targetRect.right + this.offset; - - if (fix && l + modalRect.width > viewportWidth) { - const candidate = targetRect.right - modalRect.width - this.offset; - - if (candidate > 0) { - l = candidate; - } - } - break; - } - case POSITION_LEFTTOP: - case POSITION_LEFTBOTTOM: { - l = targetRect.left - modalRect.width - this.offset; - - if (this.autoPosition && l < 0) { - const candidate = targetRect.right + this.offset; - - if (candidate + modalRect.width > viewportWidth) { - l = candidate; - } - } - break; - } - } + let y = 0; + let x = 0; - if (this.position === POSITION_FULL) { - t = targetRect.top - this.offset; - l = targetRect.left - this.offset; + if (this.position === 'full') { + x = -this.offset + targetRect.left; + y = -this.offset + targetRect.top; - const w = targetRect.width + 2 * this.offset; - const h = targetRect.height + 2 * this.offset; + const w = 2 * this.offset + targetRect.width; + const h = 2 * this.offset + targetRect.height; this.renderer.setStyle(modalRef, 'width', `${w}px`); this.renderer.setStyle(modalRef, 'height', `${h}px`); + } else { + const viewH = document.documentElement!.clientHeight; + const viewW = document.documentElement!.clientWidth; + + const position = positionModal(targetRect, modalRect, this.position, this.offset, this.autoPosition, viewW, viewH); + + x = position.x; + y = position.y; } - this.renderer.setStyle(modalRef, 'top', `${t}px`); - this.renderer.setStyle(modalRef, 'left', `${l}px`); + this.renderer.setStyle(modalRef, 'top', `${y}px`); + this.renderer.setStyle(modalRef, 'left', `${x}px`); this.renderer.setStyle(modalRef, 'right', 'auto'); this.renderer.setStyle(modalRef, 'bottom', 'auto'); this.renderer.setStyle(modalRef, 'margin', '0'); diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.html b/src/Squidex/app/framework/angular/modals/tooltip.component.html deleted file mode 100644 index d0a0beb8b..000000000 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.scss b/src/Squidex/app/framework/angular/modals/tooltip.component.scss deleted file mode 100644 index dd1f4eca9..000000000 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -.tooltip-container { - @include border-radius; - background: $color-tooltip; - border: 0; - font-size: .9rem; - font-weight: normal; - white-space: nowrap; - color: $color-dark-foreground; - padding: .5rem; -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.ts b/src/Squidex/app/framework/angular/modals/tooltip.component.ts deleted file mode 100644 index 5466ceb23..000000000 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, Renderer2 } from '@angular/core'; - -import { - fadeAnimation, - ModalModel, - ResourceOwner -} from '@app/framework/internal'; - -@Component({ - selector: 'sqx-tooltip', - styleUrls: ['./tooltip.component.scss'], - templateUrl: './tooltip.component.html', - animations: [ - fadeAnimation - ], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class TooltipComponent extends ResourceOwner implements OnInit { - @Input() - public target: any; - - @Input() - public position = 'topLeft'; - - public modal = new ModalModel(); - - constructor( - private readonly changeDetector: ChangeDetectorRef, - private readonly renderer: Renderer2 - ) { - super(); - } - - public ngOnInit() { - if (this.target) { - this.own( - this.renderer.listen(this.target, 'mouseenter', () => { - this.modal.show(); - - this.changeDetector.detectChanges(); - })); - - this.own( - this.renderer.listen(this.target, 'mouseleave', () => { - this.modal.hide(); - })); - } - } -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/tooltip.directive.ts b/src/Squidex/app/framework/angular/modals/tooltip.directive.ts new file mode 100644 index 000000000..8370d7d5e --- /dev/null +++ b/src/Squidex/app/framework/angular/modals/tooltip.directive.ts @@ -0,0 +1,62 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +// tslint:disable:directive-selector + +import { Directive, ElementRef, Input, OnInit, Renderer2 } from '@angular/core'; + +import { DialogService, ResourceOwner } from '@app/framework/internal'; +import { Tooltip } from '@app/shared'; + +@Directive({ + selector: '[title]' +}) +export class TooltipDirective extends ResourceOwner implements OnInit { + private titleText: string; + + @Input() + public titlePosition = 'top-right'; + + @Input() + public set title(value: string) { + this.titleText = value; + + this.unsetAttribute(); + } + + constructor( + private readonly dialogs: DialogService, + private readonly element: ElementRef, + private readonly renderer: Renderer2 + ) { + super(); + } + + public ngOnInit() { + const target = this.element.nativeElement; + + this.own( + this.renderer.listen(target, 'mouseenter', () => { + if (this.titleText) { + this.dialogs.tooltip(new Tooltip(target, this.titleText, this.titlePosition)); + } + })); + + this.own( + this.renderer.listen(this.element.nativeElement, 'mouseleave', () => { + this.dialogs.tooltip(new Tooltip(target, null, this.titlePosition)); + })); + } + + private unsetAttribute() { + try { + this.renderer.setAttribute(this.element.nativeElement, 'title', ''); + } catch { + return; + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 443df2412..5b6604ccf 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -35,7 +35,7 @@ export * from './angular/modals/modal-dialog.component'; export * from './angular/modals/modal-target.directive'; export * from './angular/modals/modal-view.directive'; export * from './angular/modals/onboarding-tooltip.component'; -export * from './angular/modals/tooltip.component'; +export * from './angular/modals/tooltip.directive'; export * from './angular/modals/root-view.component'; export * from './angular/pipes/colors.pipes'; diff --git a/src/Squidex/app/framework/internal.ts b/src/Squidex/app/framework/internal.ts index 0f1556ebe..16521e1bc 100644 --- a/src/Squidex/app/framework/internal.ts +++ b/src/Squidex/app/framework/internal.ts @@ -27,6 +27,7 @@ export * from './utils/interpolator'; export * from './utils/immutable-array'; export * from './utils/lazy'; export * from './utils/math-helper'; +export * from './utils/modal-positioner'; export * from './utils/modal-view'; export * from './utils/pager'; export * from './utils/permission'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 0e65813a3..9f01654b1 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -80,7 +80,7 @@ import { TitleComponent, TitleService, ToggleComponent, - TooltipComponent, + TooltipDirective, TransformInputDirective, UserReportComponent } from './declarations'; @@ -148,7 +148,7 @@ import { TemplateWrapperDirective, TitleComponent, ToggleComponent, - TooltipComponent, + TooltipDirective, TransformInputDirective, UserReportComponent ], @@ -212,7 +212,7 @@ import { TemplateWrapperDirective, TitleComponent, ToggleComponent, - TooltipComponent, + TooltipDirective, TransformInputDirective, UserReportComponent ] diff --git a/src/Squidex/app/framework/services/dialog.service.spec.ts b/src/Squidex/app/framework/services/dialog.service.spec.ts index 6826ab33a..c61f661df 100644 --- a/src/Squidex/app/framework/services/dialog.service.spec.ts +++ b/src/Squidex/app/framework/services/dialog.service.spec.ts @@ -9,7 +9,8 @@ import { DialogRequest, DialogService, DialogServiceFactory, - Notification + Notification, + Tooltip } from './dialog.service'; describe('DialogService', () => { @@ -66,8 +67,25 @@ describe('DialogService', () => { expect(isNext).toBeTruthy(); }); + it('should publish tooltip', () => { + const dialogService = new DialogService(); + + const tooltip = new Tooltip('target', 'text', 'topLeft'); + + let publishedTooltip: Tooltip; + + dialogService.tooltips.subscribe(result => { + publishedTooltip = result; + }); + + dialogService.tooltip(tooltip); + + expect(publishedTooltip!).toBe(tooltip); + }); + it('should publish notification', () => { const dialogService = new DialogService(); + const notification = Notification.error('Message'); let publishedNotification: Notification; diff --git a/src/Squidex/app/framework/services/dialog.service.ts b/src/Squidex/app/framework/services/dialog.service.ts index 5fe0a4b2f..087f09626 100644 --- a/src/Squidex/app/framework/services/dialog.service.ts +++ b/src/Squidex/app/framework/services/dialog.service.ts @@ -34,6 +34,15 @@ export class DialogRequest { } } +export class Tooltip { + constructor( + public readonly target: any, + public readonly text: string | null, + public readonly position: string + ) { + } +} + export class Notification { constructor( public readonly message: string, @@ -55,11 +64,16 @@ export class Notification { export class DialogService { private readonly requestStream$ = new Subject(); private readonly notificationsStream$ = new Subject(); + private readonly tooltipStream$ = new Subject(); public get dialogs(): Observable { return this.requestStream$; } + public get tooltips(): Observable { + return this.tooltipStream$; + } + public get notifications(): Observable { return this.notificationsStream$; } @@ -82,6 +96,10 @@ export class DialogService { this.notificationsStream$.next(notification); } + public tooltip(tooltip: Tooltip) { + this.tooltipStream$.next(tooltip); + } + public confirm(title: string, text: string): Observable { const request = new DialogRequest(title, text); diff --git a/src/Squidex/app/framework/utils/modal-positioner.spec.ts b/src/Squidex/app/framework/utils/modal-positioner.spec.ts new file mode 100644 index 000000000..68721fd6f --- /dev/null +++ b/src/Squidex/app/framework/utils/modal-positioner.spec.ts @@ -0,0 +1,85 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { positionModal } from './modal-positioner'; + +describe('position', () => { + function buildRect(x: number, y: number, w: number, h: number): ClientRect { + return { + top: y, + left: x, + right: x + w, + width: w, + height: h, + bottom: y + h + }; + } + + const targetRect = buildRect(200, 200, 100, 100); + + const tests = [ + { position: 'top', x: 235, y: 160 }, + { position: 'top-left', x: 200, y: 160 }, + { position: 'top-right', x: 270, y: 160 }, + { position: 'bottom', x: 235, y: 310 }, + { position: 'bottom-left', x: 200, y: 310 }, + { position: 'bottom-right', x: 270, y: 310 }, + { position: 'left', x: 160, y: 235 }, + { position: 'left-top', x: 160, y: 200 }, + { position: 'left-bottom', x: 160, y: 270 }, + { position: 'right', x: 310, y: 235 }, + { position: 'right-top', x: 310, y: 200 }, + { position: 'right-bottom', x: 310, y: 270 } + ]; + + for (let test of tests) { + const modalRect = buildRect(0, 0, 30, 30); + + it(`should calculate modal position for ${test.position}`, () => { + const result = positionModal(targetRect, modalRect, test.position, 10, false, 0, 0); + + expect(result.x).toBe(test.x); + expect(result.y).toBe(test.y); + }); + } + + it('should calculate modal position for vertical top fix', () => { + const modalRect = buildRect(0, 0, 30, 200); + + const result = positionModal(targetRect, modalRect, 'top-left', 10, true, 600, 600); + + expect(result.x).toBe(200); + expect(result.y).toBe(310); + }); + + it('should calculate modal position for vertical bottom fix', () => { + const modalRect = buildRect(0, 0, 30, 70); + + const result = positionModal(targetRect, modalRect, 'bottom-left', 10, true, 350, 350); + + expect(result.x).toBe(200); + expect(result.y).toBe(120); + }); + + it('should calculate modal position for horizontal left fix', () => { + const modalRect = buildRect(0, 0, 200, 30); + + const result = positionModal(targetRect, modalRect, 'left-top', 10, true, 600, 600); + + expect(result.x).toBe(310); + expect(result.y).toBe(200); + }); + + it('should calculate modal position for horizontal right fix', () => { + const modalRect = buildRect(0, 0, 70, 30); + + const result = positionModal(targetRect, modalRect, 'right-top', 10, true, 350, 350); + + expect(result.x).toBe(120); + expect(result.y).toBe(200); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/modal-positioner.ts b/src/Squidex/app/framework/utils/modal-positioner.ts new file mode 100644 index 000000000..bca4ae7c4 --- /dev/null +++ b/src/Squidex/app/framework/utils/modal-positioner.ts @@ -0,0 +1,116 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +const POSITION_TOP_CENTER = 'top'; +const POSITION_TOP_LEFT = 'top-left'; +const POSITION_TOP_RIGHT = 'top-right'; +const POSITION_BOTTOM_CENTER = 'bottom'; +const POSITION_BOTTOM_LEFT = 'bottom-left'; +const POSITION_BOTTOM_RIGHT = 'bottom-right'; +const POSITION_LEFT_CENTER = 'left'; +const POSITION_LEFT_TOP = 'left-top'; +const POSITION_LEFT_BOTTOM = 'left-bottom'; +const POSITION_RIGHT_CENTER = 'right'; +const POSITION_RIGHT_TOP = 'right-top'; +const POSITION_RIGHT_BOTTOM = 'right-bottom'; + +export function positionModal(targetRect: ClientRect, modalRect: ClientRect, relativePosition: string, offset: number, fix: boolean, viewportHeight: number, viewportWidth: number): { x: number, y: number } { + let y = 0; + let x = 0; + + switch (relativePosition) { + case POSITION_LEFT_TOP: + case POSITION_RIGHT_TOP: { + y = targetRect.top; + break; + } + case POSITION_LEFT_BOTTOM: + case POSITION_RIGHT_BOTTOM: { + y = targetRect.bottom - modalRect.height; + break; + } + case POSITION_BOTTOM_CENTER: + case POSITION_BOTTOM_LEFT: + case POSITION_BOTTOM_RIGHT: { + y = targetRect.bottom + offset; + + if (fix && y + modalRect.height > viewportHeight) { + const candidate = targetRect.top - modalRect.height - offset; + + if (candidate > 0) { + y = candidate; + } + } + break; + } + case POSITION_TOP_CENTER: + case POSITION_TOP_LEFT: + case POSITION_TOP_RIGHT: { + y = targetRect.top - modalRect.height - offset; + + if (fix && y < 0) { + const candidate = targetRect.bottom + offset; + + if (candidate + modalRect.height < viewportHeight) { + y = candidate; + } + } + break; + } + case POSITION_LEFT_CENTER: + case POSITION_RIGHT_CENTER: + y = targetRect.top + targetRect.height * 0.5 - modalRect.height * 0.5; + break; + } + + switch (relativePosition) { + case POSITION_TOP_LEFT: + case POSITION_BOTTOM_LEFT: { + x = targetRect.left; + break; + } + case POSITION_TOP_RIGHT: + case POSITION_BOTTOM_RIGHT: { + x = targetRect.right - modalRect.width; + break; + } + case POSITION_RIGHT_CENTER: + case POSITION_RIGHT_TOP: + case POSITION_RIGHT_BOTTOM: { + x = targetRect.right + offset; + + if (fix && x + modalRect.width > viewportWidth) { + const candidate = targetRect.left - modalRect.width - offset; + + if (candidate > 0) { + x = candidate; + } + } + break; + } + case POSITION_LEFT_CENTER: + case POSITION_LEFT_TOP: + case POSITION_LEFT_BOTTOM: { + x = targetRect.left - modalRect.width - offset; + + if (fix && x < 0) { + const candidate = targetRect.right + offset; + + if (candidate + modalRect.width < viewportWidth) { + x = candidate; + } + } + break; + } + case POSITION_TOP_CENTER: + case POSITION_BOTTOM_CENTER: + x = targetRect.left + targetRect.width * 0.5 - modalRect.width * 0.5; + break; + } + + return { x, y }; +} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 237b69fa5..f8cd3a23a 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -113,7 +113,7 @@ {{asset.pixelWidth}}x{{asset.pixelHeight}}px, {{asset.fileSize | sqxFileSize}} - + diff --git a/src/Squidex/app/shared/components/comment.component.html b/src/Squidex/app/shared/components/comment.component.html index 0c212b55b..78fa943ee 100644 --- a/src/Squidex/app/shared/components/comment.component.html +++ b/src/Squidex/app/shared/components/comment.component.html @@ -1,11 +1,11 @@
- +
-
{{comment.user | sqxUserNameRef:null}}
+
{{comment.user | sqxUserNameRef}}