mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
70 changed files with 1178 additions and 514 deletions
@ -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<Translation> 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<string, string> |
||||
|
{ |
||||
|
["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<Response>(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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<Translation> Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default); |
||||
|
} |
||||
|
} |
||||
@ -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<Translation> Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default) |
||||
|
{ |
||||
|
var result = new Translation(TranslationResult.NotImplemented); |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The text to translate.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Text { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The target language.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public Language TargetLanguage { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The optional source language.
|
||||
|
/// </summary>
|
||||
|
public Language SourceLanguage { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The result of the translation.
|
||||
|
/// </summary>
|
||||
|
public TranslationResult Result { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The translated text.
|
||||
|
/// </summary>
|
||||
|
public string Text { get; set; } |
||||
|
|
||||
|
public static TranslationDto FromTranslation(Translation translation) |
||||
|
{ |
||||
|
return SimpleMapper.Map(translation, new TranslationDto()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Manage translations.
|
||||
|
/// </summary>
|
||||
|
[ApiExplorerSettings(GroupName = nameof(Translations))] |
||||
|
public sealed class TranslationsController : ApiController |
||||
|
{ |
||||
|
private readonly ITranslator translator; |
||||
|
|
||||
|
public TranslationsController(ICommandBus commandBus, ITranslator translator) |
||||
|
: base(commandBus) |
||||
|
{ |
||||
|
this.translator = translator; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Translate a text.
|
||||
|
/// </summary>
|
||||
|
/// <param name="app">The name of the app.</param>
|
||||
|
/// <param name="request">The translation request.</param>
|
||||
|
/// <returns>
|
||||
|
/// 200 => Text translated.
|
||||
|
/// </returns>
|
||||
|
[HttpPost] |
||||
|
[Route("apps/{app}/translations/")] |
||||
|
[ProducesResponseType(typeof(TranslationDto), 200)] |
||||
|
[ApiPermission(Permissions.AppCommon)] |
||||
|
[ApiCosts(0)] |
||||
|
public async Task<IActionResult> 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,17 +1,13 @@ |
|||||
<ng-container *ngIf="!scheduledTo"> |
<ng-container *ngIf="!scheduledTo"> |
||||
<span class="content-status content-status-{{displayStatus | lowercase}} mr-1" [class.middle]="alignMiddle" #statusIcon> |
<span class="content-status content-status-{{displayStatus | lowercase}} mr-1" [class.middle]="alignMiddle" title="{{displayStatus}}" titlePosition="top"> |
||||
<i class="icon-circle"></i> |
<i class="icon-circle"></i> |
||||
</span> |
</span> |
||||
|
|
||||
<sqx-tooltip [target]="statusIcon">{{displayStatus}}</sqx-tooltip> |
|
||||
</ng-container> |
</ng-container> |
||||
|
|
||||
<ng-container *ngIf="scheduledTo"> |
<ng-container *ngIf="scheduledTo"> |
||||
<span class="content-status content-status-{{scheduledTo | lowercase}} mr-1" [class.middle]="alignMiddle" #statusIcon> |
<span class="content-status content-status-{{scheduledTo | lowercase}} mr-1" [class.middle]="alignMiddle" title="{{displayStatus}}" titlePosition="top"> |
||||
<i class="icon-clock"></i> |
<i class="icon-clock"></i> |
||||
</span> |
</span> |
||||
|
|
||||
<sqx-tooltip position="topRight" [target]="statusIcon">Will be set to '{{scheduledTo}}' at {{scheduledAt | sqxFullDateTime}}</sqx-tooltip> |
|
||||
</ng-container> |
</ng-container> |
||||
|
|
||||
<span class="content-status-label" *ngIf="showLabel">{{displayStatus}}</span> |
<span class="content-status-label" *ngIf="showLabel">{{displayStatus}}</span> |
||||
@ -1,3 +0,0 @@ |
|||||
<div class="tooltip-container" *sqxModalView="modal;onRoot:true;closeAuto:false" [sqxModalTarget]="target" [position]="position"> |
|
||||
<ng-content></ng-content> |
|
||||
</div> |
|
||||
@ -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; |
|
||||
} |
|
||||
@ -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(); |
|
||||
})); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
}); |
||||
|
}); |
||||
@ -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 }; |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
||||
|
import { inject, TestBed } from '@angular/core/testing'; |
||||
|
|
||||
|
import { |
||||
|
ApiUrlConfig, |
||||
|
TranslateDto, |
||||
|
TranslationDto, |
||||
|
TranslationsService |
||||
|
} from './../'; |
||||
|
|
||||
|
describe('TranslationsService', () => { |
||||
|
beforeEach(() => { |
||||
|
TestBed.configureTestingModule({ |
||||
|
imports: [ |
||||
|
HttpClientTestingModule |
||||
|
], |
||||
|
providers: [ |
||||
|
TranslationsService, |
||||
|
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') } |
||||
|
] |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
||||
|
httpMock.verify(); |
||||
|
})); |
||||
|
|
||||
|
it('should make post request to translate text', |
||||
|
inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => { |
||||
|
|
||||
|
let translation: TranslationDto; |
||||
|
|
||||
|
const request = new TranslateDto('Hello', 'en', 'de'); |
||||
|
|
||||
|
translationsService.translate('my-app', request).subscribe(result => { |
||||
|
translation = result; |
||||
|
}); |
||||
|
|
||||
|
const req = httpMock.expectOne('http://service/p/api/apps/my-app/translations'); |
||||
|
|
||||
|
expect(req.request.method).toEqual('POST'); |
||||
|
expect(req.request.headers.get('If-Match')).toBeNull(); |
||||
|
|
||||
|
req.flush({ |
||||
|
text: 'Hallo', result: 'Translated' |
||||
|
}); |
||||
|
|
||||
|
expect(translation!).toEqual(new TranslationDto('Translated', 'Hallo')); |
||||
|
})); |
||||
|
}); |
||||
@ -0,0 +1,49 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
import { ApiUrlConfig, pretifyError } from '@app/framework'; |
||||
|
|
||||
|
export class TranslationDto { |
||||
|
constructor( |
||||
|
public readonly result: string, |
||||
|
public readonly text: string |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class TranslateDto { |
||||
|
constructor( |
||||
|
public readonly text: string, |
||||
|
public readonly sourceLanguage: string, |
||||
|
public readonly targetLanguage: string |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class TranslationsService { |
||||
|
constructor( |
||||
|
private readonly http: HttpClient, |
||||
|
private readonly apiUrl: ApiUrlConfig |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public translate(appName: string, request: TranslateDto): Observable<TranslationDto> { |
||||
|
const url = this.apiUrl.buildUrl(`api/apps/${appName}/translations`); |
||||
|
|
||||
|
return this.http.post<any>(url, request).pipe( |
||||
|
map(response => { |
||||
|
return new TranslationDto(response.result, response.text); |
||||
|
}), |
||||
|
pretifyError('Failed to translate text. Please reload.')); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. |
||||
|
|
||||
|
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts |
||||
|
|
||||
|
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. |
||||
|
|
||||
|
You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection. |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue