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
310 lines
8.8 KiB
TypeScript
310 lines
8.8 KiB
TypeScript
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
|
|
import type { RichText } from '@blocksuite/affine-rich-text';
|
|
import {
|
|
ParseDocUrlProvider,
|
|
TelemetryProvider,
|
|
} from '@blocksuite/affine-shared/services';
|
|
import { getViewportElement } from '@blocksuite/affine-shared/utils';
|
|
import { BaseCellRenderer } from '@blocksuite/data-view';
|
|
import { IS_MAC } from '@blocksuite/global/env';
|
|
import { LinkedPageIcon } from '@blocksuite/icons/lit';
|
|
import type { BlockSnapshot, DeltaInsert, Text } from '@blocksuite/store';
|
|
import { computed, signal } from '@preact/signals-core';
|
|
import { property } from 'lit/decorators.js';
|
|
import { createRef, ref } from 'lit/directives/ref.js';
|
|
import { html } from 'lit/static-html.js';
|
|
|
|
import { EditorHostKey } from '../../context/host-context.js';
|
|
import type { DatabaseBlockComponent } from '../../database-block.js';
|
|
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
|
|
import { analyzeTextForUrlPaste, insertUrlTextSegments } from '../paste-url.js';
|
|
import {
|
|
headerAreaIconStyle,
|
|
titleCellStyle,
|
|
titleRichTextStyle,
|
|
} from './cell-renderer-css.js';
|
|
|
|
export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
|
activity = true;
|
|
|
|
docId$ = signal<string>();
|
|
|
|
get host() {
|
|
return this.view.serviceGet(EditorHostKey);
|
|
}
|
|
|
|
get inlineEditor() {
|
|
return this.richText.value?.inlineEditor;
|
|
}
|
|
|
|
get inlineManager() {
|
|
return this.host?.std.get(DefaultInlineManagerExtension.identifier);
|
|
}
|
|
|
|
get topContenteditableElement() {
|
|
const databaseBlock =
|
|
this.closest<DatabaseBlockComponent>('affine-database');
|
|
return databaseBlock?.topContenteditableElement;
|
|
}
|
|
|
|
get std() {
|
|
return this.view.serviceGet(EditorHostKey)?.std;
|
|
}
|
|
|
|
private readonly _onCopy = (e: ClipboardEvent) => {
|
|
const inlineEditor = this.inlineEditor;
|
|
if (!inlineEditor) return;
|
|
|
|
const inlineRange = inlineEditor.getInlineRange();
|
|
if (!inlineRange) return;
|
|
|
|
const text = inlineEditor.yTextString.slice(
|
|
inlineRange.index,
|
|
inlineRange.index + inlineRange.length
|
|
);
|
|
|
|
e.clipboardData?.setData('text/plain', text);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
private readonly _onCut = (e: ClipboardEvent) => {
|
|
const inlineEditor = this.inlineEditor;
|
|
if (!inlineEditor) return;
|
|
|
|
const inlineRange = inlineEditor.getInlineRange();
|
|
if (!inlineRange) return;
|
|
|
|
const text = inlineEditor.yTextString.slice(
|
|
inlineRange.index,
|
|
inlineRange.index + inlineRange.length
|
|
);
|
|
inlineEditor.deleteText(inlineRange);
|
|
inlineEditor.setInlineRange({
|
|
index: inlineRange.index,
|
|
length: 0,
|
|
});
|
|
|
|
e.clipboardData?.setData('text/plain', text);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
private readonly _onPaste = (e: ClipboardEvent) => {
|
|
const inlineEditor = this.inlineEditor;
|
|
const inlineRange = inlineEditor?.getInlineRange();
|
|
if (!inlineEditor || !inlineRange) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (e.clipboardData) {
|
|
try {
|
|
const getDeltas = (snapshot: BlockSnapshot): DeltaInsert[] => {
|
|
// @ts-expect-error FIXME: ts error
|
|
const text = snapshot.props?.text?.delta;
|
|
return text
|
|
? [...text, ...(snapshot.children?.flatMap(getDeltas) ?? [])]
|
|
: snapshot.children?.flatMap(getDeltas);
|
|
};
|
|
const snapshot = this.std?.clipboard?.readFromClipboard(
|
|
e.clipboardData
|
|
)['BLOCKSUITE/SNAPSHOT'];
|
|
const deltas = (
|
|
JSON.parse(snapshot).snapshot.content as BlockSnapshot[]
|
|
).flatMap(getDeltas);
|
|
deltas.forEach(delta => this.insertDelta(delta));
|
|
return;
|
|
} catch {
|
|
//
|
|
}
|
|
}
|
|
const text = e.clipboardData
|
|
?.getData('text/plain')
|
|
?.replace(/\r?\n|\r/g, '\n');
|
|
if (!text) return;
|
|
const { segments, singleUrl } = analyzeTextForUrlPaste(text);
|
|
if (singleUrl) {
|
|
const std = this.std;
|
|
const result = std
|
|
?.getOptional(ParseDocUrlProvider)
|
|
?.parseDocUrl(singleUrl);
|
|
if (result) {
|
|
const text = ' ';
|
|
inlineEditor.insertText(inlineRange, text, {
|
|
reference: {
|
|
type: 'LinkedPage',
|
|
pageId: result.docId,
|
|
params: {
|
|
blockIds: result.blockIds,
|
|
elementIds: result.elementIds,
|
|
mode: result.mode,
|
|
},
|
|
},
|
|
});
|
|
inlineEditor.setInlineRange({
|
|
index: inlineRange.index + text.length,
|
|
length: 0,
|
|
});
|
|
|
|
// Track when a linked doc is created in database title column
|
|
std?.getOptional(TelemetryProvider)?.track('LinkedDocCreated', {
|
|
module: 'database title cell',
|
|
type: 'paste',
|
|
segment: 'database',
|
|
parentFlavour: 'affine:database',
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
insertUrlTextSegments(inlineEditor, inlineRange, segments);
|
|
};
|
|
|
|
insertDelta = (delta: DeltaInsert) => {
|
|
const inlineEditor = this.inlineEditor;
|
|
const range = inlineEditor?.getInlineRange();
|
|
if (!range || !delta.insert) {
|
|
return;
|
|
}
|
|
inlineEditor?.insertText(range, delta.insert, delta.attributes);
|
|
inlineEditor?.setInlineRange({
|
|
index: range.index + delta.insert.length,
|
|
length: 0,
|
|
});
|
|
};
|
|
|
|
override connectedCallback() {
|
|
super.connectedCallback();
|
|
this.classList.add(titleCellStyle);
|
|
|
|
const yText = this.value?.yText;
|
|
if (yText) {
|
|
const cb = () => {
|
|
const id = getSingleDocIdFromText(this.value);
|
|
this.docId$.value = id;
|
|
};
|
|
cb();
|
|
if (this.activity) {
|
|
yText.observe(cb);
|
|
this.disposables.add(() => {
|
|
yText.unobserve(cb);
|
|
});
|
|
}
|
|
}
|
|
|
|
const selectAll = (e: KeyboardEvent) => {
|
|
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
this.inlineEditor?.selectAll();
|
|
}
|
|
};
|
|
|
|
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
|
}
|
|
|
|
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key !== 'Escape') {
|
|
if (event.key === 'Tab') {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
event.stopPropagation();
|
|
}
|
|
};
|
|
|
|
override firstUpdated(props: Map<string, unknown>) {
|
|
super.firstUpdated(props);
|
|
this.richText.value?.updateComplete
|
|
.then(() => {
|
|
if (this.richText.value) {
|
|
this.disposables.addFromEvent(
|
|
this.richText.value,
|
|
'copy',
|
|
this._onCopy
|
|
);
|
|
this.disposables.addFromEvent(
|
|
this.richText.value,
|
|
'cut',
|
|
this._onCut
|
|
);
|
|
this.disposables.addFromEvent(
|
|
this.richText.value,
|
|
'paste',
|
|
this._onPaste,
|
|
true
|
|
);
|
|
const inlineEditor = this.inlineEditor;
|
|
if (inlineEditor) {
|
|
this.disposables.add(
|
|
inlineEditor.slots.keydown.subscribe(this._handleKeyDown)
|
|
);
|
|
}
|
|
}
|
|
})
|
|
.catch(console.error);
|
|
}
|
|
|
|
override afterEnterEditingMode() {
|
|
this.inlineEditor?.focusEnd();
|
|
}
|
|
|
|
protected override render(): unknown {
|
|
return html`${this.renderIcon()}${this.renderBlockText()}`;
|
|
}
|
|
|
|
renderBlockText() {
|
|
return html` <rich-text
|
|
${ref(this.richText)}
|
|
data-disable-ask-ai
|
|
data-not-block-text
|
|
.yText="${this.value}"
|
|
.inlineEventSource="${this.topContenteditableElement}"
|
|
.attributesSchema="${this.inlineManager?.getSchema()}"
|
|
.attributeRenderer="${this.inlineManager?.getRenderer()}"
|
|
.embedChecker="${this.inlineManager?.embedChecker}"
|
|
.markdownMatches="${this.inlineManager?.markdownMatches}"
|
|
.readonly="${!this.isEditing$.value}"
|
|
.enableClipboard="${false}"
|
|
.verticalScrollContainerGetter="${() =>
|
|
this.topContenteditableElement?.host
|
|
? getViewportElement(this.topContenteditableElement.host)
|
|
: null}"
|
|
data-parent-flavour="affine:database"
|
|
class="${titleRichTextStyle}"
|
|
></rich-text>`;
|
|
}
|
|
icon$ = computed(() => {
|
|
const iconColumn = this.view.mainProperties$.value.iconColumn;
|
|
if (!iconColumn) return;
|
|
|
|
const icon = this.view.cellGetOrCreate(this.cell.rowId, iconColumn).value$
|
|
.value;
|
|
if (!icon) return;
|
|
return icon;
|
|
});
|
|
renderIcon() {
|
|
if (!this.showIcon) {
|
|
return;
|
|
}
|
|
if (this.docId$.value) {
|
|
return html` <div class="${headerAreaIconStyle}">
|
|
${LinkedPageIcon({})}
|
|
</div>`;
|
|
}
|
|
const icon = this.icon$.value;
|
|
if (!icon) return;
|
|
|
|
return html` <div class="${headerAreaIconStyle}">${icon}</div>`;
|
|
}
|
|
|
|
private readonly richText = createRef<RichText>();
|
|
|
|
@property({ attribute: false })
|
|
accessor showIcon = false;
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'data-view-header-area-text': HeaderAreaTextCell;
|
|
}
|
|
}
|