Skip to content

Commit

Permalink
fix(edgeless): canvas text inconsistency between view and editing sta…
Browse files Browse the repository at this point in the history
…te (#5851)
  • Loading branch information
AyushAgrawal-A2 authored Dec 27, 2023
1 parent 3fb9bcb commit bc7f0ab
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { styleMap } from 'lit/directives/style-map.js';

import type { RichText } from '../../../../_common/components/rich-text/rich-text.js';
import { isCssVariable } from '../../../../_common/theme/css-variables.js';
import { wrapFontFamily } from '../../../../surface-block/elements/text/utils.js';
import {
getLineHeight,
wrapFontFamily,
} from '../../../../surface-block/elements/text/utils.js';
import {
Bound,
type TextElement,
Expand All @@ -28,45 +31,35 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {

static override styles = css`
.edgeless-text-editor {
box-sizing: content-box;
position: absolute;
left: 0;
top: 0;
z-index: 10;
margin-right: -100%;
transform-origin: left top;
border: ${EdgelessTextEditor.BORDER_WIDTH}px solid
var(--affine-primary-color, #1e96eb);
box-shadow: 0px 0px 0px 2px rgba(30, 150, 235, 0.3);
border-radius: 4px;
box-shadow: 0px 0px 0px 2px rgba(30, 150, 235, 0.3);
padding: ${EdgelessTextEditor.VERTICAL_PADDING}px
${EdgelessTextEditor.HORIZONTAL_PADDING}px;
line-height: initial;
overflow: visible;
box-sizing: content-box;
left: 0;
top: 0;
}
.edgeless-text-editor-placeholder {
font-size: 12px;
pointer-events: none;
color: var(--affine-text-disable-color);
white-space: nowrap;
}
.edgeless-text-editor .inline-editor-container {
white-space: nowrap;
.edgeless-text-editor .inline-editor {
white-space: pre-wrap !important;
outline: none;
width: fit-content;
}
.edgeless-text-editor .inline-editor-container span {
white-space: pre !important;
word-break: keep-all !important;
overflow-wrap: normal !important;
.edgeless-text-editor .inline-editor span {
word-break: normal !important;
overflow-wrap: anywhere !important;
}
/* We cannot add styles directly from the top, as this would cause a shift in the inline elements inside. */
/* https://github.com/toeverything/blocksuite/issues/5723 */
.edgeless-text-editor rich-text v-text {
text-align: var(--text-align);
.edgeless-text-editor-placeholder {
pointer-events: none;
color: var(--affine-text-disable-color);
white-space: nowrap;
}
`;

Expand All @@ -83,6 +76,7 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
assertExists(this.richText.inlineEditor);
return this.richText.inlineEditor;
}

get inlineEditorContainer() {
return this.inlineEditor.rootElement;
}
Expand Down Expand Up @@ -167,7 +161,7 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
return { x: newCenterX - w1 / 2, y: newCenterY - h1 / 2 };
}

private _updateRect() {
private _updateRect = () => {
const edgeless = this.edgeless;
const element = this.element;

Expand Down Expand Up @@ -238,80 +232,19 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
edgeless.surface.updateElement(element.id, {
xywh: bound.serialize(),
});
}

private _renderPlaceholder() {
if (this.element.text.length === 0 && !this._isComposition) {
return html`<span
class="edgeless-text-editor-placeholder"
style=${styleMap({
fontSize: this.element.fontSize + 'px',
})}
>Type from here</span
>`;
}

return nothing;
}

getTransformOrigin(textAlign: TextElement['textAlign']) {
switch (textAlign) {
case 'left':
return 'left top';
case 'center':
return 'center top';
case 'right':
return 'right top';
}
}

getTransformOffset(textAlign: TextElement['textAlign']) {
switch (textAlign) {
case 'left':
return '0%, 0%';
case 'center':
return '-50%, 0%';
case 'right':
return '-100%, 0%';
}
}
};

getVisualPosition(element: TextElement) {
const { x, y, w, h, rotate, textAlign } = element;
switch (textAlign) {
case 'left':
return Vec.rotWith([x, y], [x + w / 2, y + h / 2], toRadian(rotate));
case 'center':
return Vec.rotWith(
[x + w / 2, y],
[x + w / 2, y + h / 2],
toRadian(rotate)
);
case 'right':
return Vec.rotWith(
[x + w, y],
[x + w / 2, y + h / 2],
toRadian(rotate)
);
}
const { x, y, w, h, rotate } = element;
return Vec.rotWith([x, y], [x + w / 2, y + h / 2], toRadian(rotate));
}

getPaddingOffset(textAlign: TextElement['textAlign']) {
getContainerOffset() {
const { VERTICAL_PADDING, HORIZONTAL_PADDING, BORDER_WIDTH } =
EdgelessTextEditor;

switch (textAlign) {
case 'left':
return `-${HORIZONTAL_PADDING + BORDER_WIDTH}px, -${
VERTICAL_PADDING + BORDER_WIDTH
}px`;
case 'center':
return `0, -${VERTICAL_PADDING + BORDER_WIDTH}px`;
case 'right':
return `${HORIZONTAL_PADDING + BORDER_WIDTH}px, -${
VERTICAL_PADDING + BORDER_WIDTH
}px`;
}
return `-${HORIZONTAL_PADDING + BORDER_WIDTH}px, -${
VERTICAL_PADDING + BORDER_WIDTH
}px`;
}

override connectedCallback(): void {
Expand Down Expand Up @@ -342,13 +275,16 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
if (id === element.id) this.requestUpdate();
})
);

