snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
This commit is contained in:
38
blocksuite/affine/blocks/edgeless-text/package.json
Normal file
38
blocksuite/affine/blocks/edgeless-text/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-block-edgeless-text",
|
||||
"description": "Edgeless text block for BlockSuite.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "yarn run -T tsc"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-gfx-text": "workspace:*",
|
||||
"@blocksuite/affine-inline-preset": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"lit": "^3.2.0"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./view": "./src/view.ts",
|
||||
"./store": "./src/store.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.27.5"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { EdgelessClipboardConfig } from '@blocksuite/affine-block-surface';
|
||||
import { type BlockSnapshot } from '@blocksuite/store';
|
||||
|
||||
export class EdgelessClipboardEdgelessTextConfig extends EdgelessClipboardConfig {
|
||||
static override readonly key = 'affine:edgeless-text';
|
||||
|
||||
override async createBlock(
|
||||
edgelessText: BlockSnapshot
|
||||
): Promise<string | null> {
|
||||
const oldId = edgelessText.id;
|
||||
delete edgelessText.props.index;
|
||||
if (!edgelessText.props.xywh) {
|
||||
console.error(
|
||||
`EdgelessText block(id: ${oldId}) does not have xywh property`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!this.surface) {
|
||||
return null;
|
||||
}
|
||||
const newId = await this.onBlockSnapshotPaste(
|
||||
edgelessText,
|
||||
this.std.store,
|
||||
this.surface.model.id
|
||||
);
|
||||
if (!newId) {
|
||||
console.error(`Failed to paste EdgelessText block(id: ${oldId})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
import { TextUtils } from '@blocksuite/affine-block-surface';
|
||||
import { formatBlockCommand } from '@blocksuite/affine-inline-preset';
|
||||
import {
|
||||
DefaultTheme,
|
||||
EDGELESS_TEXT_BLOCK_MIN_HEIGHT,
|
||||
EDGELESS_TEXT_BLOCK_MIN_WIDTH,
|
||||
type EdgelessTextBlockModel,
|
||||
EdgelessTextBlockSchema,
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
handleNativeRangeAtPoint,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound, clamp } from '@blocksuite/global/gfx';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import {
|
||||
BlockSelection,
|
||||
GfxBlockComponent,
|
||||
TextSelection,
|
||||
} from '@blocksuite/std';
|
||||
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBlockModel> {
|
||||
static override styles = css`
|
||||
.edgeless-text-block-container[data-max-width='false'] .inline-editor span {
|
||||
word-break: keep-all !important;
|
||||
text-wrap: nowrap !important;
|
||||
}
|
||||
|
||||
.edgeless-text-block-container affine-paragraph,
|
||||
affine-list {
|
||||
color: var(--edgeless-text-color);
|
||||
font-family: var(--edgeless-text-font-family);
|
||||
font-style: var(--edgeless-text-font-style);
|
||||
font-weight: var(--edgeless-text-font-weight);
|
||||
text-align: var(--edgeless-text-text-align);
|
||||
}
|
||||
|
||||
.edgeless-text-block-container .locked-content a[href] {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _resizeObserver = new ResizeObserver(() => {
|
||||
if (this.store.readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.model.props.hasMaxWidth) {
|
||||
this._updateW();
|
||||
}
|
||||
|
||||
this._updateH();
|
||||
});
|
||||
|
||||
private _updateH() {
|
||||
const bound = Bound.deserialize(this.model.xywh);
|
||||
const rect = this._textContainer.getBoundingClientRect();
|
||||
bound.h = rect.height / this.gfx.viewport.zoom;
|
||||
|
||||
this.store.updateBlock(this.model, {
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
}
|
||||
|
||||
private _updateW() {
|
||||
const bound = Bound.deserialize(this.model.xywh);
|
||||
const rect = this._textContainer.getBoundingClientRect();
|
||||
bound.w = Math.max(
|
||||
rect.width / this.gfx.viewport.zoom,
|
||||
EDGELESS_TEXT_BLOCK_MIN_WIDTH * this.gfx.viewport.zoom
|
||||
);
|
||||
|
||||
this.store.updateBlock(this.model, {
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
}
|
||||
|
||||
private readonly _style$ = computed(() => {
|
||||
const {
|
||||
color$: { value: color },
|
||||
fontFamily$: { value: fontFamily },
|
||||
fontStyle$: { value: fontStyle },
|
||||
fontWeight$: { value: fontWeight },
|
||||
textAlign$: { value: textAlign },
|
||||
} = this.model.props;
|
||||
return {
|
||||
color,
|
||||
fontFamily,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
textAlign,
|
||||
};
|
||||
});
|
||||
|
||||
checkWidthOverflow(width: number) {
|
||||
let wValid = true;
|
||||
|
||||
const oldWidthStr = this._textContainer.style.width;
|
||||
this._textContainer.style.width = `${width}px`;
|
||||
if (
|
||||
this.childrenContainer.scrollWidth > this.childrenContainer.offsetWidth
|
||||
) {
|
||||
wValid = false;
|
||||
}
|
||||
this._textContainer.style.width = oldWidthStr;
|
||||
|
||||
return wValid;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
this.model.propsUpdated.subscribe(({ key }) => {
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
const command = this.std.command;
|
||||
const blockSelections = this.model.children.map(child =>
|
||||
this.std.selection.create(BlockSelection, {
|
||||
blockId: child.id,
|
||||
})
|
||||
);
|
||||
|
||||
if (key === 'fontStyle') {
|
||||
command.exec(formatBlockCommand, {
|
||||
blockSelections,
|
||||
styles: {
|
||||
italic: null,
|
||||
},
|
||||
});
|
||||
} else if (key === 'color') {
|
||||
command.exec(formatBlockCommand, {
|
||||
blockSelections,
|
||||
styles: {
|
||||
color: null,
|
||||
},
|
||||
});
|
||||
} else if (key === 'fontWeight') {
|
||||
command.exec(formatBlockCommand, {
|
||||
blockSelections,
|
||||
styles: {
|
||||
bold: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override firstUpdated(props: Map<string, unknown>) {
|
||||
super.firstUpdated(props);
|
||||
|
||||
const { disposables, std } = this;
|
||||
const edgelessSelection = this.gfx.selection;
|
||||
|
||||
disposables.add(
|
||||
edgelessSelection.slots.updated.subscribe(() => {
|
||||
if (edgelessSelection.has(this.model.id) && edgelessSelection.editing) {
|
||||
this._editing = true;
|
||||
} else {
|
||||
this._editing = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._resizeObserver.observe(this._textContainer);
|
||||
disposables.add(() => {
|
||||
this._resizeObserver.disconnect();
|
||||
});
|
||||
|
||||
disposables.addFromEvent(this._textContainer, 'click', e => {
|
||||
if (!this._editing) return;
|
||||
|
||||
const containerRect = this._textContainer.getBoundingClientRect();
|
||||
const isTop = e.clientY < containerRect.top + containerRect.height / 2;
|
||||
|
||||
let newParagraphId: string | null = null;
|
||||
if (isTop) {
|
||||
const firstChild = this.model.firstChild();
|
||||
if (
|
||||
!firstChild ||
|
||||
!matchModels(firstChild, [ListBlockModel, ParagraphBlockModel])
|
||||
) {
|
||||
newParagraphId = this.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{},
|
||||
this.model.id,
|
||||
0
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const lastChild = this.model.lastChild();
|
||||
if (
|
||||
!lastChild ||
|
||||
!matchModels(lastChild, [ListBlockModel, ParagraphBlockModel])
|
||||
) {
|
||||
newParagraphId = this.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{},
|
||||
this.model.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (newParagraphId) {
|
||||
std.selection.setGroup('note', [
|
||||
std.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: newParagraphId,
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
disposables.addFromEvent(this._textContainer, 'focusout', () => {
|
||||
if (!this._editing) return;
|
||||
|
||||
this.std.selection.clear();
|
||||
});
|
||||
|
||||
let composingWidth = EDGELESS_TEXT_BLOCK_MIN_WIDTH;
|
||||
disposables.addFromEvent(this, 'compositionupdate', () => {
|
||||
composingWidth = Math.max(
|
||||
this._textContainer.offsetWidth,
|
||||
EDGELESS_TEXT_BLOCK_MIN_HEIGHT
|
||||
);
|
||||
});
|
||||
disposables.addFromEvent(this, 'compositionend', () => {
|
||||
if (this.model.props.hasMaxWidth) {
|
||||
composingWidth = EDGELESS_TEXT_BLOCK_MIN_WIDTH;
|
||||
return;
|
||||
}
|
||||
// when IME finish container will crash to a small width, so
|
||||
// we set a max width to prevent this
|
||||
this._textContainer.style.width = `${composingWidth}px`;
|
||||
this.model.props.hasMaxWidth = true;
|
||||
requestAnimationFrame(() => {
|
||||
this._textContainer.style.width = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override getCSSTransform(): string {
|
||||
const viewport = this.gfx.viewport;
|
||||
const { translateX, translateY, zoom } = viewport;
|
||||
const bound = Bound.deserialize(this.model.xywh);
|
||||
|
||||
const scaledX = bound.x * zoom;
|
||||
const scaledY = bound.y * zoom;
|
||||
const deltaX = scaledX - bound.x;
|
||||
const deltaY = scaledY - bound.y;
|
||||
|
||||
return `translate(${translateX + deltaX}px, ${translateY + deltaY}px) scale(${zoom * this.model.props.scale})`;
|
||||
}
|
||||
|
||||
override getRenderingRect() {
|
||||
const { xywh, scale, rotate, hasMaxWidth } = this.model.props;
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const w = hasMaxWidth ? bound.w / scale : undefined;
|
||||
|
||||
return {
|
||||
x: bound.x,
|
||||
y: bound.y,
|
||||
w,
|
||||
h: bound.h / scale,
|
||||
rotate,
|
||||
zIndex: this.toZIndex(),
|
||||
};
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const { model } = this;
|
||||
const { rotate, hasMaxWidth } = model.props;
|
||||
const editing = this._editing;
|
||||
const containerStyle: StyleInfo = {
|
||||
transform: `rotate(${rotate}deg)`,
|
||||
transformOrigin: 'center',
|
||||
border: `1px solid ${editing ? 'var(--affine—primary—color, #1e96eb)' : 'transparent'}`,
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
boxShadow: editing ? '0px 0px 0px 2px rgba(30, 150, 235, 0.3)' : 'none',
|
||||
fontWeight: '400',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
minWidth: !hasMaxWidth ? '220px' : undefined,
|
||||
};
|
||||
|
||||
this.contentEditable = String(editing && !this.store.readonly$.value);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="edgeless-text-block-container"
|
||||
data-max-width="${hasMaxWidth}"
|
||||
style=${styleMap(containerStyle)}
|
||||
>
|
||||
<div
|
||||
class=${!editing && this.model.isLocked() ? 'locked-content' : ''}
|
||||
style=${styleMap({
|
||||
pointerEvents: editing ? 'auto' : 'none',
|
||||
userSelect: editing ? 'auto' : 'none',
|
||||
})}
|
||||
>
|
||||
${this.renderPageContent()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override renderPageContent() {
|
||||
const { color, fontFamily, fontStyle, fontWeight, textAlign } =
|
||||
this._style$.value;
|
||||
const themeProvider = this.std.get(ThemeProvider);
|
||||
const textColor = themeProvider.generateColorProperty(
|
||||
color,
|
||||
DefaultTheme.textColor,
|
||||
themeProvider.theme$.value
|
||||
);
|
||||
|
||||
const style = styleMap({
|
||||
'--edgeless-text-color': textColor,
|
||||
'--edgeless-text-font-family': TextUtils.wrapFontFamily(fontFamily),
|
||||
'--edgeless-text-font-style': fontStyle,
|
||||
'--edgeless-text-font-weight': fontWeight,
|
||||
'--edgeless-text-text-align': textAlign,
|
||||
'--affine-list-margin': '0',
|
||||
'--affine-paragraph-margin': '0',
|
||||
});
|
||||
|
||||
return html`
|
||||
<div style=${style} class="affine-block-children-container">
|
||||
${this.renderChildren(this.model)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
tryFocusEnd() {
|
||||
const paragraphOrLists = Array.from(
|
||||
this.querySelectorAll<BlockComponent>('affine-paragraph, affine-list')
|
||||
);
|
||||
const last = paragraphOrLists.at(-1);
|
||||
if (last) {
|
||||
this.host.selection.setGroup('note', [
|
||||
this.host.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: last.blockId,
|
||||
index: last.model.text?.length ?? 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _editing = false;
|
||||
|
||||
@query('.edgeless-text-block-container')
|
||||
private accessor _textContainer!: HTMLDivElement;
|
||||
|
||||
@query('.affine-block-children-container')
|
||||
accessor childrenContainer!: HTMLDivElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-edgeless-text': EdgelessTextBlockComponent;
|
||||
}
|
||||
}
|
||||
|
||||
export const EdgelessTextInteraction =
|
||||
GfxViewInteractionExtension<EdgelessTextBlockComponent>(
|
||||
EdgelessTextBlockSchema.model.flavour,
|
||||
{
|
||||
resizeConstraint: {
|
||||
lockRatio: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||
allowedHandlers: [
|
||||
'top-left',
|
||||
'top-right',
|
||||
'left',
|
||||
'right',
|
||||
'bottom-left',
|
||||
'bottom-right',
|
||||
],
|
||||
minWidth: EDGELESS_TEXT_BLOCK_MIN_WIDTH,
|
||||
},
|
||||
handleResize: context => {
|
||||
const { model, view } = context;
|
||||
const initialScale = model.props.scale;
|
||||
|
||||
return {
|
||||
onResizeStart(context) {
|
||||
context.default(context);
|
||||
model.stash('scale');
|
||||
model.stash('hasMaxWidth');
|
||||
},
|
||||
onResizeMove(context) {
|
||||
const { originalBound, newBound, constraint, lockRatio } = context;
|
||||
|
||||
if (lockRatio) {
|
||||
const originalRealWidth = originalBound.w / initialScale;
|
||||
const newScale = newBound.w / originalRealWidth;
|
||||
|
||||
model.props.scale = newScale;
|
||||
model.props.xywh = newBound.serialize();
|
||||
} else {
|
||||
if (!view.checkWidthOverflow(newBound.w)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRealWidth = clamp(
|
||||
newBound.w / initialScale,
|
||||
constraint.minWidth,
|
||||
constraint.maxWidth
|
||||
);
|
||||
|
||||
const curBound = Bound.deserialize(model.xywh);
|
||||
|
||||
model.props.xywh = Bound.serialize({
|
||||
...newBound,
|
||||
w: newRealWidth * initialScale,
|
||||
h: curBound.h,
|
||||
});
|
||||
model.props.hasMaxWidth = true;
|
||||
}
|
||||
},
|
||||
onResizeEnd(context) {
|
||||
context.default(context);
|
||||
model.pop('scale');
|
||||
model.pop('hasMaxWidth');
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
handleSelection: context => {
|
||||
const { gfx, std, view, model } = context;
|
||||
return {
|
||||
onSelect(context) {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = gfx.selection;
|
||||
const alreadySelected = gfx.selection.has(model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfx.selection.set({
|
||||
elements: [model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
view.updateComplete
|
||||
.then(() => {
|
||||
if (!view.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.children.length === 0) {
|
||||
const blockId = std.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = view
|
||||
.querySelector('.affine-block-children-container')
|
||||
?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * gfx.viewport.zoom;
|
||||
const offsetX = 2 * gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
return context.default(context);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createTextActions } from '@blocksuite/affine-gfx-text';
|
||||
import { EdgelessTextBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
|
||||
export const edgelessTextToolbarConfig = {
|
||||
// No need to adjust element bounds, which updates itself using ResizeObserver
|
||||
actions: createTextActions(EdgelessTextBlockModel, 'edgeless-text'),
|
||||
|
||||
when: ctx => ctx.getSurfaceModelsByType(EdgelessTextBlockModel).length > 0,
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
export const edgelessTextToolbarExtension = ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier('affine:surface:edgeless-text'),
|
||||
config: edgelessTextToolbarConfig,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './config';
|
||||
5
blocksuite/affine/blocks/edgeless-text/src/effects.ts
Normal file
5
blocksuite/affine/blocks/edgeless-text/src/effects.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EdgelessTextBlockComponent } from './edgeless-text-block';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-edgeless-text', EdgelessTextBlockComponent);
|
||||
}
|
||||
3
blocksuite/affine/blocks/edgeless-text/src/index.ts
Normal file
3
blocksuite/affine/blocks/edgeless-text/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './edgeless-clipboard-config';
|
||||
export * from './edgeless-text-block.js';
|
||||
export * from './edgeless-toolbar';
|
||||
14
blocksuite/affine/blocks/edgeless-text/src/store.ts
Normal file
14
blocksuite/affine/blocks/edgeless-text/src/store.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
type StoreExtensionContext,
|
||||
StoreExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import { EdgelessTextBlockSchemaExtension } from '@blocksuite/affine-model';
|
||||
|
||||
export class EdgelessTextStoreExtension extends StoreExtensionProvider {
|
||||
override name = 'affine-edgeless-text-block';
|
||||
|
||||
override setup(context: StoreExtensionContext) {
|
||||
super.setup(context);
|
||||
context.register(EdgelessTextBlockSchemaExtension);
|
||||
}
|
||||
}
|
||||
37
blocksuite/affine/blocks/edgeless-text/src/view.ts
Normal file
37
blocksuite/affine/blocks/edgeless-text/src/view.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import { BlockViewExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { EdgelessClipboardEdgelessTextConfig } from './edgeless-clipboard-config';
|
||||
import { EdgelessTextInteraction } from './edgeless-text-block';
|
||||
import { edgelessTextToolbarExtension } from './edgeless-toolbar';
|
||||
import { effects } from './effects';
|
||||
|
||||
export class EdgelessTextViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-edgeless-text-block';
|
||||
|
||||
override effect() {
|
||||
super.effect();
|
||||
effects();
|
||||
}
|
||||
|
||||
override setup(context: ViewExtensionContext) {
|
||||
super.setup(context);
|
||||
const isEdgeless = this.isEdgeless(context.scope);
|
||||
|
||||
if (isEdgeless) {
|
||||
context.register([
|
||||
BlockViewExtension(
|
||||
'affine:edgeless-text',
|
||||
literal`affine-edgeless-text`
|
||||
),
|
||||
]);
|
||||
context.register(edgelessTextToolbarExtension);
|
||||
context.register(EdgelessClipboardEdgelessTextConfig);
|
||||
context.register(EdgelessTextInteraction);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
blocksuite/affine/blocks/edgeless-text/tsconfig.json
Normal file
21
blocksuite/affine/blocks/edgeless-text/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../surface" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../gfx/text" },
|
||||
{ "path": "../../inlines/preset" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
{ "path": "../../../framework/store" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user