Text layout update

commit_hash:7ef2ea17371f080d8de2cfb0fbe861f4b0dbfa4b
This commit is contained in:
4eb0da
2026-03-05 19:45:09 +03:00
parent f20e6cb443
commit 78dea19cf1
8 changed files with 135 additions and 21 deletions
+2
View File
@@ -44,6 +44,7 @@
"props.align_bottom": "Снизу",
"props.align_start": "В начале",
"props.align_end": "В конце",
"props.align_baseline": "По базовой линии",
"props.alpha": "Непрозрачность",
"props.rotation": "Поворот",
"props.insets": "Отступы",
@@ -414,6 +415,7 @@
"props.align_bottom": "Bottom",
"props.align_start": "Start",
"props.align_end": "End",
"props.align_baseline": "Baseline",
"props.alpha": "Alpha",
"props.rotation": "Rotation",
"props.insets": "Margins and paddings",
+4
View File
@@ -100,10 +100,13 @@ export type GetTranslationSuggest = (query: string, locale: string) => Promise<T
export type GetTranslationKey = (key: string) => Promise<Record<string, string> | undefined>;
export type FileDialogAlign = 'top' | 'center' | 'bottom' | 'baseline';
export interface FileDialogValue {
url: string;
width?: number;
height?: number;
align?: FileDialogAlign;
}
export type FileDialogCallback = (opts: FileDialogValue) => void;
@@ -114,6 +117,7 @@ export interface FileDialogShowProps {
subtype: 'image' | 'gif' | 'lottie' | 'video' | 'image_preview';
direction?: 'left' | 'right';
hasSize?: boolean;
hasAlign?: boolean;
hasDelete?: boolean;
target: HTMLElement;
disabled?: boolean;
@@ -1,3 +1,7 @@
<script lang="ts" context="module">
const ALLOWED_VERTICAL_ALIGN = new Set(['top', 'middle', 'bottom', 'baseline']);
</script>
<script lang="ts">
import { createEventDispatcher, getContext, onMount, tick } from 'svelte';
import { fade, fly } from 'svelte/transition';
@@ -7,7 +11,7 @@
import { htmlFilter } from '../utils/htmlFilter';
import { calcSelectionOffset, getInnerText, setSelectionOffset } from '../utils/contenteditable';
import { isEqual } from '../utils/isEqual';
import { rangeStyle, type FontWeight, type TextImage, type TextRange, type TextStyles } from '../utils/range';
import { rangeStyle, type FontWeight, type TextImage, type TextRange, type TextStyles, type TextVerticalAlignment } from '../utils/range';
import { LANGUAGE_CTX, type LanguageContext } from '../ctx/languageContext';
import { APP_CTX, type AppContext } from '../ctx/appContext';
import { copyValue } from '../utils/copyValue';
@@ -76,6 +80,7 @@
height: string;
wrapperStyle: Style;
svgFilterId?: string;
verticalAlign: TextVerticalAlignment | undefined;
};
}
@@ -227,6 +232,7 @@
let weight = '';
let underline = false;
let strike = false;
let historyIndex = 0;
let historyStack: State[] = [{
text,
@@ -427,6 +433,7 @@
height: imageHeight,
wrapperStyle,
// svgFilterId
verticalAlign: item.image.alignment_vertical
}
});
}
@@ -461,17 +468,38 @@
html = `<span ${item.actions?.length ? `class="inplace-editor__link" data-actions="${htmlFilter(JSON.stringify(item.actions))}"` : ''} data-range-index="${htmlFilter(String(item.index))}" data-range="${htmlFilter(JSON.stringify(item.textStyles))}" style="${htmlFilter(calcTextStyle(item.textStyles) || '')}">${htmlFilter(item.text)}</span>`;
}
} else {
html = `<span data-image="${htmlFilter(JSON.stringify(images[item.index]))}" data-image-index="${htmlFilter(String(item.index))}" class="inplace-editor__image-wrapper" style="${htmlFilter(makeStyle(item.image.wrapperStyle) || '')}">` +
`<img class="inplace-editor__image" src="${htmlFilter(item.image.url)}" loading="lazy" decoding="async" ` +
`aria-hidden="true" alt="" style="${htmlFilter(makeStyle({
width: item.image.width,
height: item.image.height,
// Normalizes line-height for the containing text line
'margin-top': customLineHeight ? `-${item.image.height}` : undefined,
'margin-bottom': customLineHeight ? `-${item.image.height}` : undefined,
filter: item.image.svgFilterId ? `url(#${item.image.svgFilterId})` : undefined
}) || '')}` +
'></span>';
let align = 'default';
if (item.image.verticalAlign && ALLOWED_VERTICAL_ALIGN.has(item.image.verticalAlign)) {
align = item.image.verticalAlign;
}
html = '<span ' +
`data-image="${htmlFilter(JSON.stringify(images[item.index]))}" ` +
`data-image-index="${htmlFilter(String(item.index))}" ` +
'class="inplace-editor__image-wrapper" ' +
`style="${htmlFilter(makeStyle(item.image.wrapperStyle) || '')}"` +
'>' +
'<span ' +
`class="inplace-editor__image-inner inplace-editor__image-inner_align_${align}${customLineHeight ? ' inplace-editor__image-inner_crop' : ''}"` +
`style="${htmlFilter(makeStyle({
width: item.image.width,
height: (customLineHeight && item.image.verticalAlign !== 'baseline') ? lineHeight + 'em' : undefined
}) || '')}"` +
'>' +
'<img ' +
'class="inplace-editor__image" ' +
`src="${htmlFilter(item.image.url)}" ` +
'loading="lazy" ' +
'decoding="async" ' +
'aria-hidden="true" ' +
'alt="" ' +
`style="${htmlFilter(makeStyle({
height: item.image.height,
filter: item.image.svgFilterId ? `url(#${item.image.svgFilterId})` : undefined
}) || '')}"` +
'>' +
'</span>' +
'</span>';
}
return acc + html;
@@ -495,7 +523,7 @@
}
// rotated height - non-rotated height
additionalHeight = elem.getBoundingClientRect().height - elem.offsetHeight;
additionalHeight = elem.getBoundingClientRect().height / scale - elem.offsetHeight;
tick().then(() => {
dispatch('resize');
});
@@ -506,7 +534,7 @@
}
$: style = {
'font-size': pxToEm(fontSize * scale),
'font-size': pxToEm(fontSize),
'line-height': lineHeight,
// 'max-height': maxHeight,
// '-webkit-line-clamp': lineClamp,
@@ -572,12 +600,14 @@
direction: 'right',
subtype: 'image',
hasSize: true,
hasAlign: true,
hasDelete: true,
disabled: disabled || textDisabled,
value: {
url: image.url,
width: image.width?.value,
height: image.height?.value
height: image.height?.value,
align: image.alignment_vertical || 'center'
},
callback(val) {
images = [...images];
@@ -593,7 +623,8 @@
height: {
type: 'fixed',
value: val.height || 24
}
},
alignment_vertical: val.align
} as const;
images[index] = img;
} else {
@@ -1012,7 +1043,7 @@
>
<div
class="inplace-editor__toolbar"
style:transform="translateY({-additionalHeight / 2}px)"
style:transform="scale({1 / scale}) translateY({-additionalHeight / 2}px)"
transition:fade={{ duration: 150 }}
>
<div class="inplace-editor__toolbar-inner" bind:this={toolbarElem}>
@@ -1127,6 +1158,7 @@
{#if showTextDisabledAlert}
<div
class="inplace-editor__disabled-alert-wrapper"
style:transform="scale({1 / scale})"
transition:fly={{ y: 20, duration: 150 }}
>
<div class="inplace-editor__disabled-alert">
@@ -1162,6 +1194,7 @@
width: 0;
display: flex;
justify-content: center;
transform-origin: 50% 100%;
}
.inplace-editor__toolbar-inner {
@@ -1281,11 +1314,57 @@
outline-color: #00a2ff;
}
:global(.inplace-editor__image) {
display: inline-block;
:global(.inplace-editor__image-inner) {
position: relative;
display: inline-flex;
align-items: center;
vertical-align: middle;
}
:global(.inplace-editor__image-inner_crop) {
height: 0;
}
:global(.inplace-editor__image-inner_align_top) {
vertical-align: top;
}
:global(.inplace-editor__image-inner_align_center) {
vertical-align: middle;
}
:global(.inplace-editor__image-inner_align_bottom) {
vertical-align: bottom;
}
:global(.inplace-editor__image-inner_align_baseline) {
vertical-align: baseline;
}
:global(.inplace-editor__image) {
display: block;
width: 100%;
}
:global(.inplace-editor__image-inner_crop .inplace-editor__image) {
position: absolute;
}
:global(.inplace-editor__image-inner_align_top .inplace-editor__image) {
top: 0;
left: 0;
}
:global(.inplace-editor__image-inner_align_bottom .inplace-editor__image) {
bottom: 0;
left: 0;
}
:global(.inplace-editor__image-inner_align_baseline .inplace-editor__image) {
bottom: 0;
left: 0;
}
:global(.inplace-editor__image_hidden) {
display: none;
}
@@ -1383,6 +1462,7 @@
z-index: 1;
top: calc(100% + 14px);
left: 50%;
transform-origin: 50% 0;
}
.inplace-editor__disabled-alert {
@@ -64,6 +64,7 @@
style:width={currentProps.style.width}
style:max-width={currentProps.style.maxWidth}
style:font-size={currentProps.style.fontSize}
style:transform="scale({currentProps.scale})"
bind:this={dialog}
on:close={onClose}
on:click={onDialogClick}
@@ -91,6 +92,7 @@
border: none;
background: none;
overflow: visible;
transform-origin: 0 0;
}
.inplace-editor-dialog::backdrop {
@@ -2956,7 +2956,7 @@
const style = {
left: `${startBbox.left + paddingLeft * scale}px`,
top: `${startBbox.top + paddingTop * scale}px`,
width: `${Math.ceil((offsetWidth - paddingLeft - paddingRight) * scale)}px`,
width: `${Math.ceil(offsetWidth - paddingLeft - paddingRight)}px`,
maxWidth: `${parentBbox.right - startBbox.left - (paddingLeft + paddingRight) * scale}px`,
fontSize: computedStyle.fontSize
};
@@ -17,7 +17,7 @@
}
export let items: Item[];
export let value: string;
export let value: string | undefined;
export let theme: 'normal' | 'canvas' | 'transparent' | 'preview';
export let size: 'small' | 'medium' = 'small';
export let iconTheme: 'normal' | 'transparent' = 'normal';
@@ -123,6 +123,7 @@
value = props.value;
title = props.title;
hasSize = props.hasSize;
hasAlign = props.hasAlign;
hasDelete = props.hasDelete;
subtype = props.subtype;
commonSubtype = subtype === 'image_preview' ? 'image' : subtype;
@@ -153,6 +154,7 @@
let target: HTMLElement;
let isShown = false;
let hasSize: boolean | undefined = false;
let hasAlign: boolean | undefined = false;
let hasDelete: boolean | undefined = false;
let value: FileDialogValue = {
url: '',
@@ -776,6 +778,23 @@
</label>
</div>
{/if}
{#if hasAlign}
<label class="file2-dialog__align">
<div class="file2-dialog__label">{$l10nString('props.alignment')}</div>
<Select
theme="normal"
size="medium"
{disabled}
bind:value={value.align}
items={[
{ value: 'top', text: $l10nString('props.align_top') },
{ value: 'center', text: $l10nString('props.align_center') },
{ value: 'bottom', text: $l10nString('props.align_bottom') },
{ value: 'baseline', text: $l10nString('props.align_baseline') },
]}
/>
</label>
{/if}
</ContextDialog>
{/if}
@@ -1036,6 +1055,10 @@
line-height: 20px;
}
.file2-dialog__align {
margin: 0 20px 20px;
}
@keyframes file2-dialog__rotate {
from {
transform: rotate(0deg);
+3
View File
@@ -35,6 +35,8 @@ export interface FixedSize {
value: number;
}
export type TextVerticalAlignment = 'top' | 'center' | 'bottom' | 'baseline';
export interface TextImage {
start: number;
url: string;
@@ -42,6 +44,7 @@ export interface TextImage {
height?: FixedSize;
tint_color?: string;
// tint_mode?: TintMode;
alignment_vertical?: TextVerticalAlignment;
}
export function rangeStyle(range: TextRange): Omit<TextRange, 'start' | 'end'> {