refactor(editor): remove edit view of database block properties (#10748)

This commit is contained in:
zzj3720
2025-03-10 16:24:44 +00:00
parent 4a45cc9ba4
commit db707dff7f
49 changed files with 1387 additions and 1782 deletions

View File

@@ -30,6 +30,7 @@
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@@ -4,37 +4,20 @@ import { DatabaseBlockComponent } from './database-block';
import { DatabaseDndPreviewBlockComponent } from './database-dnd-preview-block';
import { BlockRenderer } from './detail-panel/block-renderer';
import { NoteRenderer } from './detail-panel/note-renderer';
import { LinkCell, LinkCellEditing } from './properties/link/cell-renderer';
import { LinkNode } from './properties/link/components/link-node';
import {
RichTextCell,
RichTextCellEditing,
} from './properties/rich-text/cell-renderer';
import { LinkCell } from './properties/link/cell-renderer';
import { RichTextCell } from './properties/rich-text/cell-renderer';
import { IconCell } from './properties/title/icon';
import {
HeaderAreaTextCell,
HeaderAreaTextCellEditing,
} from './properties/title/text';
import { HeaderAreaTextCell } from './properties/title/text';
export function effects() {
customElements.define('affine-database-title', DatabaseTitle);
customElements.define('data-view-header-area-icon', IconCell);
customElements.define('affine-database-link-cell', LinkCell);
customElements.define('affine-database-link-cell-editing', LinkCellEditing);
customElements.define('data-view-header-area-text', HeaderAreaTextCell);
customElements.define(
'data-view-header-area-text-editing',
HeaderAreaTextCellEditing
);
customElements.define('affine-database-rich-text-cell', RichTextCell);
customElements.define(
'affine-database-rich-text-cell-editing',
RichTextCellEditing
);
customElements.define('center-peek', CenterPeek);
customElements.define('database-datasource-note-renderer', NoteRenderer);
customElements.define('database-datasource-block-renderer', BlockRenderer);
customElements.define('affine-database-link-node', LinkNode);
customElements.define('affine-database', DatabaseBlockComponent);
customElements.define(

View File

@@ -0,0 +1,99 @@
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const linkCellStyle = style({
width: '100%',
height: '100%',
userSelect: 'none',
position: 'relative',
});
export const linkContainerStyle = style({
display: 'flex',
position: 'relative',
alignItems: 'center',
width: '100%',
height: '100%',
outline: 'none',
overflow: 'hidden',
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
wordBreak: 'break-all',
});
export const linkIconContainerStyle = style({
position: 'absolute',
right: '8px',
top: '8px',
display: 'flex',
alignItems: 'center',
visibility: 'hidden',
backgroundColor: cssVarV2.layer.background.primary,
boxShadow: 'var(--affine-button-shadow)',
borderRadius: '4px',
overflow: 'hidden',
zIndex: 1,
});
export const linkIconStyle = style({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
color: cssVarV2.icon.primary,
fontSize: '14px',
padding: '2px',
':hover': {
backgroundColor: cssVarV2.layer.background.hoverOverlay,
},
});
export const showLinkIconStyle = style({
selectors: {
[`${linkCellStyle}:hover &`]: {
visibility: 'visible',
},
},
});
export const linkedDocStyle = style({
textDecoration: 'underline',
textDecorationColor: 'var(--affine-divider-color)',
transition: 'text-decoration-color 0.2s ease-out',
cursor: 'pointer',
':hover': {
textDecorationColor: 'var(--affine-icon-color)',
},
});
export const linkEditingStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
wordBreak: 'break-all',
':focus': {
outline: 'none',
},
});
export const inlineLinkNodeStyle = style({
wordBreak: 'break-all',
color: 'var(--affine-link-color)',
fill: 'var(--affine-link-color)',
cursor: 'pointer',
fontWeight: 'normal',
fontStyle: 'normal',
textDecoration: 'none',
});
export const normalTextStyle = style({
wordBreak: 'break-all',
});

View File

@@ -1,6 +1,5 @@
import { RefNodeSlotsProvider } from '@blocksuite/affine-rich-text';
import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
isValidUrl,
normalizeUrl,
@@ -12,106 +11,69 @@ import {
createIcon,
} from '@blocksuite/data-view';
import { EditIcon } from '@blocksuite/icons/lit';
import { baseTheme } from '@toeverything/theme';
import { css, nothing, unsafeCSS } from 'lit';
import { query, state } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import { computed } from '@preact/signals-core';
import { html, nothing, type PropertyValues } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
import { HostContextKey } from '../../context/host-context.js';
import {
inlineLinkNodeStyle,
linkCellStyle,
linkContainerStyle,
linkedDocStyle,
linkEditingStyle,
linkIconContainerStyle,
linkIconStyle,
normalTextStyle,
showLinkIconStyle,
} from './cell-renderer.css.js';
import { linkPropertyModelConfig } from './define.js';
export class LinkCell extends BaseCellRenderer<string> {
static override styles = css`
affine-database-link-cell {
width: 100%;
user-select: none;
position: relative;
}
affine-database-link-cell:hover .affine-database-link-icon {
visibility: visible;
}
.affine-database-link {
display: flex;
position: relative;
align-items: center;
width: 100%;
height: 100%;
outline: none;
overflow: hidden;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
word-break: break-all;
}
affine-database-link-node {
flex: 1;
word-break: break-all;
}
.affine-database-link-icon {
position: absolute;
right: 8px;
top: 8px;
display: flex;
align-items: center;
visibility: hidden;
cursor: pointer;
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
color: ${unsafeCSSVarV2('icon/primary')};
box-shadow: var(--affine-button-shadow);
border-radius: 4px;
font-size: 14px;
padding: 2px;
}
.affine-database-link-icon:hover {
background: var(--affine-hover-color);
}
.data-view-link-column-linked-doc {
text-decoration: underline;
text-decoration-color: var(--affine-divider-color);
transition: text-decoration-color 0.2s ease-out;
cursor: pointer;
}
.data-view-link-column-linked-doc:hover {
text-decoration-color: var(--affine-icon-color);
}
`;
private readonly _onClick = (event: Event) => {
event.stopPropagation();
const value = this.value ?? '';
if (!value || !isValidUrl(value)) {
this.selectCurrentCell(true);
return;
}
if (isValidUrl(value)) {
const target = event.target as HTMLElement;
const link = target.querySelector<HTMLAnchorElement>('.link-node');
if (link) {
event.preventDefault();
link.click();
}
return;
}
};
protected override firstUpdated(_changedProperties: PropertyValues) {
super.firstUpdated(_changedProperties);
this.classList.add(linkCellStyle);
}
private readonly _onEdit = (e: Event) => {
e.stopPropagation();
this.selectCurrentCell(true);
this.selectCurrentCell(true);
};
private preValue?: string;
private readonly _focusEnd = () => {
const ele = this._container.value;
if (!ele) {
return;
}
const end = ele?.value.length;
ele?.focus();
ele?.setSelectionRange(end, end);
};
private readonly _onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.isComposing) {
this.selectCurrentCell(false);
}
};
private readonly _setValue = (
value: string = this._container.value?.value ?? ''
) => {
let url = value;
if (isValidUrl(value)) {
url = normalizeUrl(value);
}
this.valueSetNextTick(url);
if (this._container.value) {
this._container.value.value = url;
}
};
openDoc = (e: MouseEvent) => {
e.stopPropagation();
if (!this.docId) {
if (!this.docId$.value) {
return;
}
const std = this.std;
@@ -120,7 +82,7 @@ export class LinkCell extends BaseCellRenderer<string> {
}
std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({
pageId: this.docId,
pageId: this.docId$.value,
host: std.host,
});
};
@@ -130,128 +92,95 @@ export class LinkCell extends BaseCellRenderer<string> {
return host?.std;
}
override render() {
const linkText = this.value ?? '';
const docName =
this.docId && this.std?.workspace.getDoc(this.docId)?.meta?.title;
return html`
<div class="affine-database-link" @click="${this._onClick}">
${docName
? html`<span
class="data-view-link-column-linked-doc"
@click="${this.openDoc}"
>${docName}</span
>`
: html` <affine-database-link-node
.link="${linkText}"
></affine-database-link-node>`}
</div>
${docName || linkText
? html` <div class="affine-database-link-icon" @click="${this._onEdit}">
${EditIcon()}
</div>`
: nothing}
`;
}
override updated() {
if (this.value !== this.preValue) {
const std = this.std;
this.preValue = this.value;
if (!this.value || !isValidUrl(this.value)) {
this.docId = undefined;
return;
}
this.docId =
std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(this.value)?.docId ??
undefined;
docId$ = computed(() => {
if (!this.value || !isValidUrl(this.value)) {
return;
}
}
return this.parseDocUrl(this.value)?.docId;
});
@state()
accessor docId: string | undefined = undefined;
}
private readonly _container = createRef<HTMLInputElement>();
export class LinkCellEditing extends BaseCellRenderer<string> {
static override styles = css`
affine-database-link-cell-editing {
width: 100%;
cursor: text;
}
.affine-database-link-editing {
display: flex;
align-items: center;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
word-break: break-all;
}
.affine-database-link-editing:focus {
outline: none;
}
`;
private readonly _focusEnd = () => {
const end = this._container.value.length;
this._container.focus();
this._container.setSelectionRange(end, end);
};
private readonly _onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.isComposing) {
this._setValue();
setTimeout(() => {
this.selectCurrentCell(false);
});
}
};
private readonly _setValue = (value: string = this._container.value) => {
let url = value;
if (isValidUrl(value)) {
url = normalizeUrl(value);
}
this.onChange(url);
this._container.value = url;
};
override firstUpdated() {
override afterEnterEditingMode() {
this._focusEnd();
}
override onExitEditMode() {
override beforeExitEditingMode() {
this._setValue();
}
override render() {
const linkText = this.value ?? '';
return html`<input
class="affine-database-link-editing link"
.value="${linkText}"
@keydown="${this._onKeydown}"
@pointerdown="${stopPropagation}"
/>`;
parseDocUrl(url: string) {
return this.std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(url);
}
@query('.affine-database-link-editing')
private accessor _container!: HTMLInputElement;
docName$ = computed(() => {
const title =
this.docId$.value &&
this.std?.workspace.getDoc(this.docId$.value)?.meta?.title;
if (title == null) {
return;
}
return title || 'Untitled';
});
renderLink() {
const linkText = this.value ?? '';
const docName = this.docName$.value;
const isDoc = !!docName;
const isLink = !!linkText;
const hasLink = isDoc || isLink;
return html`
<div>
<div class="${linkContainerStyle}">
${isDoc
? html`<span class="${linkedDocStyle}" @click="${this.openDoc}"
>${docName}</span
>`
: isValidUrl(linkText)
? html`<a
data-testid="property-link-a"
class="${inlineLinkNodeStyle}"
href="${linkText}"
rel="noopener noreferrer"
target="_blank"
>${linkText}</a
>`
: html`<span class="${normalTextStyle}">${linkText}</span>`}
</div>
${hasLink
? html` <div class="${linkIconContainerStyle} ${showLinkIconStyle}">
<div
class="${linkIconStyle}"
data-testid="edit-link-button"
@click="${this._onEdit}"
>
${EditIcon()}
</div>
</div>`
: nothing}
</div>
`;
}
override render() {
if (this.isEditing$.value) {
const linkText = this.value ?? '';
return html`<input
class="${linkEditingStyle} link"
${ref(this._container)}
.value="${linkText}"
@keydown="${this._onKeydown}"
@pointerdown="${stopPropagation}"
/>`;
} else {
return this.renderLink();
}
}
}
export const linkColumnConfig = linkPropertyModelConfig.createPropertyMeta({
icon: createIcon('LinkIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(LinkCell),
edit: createFromBaseCellRenderer(LinkCellEditing),
},
});

View File

@@ -1,41 +0,0 @@
import { isValidUrl } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
export class LinkNode extends ShadowlessElement {
static override styles = css`
.link-node {
word-break: break-all;
color: var(--affine-link-color);
fill: var(--affine-link-color);
cursor: pointer;
font-weight: normal;
font-style: normal;
text-decoration: none;
}
`;
protected override render() {
if (!isValidUrl(this.link)) {
return html`<span class="normal-text">${this.link}</span>`;
}
return html`<a
class="link-node"
href=${this.link}
rel="noopener noreferrer"
target="_blank"
><span class="link-node-text">${this.link}</span></a
>`;
}
@property({ attribute: false })
accessor link!: string;
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-link-node': LinkNode;
}
}

View File

@@ -0,0 +1,20 @@
import { style } from '@vanilla-extract/css';
export const richTextCellStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
userSelect: 'none',
});
export const richTextContainerStyle = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
height: '100%',
outline: 'none',
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
wordBreak: 'break-all',
});