this.disposables.add(
edgeless.surface.viewport.slots.viewportUpdated.on(() => {
this.requestUpdate();
})
);

this.disposables.add(dispatcher.add('click', () => true));
this.disposables.add(dispatcher.add('doubleClick', () => true));

this.disposables.add(() => {
element.display = true;

Expand All @@ -361,11 +297,13 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
editing: false,
});
});

this.disposables.addFromEvent(
this.inlineEditorContainer,
'blur',
() => !this._keeping && this.remove()
);

this.disposables.addFromEvent(
this.inlineEditorContainer,
'compositionstart',
Expand Down Expand Up @@ -395,66 +333,67 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
}

override render() {
const { zoom, translateX, translateY } = this.edgeless.surface.viewport;
const { fontFamily, fontSize, textAlign, rotate, fontWeight } =
this.element;
const transformOrigin = this.getTransformOrigin(textAlign);
const offset = this.getTransformOffset(textAlign);
const paddingOffset = this.getPaddingOffset(textAlign);
const {
text,
fontFamily,
fontSize,
fontWeight,
color,
textAlign,
rotate,
hasMaxWidth,
w,
} = this.element;
const lineHeight = getLineHeight(fontFamily, fontSize);
const rect = getSelectedRect([this.element]);
const hasMaxWidth = this.element.hasMaxWidth;
const w = this.element.w;
const [x, y] = this.getVisualPosition(this.element);
const placeholder = this._renderPlaceholder();
const hasPlaceholder = placeholder !== nothing;

const { translateX, translateY, zoom } = this.edgeless.surface.viewport;
const [visualX, visualY] = this.getVisualPosition(this.element);
const containerOffset = this.getContainerOffset();
const transformOperation = [
`translate(${translateX}px, ${translateY}px)`,
`translate(${x * zoom}px, ${y * zoom}px)`,
`translate(${offset})`,
`translate(${visualX * zoom}px, ${visualY * zoom}px)`,
`scale(${zoom})`,
`rotate(${rotate}deg)`,
`translate(${paddingOffset})`,
`translate(${containerOffset})`,
];
const left =
textAlign === 'left'
? EdgelessTextEditor.HORIZONTAL_PADDING + 'px'
: textAlign === 'center'
? '50%'
: `calc(100% - ${EdgelessTextEditor.HORIZONTAL_PADDING}px)`;

this.style.setProperty('--text-align', textAlign);
const isEmpty = !text.length && !this._isComposition;

return html`<div
style=${styleMap({
fontFamily: wrapFontFamily(fontFamily),
transform: transformOperation.join(' '),
minWidth: hasMaxWidth ? `${rect.width}px` : 'none',
maxWidth: hasMaxWidth ? `${w}px` : 'none',
fontFamily: wrapFontFamily(fontFamily),
fontSize: `${fontSize}px`,
fontWeight,
transform: transformOperation.join(' '),
transformOrigin,
color: isCssVariable(this.element.color)
? `var(${this.element.color})`
: this.element.color,
color: isCssVariable(color) ? `var(${color})` : color,
textAlign,
lineHeight: `${lineHeight}px`,
})}
class="edgeless-text-editor"
>
<rich-text
.yText=${this.element.text}
.yText=${text}
.enableFormat=${false}
.enableAutoScrollHorizontally=${false}
.enableAutoScrollVertically=${false}
style=${hasPlaceholder
style=${isEmpty
? styleMap({
position: 'absolute',
minWidth: '2px',
top: EdgelessTextEditor.VERTICAL_PADDING + 'px',
left,
left: 0,
top: 0,
padding: `${EdgelessTextEditor.VERTICAL_PADDING}px
${EdgelessTextEditor.HORIZONTAL_PADDING}px`,
})
: nothing}
></rich-text>
${placeholder}
${isEmpty
? html`<span class="edgeless-text-editor-placeholder">
Type from here
</span>`
: nothing}
</div>`;
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/blocks/src/page-block/font-loader/font-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class FontLoader {
style,
});
document.fonts.add(fontFace);
fontFace.load().catch(console.error);
return fontFace;
})
);
Expand Down
2 changes: 1 addition & 1 deletion packages/blocks/src/surface-block/elements/text/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function getFontString({
const lineHeight = getLineHeight(fontFamily, fontSize);
return `${fontStyle} ${fontWeight} ${fontSize}px/${lineHeight}px ${wrapFontFamily(
fontFamily
)}`.trim();
)}, sans-serif`.trim();
}

export function normalizeText(text: string): string {
Expand Down
11 changes: 8 additions & 3 deletions tests/paragraph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ test('press arrow up in the second line should move caret to the first line', as
title: new page.Text(),
});
const note = page.addBlock('affine:note', {}, pageId);
const delta = Array.from({ length: 120 }, (_, i) => {
const delta = Array.from({ length: 150 }, (_, i) => {
return i % 2 === 0
? { insert: 'i', attributes: { italic: true } }
: { insert: 'b', attributes: { bold: true } };
Expand All @@ -1293,9 +1293,13 @@ test('press arrow up in the second line should move caret to the first line', as

// Focus the empty paragraph
await focusRichText(page, 1);
await page.waitForTimeout(100);
await assertRichTexts(page, ['ib'.repeat(75), '']);
await pressArrowUp(page);
await pressArrowUp(page);
await type(page, '0');
await assertTitle(page, '');
await assertRichTexts(page, ['0' + 'ib'.repeat(75), '']);
await pressArrowUp(page);

// workaround for selection manager
Expand All @@ -1308,6 +1312,7 @@ test('press arrow up in the second line should move caret to the first line', as
// At title
await type(page, '1');
await assertTitle(page, '1');
await assertRichTexts(page, ['0' + 'ib'.repeat(75), '']);

// At the first line of the first paragraph
await pressArrowDown(page);
Expand All @@ -1316,15 +1321,15 @@ test('press arrow up in the second line should move caret to the first line', as
await pressArrowRight(page);
await type(page, '2');

await assertRichTexts(page, ['0' + 'ib'.repeat(60), '2']);
await assertRichTexts(page, ['0' + 'ib'.repeat(75), '2']);

// Go to the start of the second paragraph
await pressArrowLeft(page);
await pressArrowUp(page);
await pressArrowDown(page);
// Should be inserted at the start of the second paragraph
await type(page, '3');
await assertRichTexts(page, ['0' + 'ib'.repeat(60), '32']);
await assertRichTexts(page, ['0' + 'ib'.repeat(75), '32']);
});

test('press arrow down in indent line should not move caret to the start of line', async ({
Expand Down

1 comment on commit bc7f0ab

@vercel
Copy link

@vercel vercel bot commented on bc7f0ab Dec 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

blocksuite – ./packages/playground

try-blocksuite.vercel.app
blocksuite-git-master-toeverything.vercel.app
blocksuite-toeverything.vercel.app

Please sign in to comment.