View File

@@ -21,13 +21,16 @@ import { IS_MAC } from '@blocksuite/global/env';
import type { DeltaInsert } from '@blocksuite/inline';
import type { BlockSnapshot } from '@blocksuite/store';
import { Text } from '@blocksuite/store';
import { css } from 'lit';
import { query } from 'lit/decorators.js';
import { keyed } from 'lit/directives/keyed.js';
import { computed, effect, signal } from '@preact/signals-core';
import { ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import {
richTextCellStyle,
richTextContainerStyle,
} from './cell-renderer.css.js';
import { richTextPropertyModelConfig } from './define.js';
function toggleStyle(
@@ -78,61 +81,10 @@ function toggleStyle(
inlineEditor.syncInlineRange();
}
abstract class BaseRichTextCell extends BaseCellRenderer<Text> {
static override styles = css`
affine-database-rich-text-cell,
affine-database-rich-text-cell-editing {
display: flex;
align-items: center;
width: 100%;
user-select: none;
}
.affine-database-rich-text {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
outline: none;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
word-break: break-all;
}
.affine-database-rich-text v-line {
display: flex !important;
align-items: center;
height: 100%;
width: 100%;
}
.affine-database-rich-text v-line > div {
flex-grow: 1;
}
.data-view-header-area-icon {
height: max-content;
display: flex;
align-items: center;
margin-right: 8px;
padding: 2px;
border-radius: 4px;
margin-top: 2px;
background-color: var(--affine-background-secondary-color);
}
.data-view-header-area-icon svg {
width: 14px;
height: 14px;
fill: var(--affine-icon-color);
color: var(--affine-icon-color);
}
`;
get inlineEditor() {
return this.richText?.inlineEditor;
}
export class RichTextCell extends BaseCellRenderer<Text> {
inlineEditor$ = computed(() => {
return this.richText$.value?.inlineEditor;
});
get inlineManager() {
return this.view
@@ -146,57 +98,11 @@ abstract class BaseRichTextCell extends BaseCellRenderer<Text> {
return databaseBlock?.topContenteditableElement;
}
get attributeRenderer() {
return this.inlineManager?.getRenderer();
}
get attributesSchema() {
return this.inlineManager?.getSchema();
}
get host() {
return this.view.contextGet(HostContextKey);
}
@query('rich-text')
accessor richText!: RichText;
@query('.affine-database-rich-text')
accessor _richTextElement!: HTMLElement;
}
export class RichTextCell extends BaseRichTextCell {
static override styles = css`
affine-database-rich-text-cell {
display: flex;
align-items: center;
width: 100%;
user-select: none;
}
.affine-database-rich-text {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
outline: none;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
word-break: break-all;
}
.affine-database-rich-text v-line {
display: flex !important;
align-items: center;
height: 100%;
width: 100%;
}
.affine-database-rich-text v-line > div {
flex-grow: 1;
}
`;
private readonly richText$ = signal<RichText>();
private changeUserSelectAccordToReadOnly() {
if (this && this instanceof HTMLElement) {
@@ -204,61 +110,6 @@ export class RichTextCell extends BaseRichTextCell {
}
}
override connectedCallback() {
super.connectedCallback();
this.changeUserSelectAccordToReadOnly();
}
override render() {
if (!this.value || !(this.value instanceof Text)) {
return html`<div class="affine-database-rich-text"></div>`;
}
return keyed(
this.value,
html`<rich-text
.yText=${this.value}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownMatches=${this.inlineManager?.markdownMatches}
.readonly=${true}
class="affine-database-rich-text inline-editor"
></rich-text>`
);
}
}
export class RichTextCellEditing extends BaseRichTextCell {
static override styles = css`
affine-database-rich-text-cell-editing {
display: flex;
align-items: center;
width: 100%;
min-width: 1px;
cursor: text;
}
.affine-database-rich-text {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
outline: none;
}
.affine-database-rich-text v-line {
display: flex !important;
align-items: center;
height: 100%;
width: 100%;
}
.affine-database-rich-text v-line > div {
flex-grow: 1;
}
`;
private readonly _handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape') {
if (event.key === 'Tab') {
@@ -280,7 +131,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
return;
}
const inlineEditor = this.inlineEditor;
const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return;
switch (event.key) {
@@ -331,17 +182,17 @@ export class RichTextCellEditing extends BaseRichTextCell {
private readonly _initYText = (text?: string) => {
const yText = new Text(text);
this.onChange(yText);
this.valueSetImmediate(yText);
};
private readonly _onSoftEnter = () => {
if (this.value && this.inlineEditor) {
const inlineRange = this.inlineEditor.getInlineRange();
if (this.value && this.inlineEditor$.value) {
const inlineRange = this.inlineEditor$.value.getInlineRange();
if (!inlineRange) return;
const text = new Text(this.inlineEditor.yText);
const text = new Text(this.inlineEditor$.value.yText);
text.replace(inlineRange.index, inlineRange.length, '\n');
this.inlineEditor.setInlineRange({
this.inlineEditor$.value.setInlineRange({
index: inlineRange.index + 1,
length: 0,
});
@@ -349,7 +200,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
};
private readonly _onCopy = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor;
const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return;
const inlineRange = inlineEditor.getInlineRange();
@@ -366,7 +217,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
};
private readonly _onCut = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor;
const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return;
const inlineRange = inlineEditor.getInlineRange();
@@ -388,7 +239,9 @@ export class RichTextCellEditing extends BaseRichTextCell {
};
private readonly _onPaste = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor;
e.preventDefault();
e.stopPropagation();
const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return;
const inlineRange = inlineEditor.getInlineRange();
@@ -419,8 +272,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
?.getData('text/plain')
?.replace(/\r?\n|\r/g, '\n');
if (!text) return;
e.preventDefault();
e.stopPropagation();
if (isValidUrl(text)) {
const std = this.std;
const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
@@ -459,6 +311,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
});
}
} else {
console.log(text);
inlineEditor.insertText(inlineRange, text);
inlineEditor.setInlineRange({
index: inlineRange.index + text.length,
@@ -469,67 +322,78 @@ export class RichTextCellEditing extends BaseRichTextCell {
override connectedCallback() {
super.connectedCallback();
if (!this.value || typeof this.value === 'string') {
this._initYText(this.value);
}
this.classList.add(richTextCellStyle);
this.changeUserSelectAccordToReadOnly();
const selectAll = (e: KeyboardEvent) => {
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
e.stopPropagation();
e.preventDefault();
this.inlineEditor?.selectAll();
this.inlineEditor$.value?.selectAll();
}
};
this.addEventListener('keydown', selectAll);
this.disposables.addFromEvent(this, 'keydown', selectAll);
this.disposables.add(
effect(() => {
const editor = this.inlineEditor$.value;
if (editor) {
const disposable = editor.slots.keydown.on(this._handleKeyDown);
return () => disposable.dispose();
}
return;
})
);
this.disposables.add(
effect(() => {
const richText = this.richText$.value;
if (richText) {
richText.addEventListener('copy', this._onCopy, true);
richText.addEventListener('cut', this._onCut, true);
richText.addEventListener('paste', this._onPaste, true);
return () => {
richText.removeEventListener('copy', this._onCopy);
richText.removeEventListener('cut', this._onCut);
richText.removeEventListener('paste', this._onPaste);
};
}
return;
})
);
}
override firstUpdated() {
this.richText?.updateComplete
.then(() => {
const inlineEditor = this.inlineEditor;
if (!inlineEditor) return;
override beforeEnterEditMode() {
if (!this.value || typeof this.value === 'string') {
this._initYText(this.value);
}
return true;
}
this.disposables.add(
inlineEditor.slots.keydown.on(this._handleKeyDown)
);
this.disposables.addFromEvent(
this._richTextElement!,
'copy',
this._onCopy
);
this.disposables.addFromEvent(
this._richTextElement!,
'cut',
this._onCut
);
this.disposables.addFromEvent(
this._richTextElement!,
'paste',
this._onPaste
);
inlineEditor.focusEnd();
})
.catch(console.error);
override afterEnterEditingMode() {
this.inlineEditor$.value?.focusEnd();
}
override render() {
return html`<rich-text
if (!this.value || !(this.value instanceof Text)) {
return html` <div class="${richTextContainerStyle}"></div>`;
}
return html` <rich-text
${ref(this.richText$)}
data-disable-ask-ai
data-not-block-text
.yText=${this.value}
.inlineEventSource=${this.topContenteditableElement}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownMatches=${this.inlineManager?.markdownMatches}
.verticalScrollContainerGetter=${() =>
.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 || this.readonly}"
.verticalScrollContainerGetter="${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)
: null}
class="affine-database-rich-text inline-editor"
: null}"
class="${richTextContainerStyle} inline-editor"
></rich-text>`;
}
@@ -538,7 +402,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
}
insertDelta = (delta: DeltaInsert<AffineTextAttributes>) => {
const inlineEditor = this.inlineEditor;
const inlineEditor = this.inlineEditor$.value;
const range = inlineEditor?.getInlineRange();
if (!range || !delta.insert) {
return;
@@ -553,7 +417,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
declare global {
interface HTMLElementTagNameMap {
'affine-database-rich-text-cell-editing': RichTextCellEditing;
'affine-database-rich-text-cell': RichTextCell;
}
}
@@ -563,6 +427,5 @@ export const richTextColumnConfig =
cellRenderer: {
view: createFromBaseCellRenderer(RichTextCell),
edit: createFromBaseCellRenderer(RichTextCellEditing),
},
});

View File

@@ -0,0 +1,31 @@
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
import { style } from '@vanilla-extract/css';
export const titleCellStyle = style({
width: '100%',
display: 'flex',
});
export const titleRichTextStyle = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
height: '100%',
outline: 'none',
wordBreak: 'break-all',
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
});
export const headerAreaIconStyle = style({
height: 'max-content',
display: 'flex',
alignItems: 'center',
marginRight: '8px',
padding: '2px',
borderRadius: '4px',
marginTop: '2px',
color: cssVarV2.icon.primary,
backgroundColor: 'var(--affine-background-secondary-color)',
});

View File

@@ -7,7 +7,7 @@ import {
import { TableSingleView } from '@blocksuite/data-view/view-presets';
import { titlePropertyModelConfig } from './define.js';
import { HeaderAreaTextCell, HeaderAreaTextCellEditing } from './text.js';
import { HeaderAreaTextCell } from './text.js';
export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({
icon: createIcon('TitleIcon'),
@@ -19,12 +19,5 @@ export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({
showIcon: props.cell.view instanceof TableSingleView,
})
),
edit: uniMap(
createFromBaseCellRenderer(HeaderAreaTextCellEditing),
(props: CellRenderProps) => ({
...props,
showIcon: props.cell.view instanceof TableSingleView,
})
),
},
});

View File

@@ -1,4 +1,3 @@
import type { RootBlockModel } from '@blocksuite/affine-model';
import {
DefaultInlineManagerExtension,
type RichText,
@@ -16,106 +15,31 @@ import { IS_MAC } from '@blocksuite/global/env';
import { LinkedPageIcon } from '@blocksuite/icons/lit';
import type { DeltaInsert } from '@blocksuite/inline';
import type { BlockSnapshot, Text } from '@blocksuite/store';
import { computed, effect, signal } from '@preact/signals-core';
import { css, type TemplateResult } from 'lit';
import { property, query } from 'lit/decorators.js';
import { 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 { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
import {
headerAreaIconStyle,
titleCellStyle,
titleRichTextStyle,
} from './cell-renderer.css.js';
const styles = css`
data-view-header-area-text {
width: 100%;
display: flex;
}
data-view-header-area-text rich-text {
pointer-events: none;
user-select: none;
}
data-view-header-area-text-editing {
width: 100%;
display: flex;
cursor: text;
}
.data-view-header-area-rich-text {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
outline: none;
word-break: break-all;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
}
.data-view-header-area-rich-text v-line {
display: flex !important;
align-items: center;
height: 100%;
width: 100%;
}
.data-view-header-area-rich-text v-line > div {
flex-grow: 1;
}
.data-view-header-area-icon {
height: max-content;
display: flex;
align-items: center;
margin-right: 8px;
padding: 2px;
border-radius: 4px;
margin-top: 2px;
background-color: var(--affine-background-secondary-color);
}
.data-view-header-area-icon svg {
width: 14px;
height: 14px;
fill: var(--affine-icon-color);
color: var(--affine-icon-color);
}
`;
abstract class BaseTextCell extends BaseCellRenderer<Text> {
static override styles = styles;
export class HeaderAreaTextCell extends BaseCellRenderer<Text> {
activity = true;
docId$ = signal<string>();
isLinkedDoc$ = computed(() => false);
linkedDocTitle$ = computed(() => {
if (!this.docId$.value) {
return this.value;
}
const doc = this.host?.std.workspace.getDoc(this.docId$.value);
const root = doc?.root as RootBlockModel;
return root.title;
});
get attributeRenderer() {
return this.inlineManager?.getRenderer();
}
get attributesSchema() {
return this.inlineManager?.getSchema();
}
get host() {
return this.view.contextGet(HostContextKey);
}
get inlineEditor() {
return this.richText.inlineEditor;
return this.richText.value?.inlineEditor;
}
get inlineManager() {
@@ -128,80 +52,10 @@ abstract class BaseTextCell extends BaseCellRenderer<Text> {
return databaseBlock?.topContenteditableElement;
}
override connectedCallback() {
super.connectedCallback();
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);
});
}
}
get std() {
return this.view.contextGet(HostContextKey)?.std;
}
protected override render(): unknown {
return html`${this.renderIcon()}${this.renderBlockText()}`;
}
abstract renderBlockText(): TemplateResult;
renderIcon() {
if (this.docId$.value) {
return html` <div class="data-view-header-area-icon">
${LinkedPageIcon()}
</div>`;
}
if (!this.showIcon) {
return;
}
const iconColumn = this.view.mainProperties$.value.iconColumn;
if (!iconColumn) return;
const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string;
if (!icon) return;
return html` <div class="data-view-header-area-icon">${icon}</div>`;
}
abstract renderLinkedDoc(): TemplateResult;
@query('rich-text')
accessor richText!: RichText;
@property({ attribute: false })
accessor showIcon = false;
}
export class HeaderAreaTextCell extends BaseTextCell {
override renderBlockText() {
return html` <rich-text
.yText="${this.value}"
.attributesSchema="${this.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownMatches="${this.inlineManager?.markdownMatches}"
.readonly="${true}"
class="data-view-header-area-rich-text"
></rich-text>`;
}
override renderLinkedDoc(): TemplateResult {
return html` <rich-text
.yText="${this.linkedDocTitle$.value}"
.readonly="${true}"
class="data-view-header-area-rich-text"
></rich-text>`;
}
}
export class HeaderAreaTextCellEditing extends BaseTextCell {
private readonly _onCopy = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor;
if (!inlineEditor) return;
@@ -318,8 +172,6 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
}
};
override activity = false;
insertDelta = (delta: DeltaInsert) => {
const inlineEditor = this.inlineEditor;
const range = inlineEditor?.getInlineRange();
@@ -333,12 +185,25 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
});
};
private get std() {
return this.host?.std;
}
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();
@@ -346,82 +211,86 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
this.inlineEditor?.selectAll();
}
};
this.addEventListener('keydown', selectAll);
this.disposables.add(() => {
this.removeEventListener('keydown', selectAll);
});
this.disposables.addFromEvent(this, 'keydown', selectAll);
}
override firstUpdated(props: Map<string, unknown>) {
super.firstUpdated(props);
if (!this.isLinkedDoc$.value) {
this.disposables.addFromEvent(this.richText, 'copy', this._onCopy);
this.disposables.addFromEvent(this.richText, 'cut', this._onCut);
this.disposables.addFromEvent(this.richText, 'paste', this._onPaste);
}
this.richText.updateComplete
this.richText.value?.updateComplete
.then(() => {
this.inlineEditor?.focusEnd();
this.disposables.add(
effect(() => {
const inlineRange = this.inlineEditor?.inlineRange$.value;
if (inlineRange) {
if (!this.isEditing) {
this.selectCurrentCell(true);
}
} else {
if (this.isEditing) {
this.selectCurrentCell(false);
}
}
})
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
);
})
.catch(console.error);
}
override renderBlockText() {
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.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.attributesSchema="${this.inlineManager?.getSchema()}"
.attributeRenderer="${this.inlineManager?.getRenderer()}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownMatches="${this.inlineManager?.markdownMatches}"
.readonly="${this.readonly}"
.readonly="${!this.isEditing$.value}"
.enableClipboard="${false}"
.verticalScrollContainerGetter="${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)
: null}"
data-parent-flavour="affine:database"
class="data-view-header-area-rich-text"
class="${titleRichTextStyle}"
></rich-text>`;
}
override renderLinkedDoc(): TemplateResult {
return html` <rich-text
data-disable-ask-ai
data-not-block-text
.yText="${this.linkedDocTitle$.value}"
.inlineEventSource="${this.topContenteditableElement}"
.readonly="${this.readonly}"
.enableClipboard="${true}"
.verticalScrollContainerGetter="${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)
: null}"
class="data-view-header-area-rich-text"
></rich-text>`;
renderIcon() {
if (!this.showIcon) {
return;
}
if (this.docId$.value) {
return html` <div class="${headerAreaIconStyle}">
${LinkedPageIcon({})}
</div>`;
}
const iconColumn = this.view.mainProperties$.value.iconColumn;
if (!iconColumn) return;
const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string;
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;
'data-view-header-area-text-editing': HeaderAreaTextCellEditing;
}
}