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
@@ -30,6 +30,7 @@
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12", "@toeverything/theme": "^1.1.12",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",
"date-fns": "^4.0.0", "date-fns": "^4.0.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
@@ -4,37 +4,20 @@ import { DatabaseBlockComponent } from './database-block';
import { DatabaseDndPreviewBlockComponent } from './database-dnd-preview-block'; import { DatabaseDndPreviewBlockComponent } from './database-dnd-preview-block';
import { BlockRenderer } from './detail-panel/block-renderer'; import { BlockRenderer } from './detail-panel/block-renderer';
import { NoteRenderer } from './detail-panel/note-renderer'; import { NoteRenderer } from './detail-panel/note-renderer';
import { LinkCell, LinkCellEditing } from './properties/link/cell-renderer'; import { LinkCell } from './properties/link/cell-renderer';
import { LinkNode } from './properties/link/components/link-node'; import { RichTextCell } from './properties/rich-text/cell-renderer';
import {
RichTextCell,
RichTextCellEditing,
} from './properties/rich-text/cell-renderer';
import { IconCell } from './properties/title/icon'; import { IconCell } from './properties/title/icon';
import { import { HeaderAreaTextCell } from './properties/title/text';
HeaderAreaTextCell,
HeaderAreaTextCellEditing,
} from './properties/title/text';
export function effects() { export function effects() {
customElements.define('affine-database-title', DatabaseTitle); customElements.define('affine-database-title', DatabaseTitle);
customElements.define('data-view-header-area-icon', IconCell); customElements.define('data-view-header-area-icon', IconCell);
customElements.define('affine-database-link-cell', LinkCell); 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', 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', RichTextCell);
customElements.define(
'affine-database-rich-text-cell-editing',
RichTextCellEditing
);
customElements.define('center-peek', CenterPeek); customElements.define('center-peek', CenterPeek);
customElements.define('database-datasource-note-renderer', NoteRenderer); customElements.define('database-datasource-note-renderer', NoteRenderer);
customElements.define('database-datasource-block-renderer', BlockRenderer); customElements.define('database-datasource-block-renderer', BlockRenderer);
customElements.define('affine-database-link-node', LinkNode);
customElements.define('affine-database', DatabaseBlockComponent); customElements.define('affine-database', DatabaseBlockComponent);
customElements.define( customElements.define(
@@ -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',
});
@@ -1,6 +1,5 @@
import { RefNodeSlotsProvider } from '@blocksuite/affine-rich-text'; import { RefNodeSlotsProvider } from '@blocksuite/affine-rich-text';
import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services'; import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { import {
isValidUrl, isValidUrl,
normalizeUrl, normalizeUrl,
@@ -12,106 +11,69 @@ import {
createIcon, createIcon,
} from '@blocksuite/data-view'; } from '@blocksuite/data-view';
import { EditIcon } from '@blocksuite/icons/lit'; import { EditIcon } from '@blocksuite/icons/lit';
import { baseTheme } from '@toeverything/theme'; import { computed } from '@preact/signals-core';
import { css, nothing, unsafeCSS } from 'lit'; import { html, nothing, type PropertyValues } from 'lit';
import { query, state } 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 { 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'; import { linkPropertyModelConfig } from './define.js';
export class LinkCell extends BaseCellRenderer<string> { export class LinkCell extends BaseCellRenderer<string> {
static override styles = css` protected override firstUpdated(_changedProperties: PropertyValues) {
affine-database-link-cell { super.firstUpdated(_changedProperties);
width: 100%; this.classList.add(linkCellStyle);
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;
}
};
private readonly _onEdit = (e: Event) => { private readonly _onEdit = (e: Event) => {
e.stopPropagation(); e.stopPropagation();
this.selectCurrentCell(true); 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) => { openDoc = (e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (!this.docId) { if (!this.docId$.value) {
return; return;
} }
const std = this.std; const std = this.std;
@@ -120,7 +82,7 @@ export class LinkCell extends BaseCellRenderer<string> {
} }
std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({ std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({
pageId: this.docId, pageId: this.docId$.value,
host: std.host, host: std.host,
}); });
}; };
@@ -130,128 +92,95 @@ export class LinkCell extends BaseCellRenderer<string> {
return host?.std; return host?.std;
} }
override render() { docId$ = computed(() => {
const linkText = this.value ?? ''; if (!this.value || !isValidUrl(this.value)) {
const docName = return;
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;
} }
} return this.parseDocUrl(this.value)?.docId;
});
@state() private readonly _container = createRef<HTMLInputElement>();
accessor docId: string | undefined = undefined;
}
export class LinkCellEditing extends BaseCellRenderer<string> { override afterEnterEditingMode() {
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() {
this._focusEnd(); this._focusEnd();
} }
override onExitEditMode() { override beforeExitEditingMode() {
this._setValue(); this._setValue();
} }
override render() { parseDocUrl(url: string) {
const linkText = this.value ?? ''; return this.std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(url);
return html`<input
class="affine-database-link-editing link"
.value="${linkText}"
@keydown="${this._onKeydown}"
@pointerdown="${stopPropagation}"
/>`;
} }
@query('.affine-database-link-editing') docName$ = computed(() => {
private accessor _container!: HTMLInputElement; 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({ export const linkColumnConfig = linkPropertyModelConfig.createPropertyMeta({
icon: createIcon('LinkIcon'), icon: createIcon('LinkIcon'),
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(LinkCell), view: createFromBaseCellRenderer(LinkCell),
edit: createFromBaseCellRenderer(LinkCellEditing),
}, },
}); });
@@ -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;
}
}
@@ -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',
});
@@ -21,13 +21,16 @@ import { IS_MAC } from '@blocksuite/global/env';
import type { DeltaInsert } from '@blocksuite/inline'; import type { DeltaInsert } from '@blocksuite/inline';
import type { BlockSnapshot } from '@blocksuite/store'; import type { BlockSnapshot } from '@blocksuite/store';
import { Text } from '@blocksuite/store'; import { Text } from '@blocksuite/store';
import { css } from 'lit'; import { computed, effect, signal } from '@preact/signals-core';
import { query } from 'lit/decorators.js'; import { ref } from 'lit/directives/ref.js';
import { keyed } from 'lit/directives/keyed.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import { HostContextKey } from '../../context/host-context.js'; import { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js'; import type { DatabaseBlockComponent } from '../../database-block.js';
import {
richTextCellStyle,
richTextContainerStyle,
} from './cell-renderer.css.js';
import { richTextPropertyModelConfig } from './define.js'; import { richTextPropertyModelConfig } from './define.js';
function toggleStyle( function toggleStyle(
@@ -78,61 +81,10 @@ function toggleStyle(
inlineEditor.syncInlineRange(); inlineEditor.syncInlineRange();
} }
abstract class BaseRichTextCell extends BaseCellRenderer<Text> { export class RichTextCell extends BaseCellRenderer<Text> {
static override styles = css` inlineEditor$ = computed(() => {
affine-database-rich-text-cell, return this.richText$.value?.inlineEditor;
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;
}
get inlineManager() { get inlineManager() {
return this.view return this.view
@@ -146,57 +98,11 @@ abstract class BaseRichTextCell extends BaseCellRenderer<Text> {
return databaseBlock?.topContenteditableElement; return databaseBlock?.topContenteditableElement;
} }
get attributeRenderer() {
return this.inlineManager?.getRenderer();
}
get attributesSchema() {
return this.inlineManager?.getSchema();
}
get host() { get host() {
return this.view.contextGet(HostContextKey); return this.view.contextGet(HostContextKey);
} }
@query('rich-text') private readonly richText$ = signal<RichText>();
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 changeUserSelectAccordToReadOnly() { private changeUserSelectAccordToReadOnly() {
if (this && this instanceof HTMLElement) { 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) => { private readonly _handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape') { if (event.key !== 'Escape') {
if (event.key === 'Tab') { if (event.key === 'Tab') {
@@ -280,7 +131,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
return; return;
} }
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return; if (!inlineEditor) return;
switch (event.key) { switch (event.key) {
@@ -331,17 +182,17 @@ export class RichTextCellEditing extends BaseRichTextCell {
private readonly _initYText = (text?: string) => { private readonly _initYText = (text?: string) => {
const yText = new Text(text); const yText = new Text(text);
this.onChange(yText); this.valueSetImmediate(yText);
}; };
private readonly _onSoftEnter = () => { private readonly _onSoftEnter = () => {
if (this.value && this.inlineEditor) { if (this.value && this.inlineEditor$.value) {
const inlineRange = this.inlineEditor.getInlineRange(); const inlineRange = this.inlineEditor$.value.getInlineRange();
if (!inlineRange) return; 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'); text.replace(inlineRange.index, inlineRange.length, '\n');
this.inlineEditor.setInlineRange({ this.inlineEditor$.value.setInlineRange({
index: inlineRange.index + 1, index: inlineRange.index + 1,
length: 0, length: 0,
}); });
@@ -349,7 +200,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
}; };
private readonly _onCopy = (e: ClipboardEvent) => { private readonly _onCopy = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return; if (!inlineEditor) return;
const inlineRange = inlineEditor.getInlineRange(); const inlineRange = inlineEditor.getInlineRange();
@@ -366,7 +217,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
}; };
private readonly _onCut = (e: ClipboardEvent) => { private readonly _onCut = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return; if (!inlineEditor) return;
const inlineRange = inlineEditor.getInlineRange(); const inlineRange = inlineEditor.getInlineRange();
@@ -388,7 +239,9 @@ export class RichTextCellEditing extends BaseRichTextCell {
}; };
private readonly _onPaste = (e: ClipboardEvent) => { private readonly _onPaste = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor; e.preventDefault();
e.stopPropagation();
const inlineEditor = this.inlineEditor$.value;
if (!inlineEditor) return; if (!inlineEditor) return;
const inlineRange = inlineEditor.getInlineRange(); const inlineRange = inlineEditor.getInlineRange();
@@ -419,8 +272,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
?.getData('text/plain') ?.getData('text/plain')
?.replace(/\r?\n|\r/g, '\n'); ?.replace(/\r?\n|\r/g, '\n');
if (!text) return; if (!text) return;
e.preventDefault();
e.stopPropagation();
if (isValidUrl(text)) { if (isValidUrl(text)) {
const std = this.std; const std = this.std;
const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text); const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
@@ -459,6 +311,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
}); });
} }
} else { } else {
console.log(text);
inlineEditor.insertText(inlineRange, text); inlineEditor.insertText(inlineRange, text);
inlineEditor.setInlineRange({ inlineEditor.setInlineRange({
index: inlineRange.index + text.length, index: inlineRange.index + text.length,
@@ -469,67 +322,78 @@ export class RichTextCellEditing extends BaseRichTextCell {
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (!this.value || typeof this.value === 'string') { this.classList.add(richTextCellStyle);
this._initYText(this.value);
} this.changeUserSelectAccordToReadOnly();
const selectAll = (e: KeyboardEvent) => { const selectAll = (e: KeyboardEvent) => {
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) { if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.inlineEditor?.selectAll(); this.inlineEditor$.value?.selectAll();
} }
}; };
this.addEventListener('keydown', selectAll); this.addEventListener('keydown', selectAll);
this.disposables.addFromEvent(this, '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() { override beforeEnterEditMode() {
this.richText?.updateComplete if (!this.value || typeof this.value === 'string') {
.then(() => { this._initYText(this.value);
const inlineEditor = this.inlineEditor; }
if (!inlineEditor) return; return true;
}
this.disposables.add( override afterEnterEditingMode() {
inlineEditor.slots.keydown.on(this._handleKeyDown) this.inlineEditor$.value?.focusEnd();
);
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 render() { 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-disable-ask-ai
data-not-block-text data-not-block-text
.yText=${this.value} .yText="${this.value}"
.inlineEventSource=${this.topContenteditableElement} .inlineEventSource="${this.topContenteditableElement}"
.attributesSchema=${this.attributesSchema} .attributesSchema="${this.inlineManager?.getSchema()}"
.attributeRenderer=${this.attributeRenderer} .attributeRenderer="${this.inlineManager?.getRenderer()}"
.embedChecker=${this.inlineManager?.embedChecker} .embedChecker="${this.inlineManager?.embedChecker}"
.markdownMatches=${this.inlineManager?.markdownMatches} .markdownMatches="${this.inlineManager?.markdownMatches}"
.verticalScrollContainerGetter=${() => .readonly="${!this.isEditing$.value || this.readonly}"
.verticalScrollContainerGetter="${() =>
this.topContenteditableElement?.host this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host) ? getViewportElement(this.topContenteditableElement.host)
: null} : null}"
class="affine-database-rich-text inline-editor" class="${richTextContainerStyle} inline-editor"
></rich-text>`; ></rich-text>`;
} }
@@ -538,7 +402,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
} }
insertDelta = (delta: DeltaInsert<AffineTextAttributes>) => { insertDelta = (delta: DeltaInsert<AffineTextAttributes>) => {
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor$.value;
const range = inlineEditor?.getInlineRange(); const range = inlineEditor?.getInlineRange();
if (!range || !delta.insert) { if (!range || !delta.insert) {
return; return;
@@ -553,7 +417,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'affine-database-rich-text-cell-editing': RichTextCellEditing; 'affine-database-rich-text-cell': RichTextCell;
} }
} }
@@ -563,6 +427,5 @@ export const richTextColumnConfig =
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(RichTextCell), view: createFromBaseCellRenderer(RichTextCell),
edit: createFromBaseCellRenderer(RichTextCellEditing),
}, },
}); });
@@ -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)',
});
@@ -7,7 +7,7 @@ import {
import { TableSingleView } from '@blocksuite/data-view/view-presets'; import { TableSingleView } from '@blocksuite/data-view/view-presets';
import { titlePropertyModelConfig } from './define.js'; import { titlePropertyModelConfig } from './define.js';
import { HeaderAreaTextCell, HeaderAreaTextCellEditing } from './text.js'; import { HeaderAreaTextCell } from './text.js';
export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({ export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({
icon: createIcon('TitleIcon'), icon: createIcon('TitleIcon'),
@@ -19,12 +19,5 @@ export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({
showIcon: props.cell.view instanceof TableSingleView, showIcon: props.cell.view instanceof TableSingleView,
}) })
), ),
edit: uniMap(
createFromBaseCellRenderer(HeaderAreaTextCellEditing),
(props: CellRenderProps) => ({
...props,
showIcon: props.cell.view instanceof TableSingleView,
})
),
}, },
}); });
@@ -1,4 +1,3 @@
import type { RootBlockModel } from '@blocksuite/affine-model';
import { import {
DefaultInlineManagerExtension, DefaultInlineManagerExtension,
type RichText, type RichText,
@@ -16,106 +15,31 @@ import { IS_MAC } from '@blocksuite/global/env';
import { LinkedPageIcon } from '@blocksuite/icons/lit'; import { LinkedPageIcon } from '@blocksuite/icons/lit';
import type { DeltaInsert } from '@blocksuite/inline'; import type { DeltaInsert } from '@blocksuite/inline';
import type { BlockSnapshot, Text } from '@blocksuite/store'; import type { BlockSnapshot, Text } from '@blocksuite/store';
import { computed, effect, signal } from '@preact/signals-core'; import { signal } from '@preact/signals-core';
import { css, type TemplateResult } from 'lit'; import { property } from 'lit/decorators.js';
import { property, query } from 'lit/decorators.js'; import { createRef, ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import { HostContextKey } from '../../context/host-context.js'; import { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js'; import type { DatabaseBlockComponent } from '../../database-block.js';
import { getSingleDocIdFromText } from '../../utils/title-doc.js'; import { getSingleDocIdFromText } from '../../utils/title-doc.js';
import {
headerAreaIconStyle,
titleCellStyle,
titleRichTextStyle,
} from './cell-renderer.css.js';
const styles = css` export class HeaderAreaTextCell extends BaseCellRenderer<Text> {
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;
activity = true; activity = true;
docId$ = signal<string>(); 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() { get host() {
return this.view.contextGet(HostContextKey); return this.view.contextGet(HostContextKey);
} }
get inlineEditor() { get inlineEditor() {
return this.richText.inlineEditor; return this.richText.value?.inlineEditor;
} }
get inlineManager() { get inlineManager() {
@@ -128,80 +52,10 @@ abstract class BaseTextCell extends BaseCellRenderer<Text> {
return databaseBlock?.topContenteditableElement; return databaseBlock?.topContenteditableElement;
} }
override connectedCallback() { get std() {
super.connectedCallback(); return this.view.contextGet(HostContextKey)?.std;
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);
});
}
}
} }
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) => { private readonly _onCopy = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor;
if (!inlineEditor) return; if (!inlineEditor) return;
@@ -318,8 +172,6 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
} }
}; };
override activity = false;
insertDelta = (delta: DeltaInsert) => { insertDelta = (delta: DeltaInsert) => {
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor;
const range = inlineEditor?.getInlineRange(); const range = inlineEditor?.getInlineRange();
@@ -333,12 +185,25 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
}); });
}; };
private get std() {
return this.host?.std;
}
override connectedCallback() { override connectedCallback() {
super.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) => { const selectAll = (e: KeyboardEvent) => {
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) { if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
e.stopPropagation(); e.stopPropagation();
@@ -346,82 +211,86 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
this.inlineEditor?.selectAll(); this.inlineEditor?.selectAll();
} }
}; };
this.addEventListener('keydown', selectAll); this.addEventListener('keydown', selectAll);
this.disposables.add(() => { this.disposables.addFromEvent(this, 'keydown', selectAll);
this.removeEventListener('keydown', selectAll);
});
} }
override firstUpdated(props: Map<string, unknown>) { override firstUpdated(props: Map<string, unknown>) {
super.firstUpdated(props); super.firstUpdated(props);
if (!this.isLinkedDoc$.value) { this.richText.value?.updateComplete
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
.then(() => { .then(() => {
this.inlineEditor?.focusEnd(); this.disposables.addFromEvent(
this.richText.value,
this.disposables.add( 'copy',
effect(() => { this._onCopy
const inlineRange = this.inlineEditor?.inlineRange$.value; );
if (inlineRange) { this.disposables.addFromEvent(this.richText.value, 'cut', this._onCut);
if (!this.isEditing) { this.disposables.addFromEvent(
this.selectCurrentCell(true); this.richText.value,
} 'paste',
} else { this._onPaste
if (this.isEditing) {
this.selectCurrentCell(false);
}
}
})
); );
}) })
.catch(console.error); .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 return html` <rich-text
${ref(this.richText)}
data-disable-ask-ai data-disable-ask-ai
data-not-block-text data-not-block-text
.yText="${this.value}" .yText="${this.value}"
.inlineEventSource="${this.topContenteditableElement}" .inlineEventSource="${this.topContenteditableElement}"
.attributesSchema="${this.attributesSchema}" .attributesSchema="${this.inlineManager?.getSchema()}"
.attributeRenderer="${this.attributeRenderer}" .attributeRenderer="${this.inlineManager?.getRenderer()}"
.embedChecker="${this.inlineManager?.embedChecker}" .embedChecker="${this.inlineManager?.embedChecker}"
.markdownMatches="${this.inlineManager?.markdownMatches}" .markdownMatches="${this.inlineManager?.markdownMatches}"
.readonly="${this.readonly}" .readonly="${!this.isEditing$.value}"
.enableClipboard="${false}" .enableClipboard="${false}"
.verticalScrollContainerGetter="${() => .verticalScrollContainerGetter="${() =>
this.topContenteditableElement?.host this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host) ? getViewportElement(this.topContenteditableElement.host)
: null}" : null}"
data-parent-flavour="affine:database" data-parent-flavour="affine:database"
class="data-view-header-area-rich-text" class="${titleRichTextStyle}"
></rich-text>`; ></rich-text>`;
} }
override renderLinkedDoc(): TemplateResult { renderIcon() {
return html` <rich-text if (!this.showIcon) {
data-disable-ask-ai return;
data-not-block-text }
.yText="${this.linkedDocTitle$.value}" if (this.docId$.value) {
.inlineEventSource="${this.topContenteditableElement}" return html` <div class="${headerAreaIconStyle}">
.readonly="${this.readonly}" ${LinkedPageIcon({})}
.enableClipboard="${true}" </div>`;
.verticalScrollContainerGetter="${() => }
this.topContenteditableElement?.host const iconColumn = this.view.mainProperties$.value.iconColumn;
? getViewportElement(this.topContenteditableElement.host) if (!iconColumn) return;
: null}"
class="data-view-header-area-rich-text" const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string;
></rich-text>`; if (!icon) return;
return html` <div class="${headerAreaIconStyle}">${icon}</div>`;
} }
private readonly richText = createRef<RichText>();
@property({ attribute: false })
accessor showIcon = false;
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'data-view-header-area-text': HeaderAreaTextCell; 'data-view-header-area-text': HeaderAreaTextCell;
'data-view-header-area-text-editing': HeaderAreaTextCellEditing;
} }
} }
+1
View File
@@ -25,6 +25,7 @@
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12", "@toeverything/theme": "^1.1.12",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@vanilla-extract/css": "^1.17.0",
"date-fns": "^4.0.0", "date-fns": "^4.0.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -20,8 +20,9 @@ import { flip, offset, shift } from '@floating-ui/dom';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { nothing } from 'lit'; import { nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
@@ -37,7 +38,22 @@ import {
import { verticalListSortingStrategy } from '../../utils/wc-dnd/sort/strategies/index.js'; import { verticalListSortingStrategy } from '../../utils/wc-dnd/sort/strategies/index.js';
import { arrayMove } from '../../utils/wc-dnd/utils/array-move.js'; import { arrayMove } from '../../utils/wc-dnd/utils/array-move.js';
import { getTagColor, selectOptionColors } from './colors.js'; import { getTagColor, selectOptionColors } from './colors.js';
import { styles } from './styles.js'; import {
selectedStyle,
selectOptionContentStyle,
selectOptionDragHandlerStyle,
selectOptionIconStyle,
selectOptionNewIconStyle,
selectOptionsContainerStyle,
selectOptionsTipsStyle,
selectOptionStyle,
tagContainerStyle,
tagDeleteIconStyle,
tagSelectContainerStyle,
tagSelectInputContainerStyle,
tagSelectInputStyle,
tagTextStyle,
} from './styles.css.js';
type RenderOption = { type RenderOption = {
value: string; value: string;
@@ -70,23 +86,23 @@ class TagManager {
); );
}; };
color = signal(getTagColor()); color$ = signal(getTagColor());
createOption = () => { createOption = () => {
const value = this.text.value.trim(); const value = this.text$.value.trim();
if (value === '') return; if (value === '') return;
const id = nanoid(); const id = nanoid();
this.ops.onOptionsChange([ this.ops.onOptionsChange([
{ {
id: id, id: id,
value: value, value: value,
color: this.color.value, color: this.color$.value,
}, },
...this.ops.options.value, ...this.ops.options.value,
]); ]);
this.selectTag(id); this.selectTag(id);
this.text.value = ''; this.text$.value = '';
this.color.value = getTagColor(); this.color$.value = getTagColor();
if (this.isSingleMode) { if (this.isSingleMode) {
this.ops.onComplete?.(); this.ops.onComplete?.();
} }
@@ -101,12 +117,12 @@ class TagManager {
filteredOptions$ = computed(() => { filteredOptions$ = computed(() => {
let matched = false; let matched = false;
const options: RenderOption[] = []; const options: RenderOption[] = [];
for (const option of this.options.value) { for (const option of this.options$.value) {
if ( if (
!this.text.value || !this.text$.value ||
option.value option.value
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(this.text.value.toLocaleLowerCase()) .includes(this.text$.value.toLocaleLowerCase())
) { ) {
options.push({ options.push({
...option, ...option,
@@ -114,15 +130,15 @@ class TagManager {
select: () => this.selectTag(option.id), select: () => this.selectTag(option.id),
}); });
} }
if (option.value === this.text.value) { if (option.value === this.text$.value) {
matched = true; matched = true;
} }
} }
if (this.text.value && !matched) { if (this.text$.value && !matched) {
options.push({ options.push({
id: 'create', id: 'create',
color: this.color.value, color: this.color$.value,
value: this.text.value, value: this.text$.value,
isCreate: true, isCreate: true,
select: this.createOption, select: this.createOption,
}); });
@@ -136,37 +152,37 @@ class TagManager {
); );
}); });
text = signal(''); text$ = signal('');
get isSingleMode() { get isSingleMode() {
return this.ops.mode === 'single'; return this.ops.mode === 'single';
} }
get options() { get options$() {
return this.ops.options; return this.ops.options;
} }
get value() { get value$() {
return this.ops.value; return this.ops.value;
} }
constructor(private readonly ops: TagManagerOptions) {} constructor(private readonly ops: TagManagerOptions) {}
deleteTag(id: string) { deleteTag(id: string) {
this.ops.onChange(this.value.value.filter(item => item !== id)); this.ops.onChange(this.value$.value.filter(item => item !== id));
} }
isSelected(id: string) { isSelected(id: string) {
return this.value.value.includes(id); return this.value$.value.includes(id);
} }
selectTag(id: string) { selectTag(id: string) {
if (this.isSelected(id)) { if (this.isSelected(id)) {
return; return;
} }
const newValue = this.isSingleMode ? [id] : [...this.value.value, id]; const newValue = this.isSingleMode ? [id] : [...this.value$.value, id];
this.ops.onChange(newValue); this.ops.onChange(newValue);
this.text.value = ''; this.text$.value = '';
if (this.isSingleMode) { if (this.isSingleMode) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.ops.onComplete?.(); this.ops.onComplete?.();
@@ -178,8 +194,6 @@ class TagManager {
export class MultiTagSelect extends SignalWatcher( export class MultiTagSelect extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
static override styles = styles;
private readonly _clickItemOption = (e: MouseEvent, id: string) => { private readonly _clickItemOption = (e: MouseEvent, id: string) => {
e.stopPropagation(); e.stopPropagation();
const option = this.options.value.find(v => v.id === id); const option = this.options.value.find(v => v.id === id);
@@ -236,7 +250,7 @@ export class MultiTagSelect extends SignalWatcher(
}; };
private readonly _onInput = (event: KeyboardEvent) => { private readonly _onInput = (event: KeyboardEvent) => {
this.tagManager.text.value = (event.target as HTMLInputElement).value; this.tagManager.text$.value = (event.target as HTMLInputElement).value;
}; };
private readonly _onInputKeydown = (event: KeyboardEvent) => { private readonly _onInputKeydown = (event: KeyboardEvent) => {
@@ -251,10 +265,10 @@ export class MultiTagSelect extends SignalWatcher(
this.selectedTag$.value?.select(); this.selectedTag$.value?.select();
} else if (event.key === 'ArrowUp') { } else if (event.key === 'ArrowUp') {
event.preventDefault(); event.preventDefault();
this.setSelectedOption(this.selectedIndex - 1); this.setSelectedOption(this.selectedIndex$.value - 1);
} else if (event.key === 'ArrowDown') { } else if (event.key === 'ArrowDown') {
event.preventDefault(); event.preventDefault();
this.setSelectedOption(this.selectedIndex + 1); this.setSelectedOption(this.selectedIndex$.value + 1);
} else if (event.key === 'Escape') { } else if (event.key === 'Escape') {
this.onComplete(); this.onComplete();
} }
@@ -263,7 +277,7 @@ export class MultiTagSelect extends SignalWatcher(
private readonly tagManager = new TagManager(this); private readonly tagManager = new TagManager(this);
private readonly selectedTag$ = computed(() => { private readonly selectedTag$ = computed(() => {
return this.tagManager.filteredOptions$.value[this.selectedIndex]; return this.tagManager.filteredOptions$.value[this.selectedIndex$.value];
}); });
sortContext = createSortContext({ sortContext = createSortContext({
@@ -301,12 +315,12 @@ export class MultiTagSelect extends SignalWatcher(
}); });
private get text() { private get text() {
return this.tagManager.text; return this.tagManager.text$;
} }
private renderInput() { private renderInput() {
return html` return html`
<div class="tag-select-input-container"> <div class="${tagSelectInputContainerStyle}">
${this.value.value.map(id => { ${this.value.value.map(id => {
const option = this.tagManager.optionsMap$.value.get(id); const option = this.tagManager.optionsMap$.value.get(id);
if (!option) { if (!option) {
@@ -317,7 +331,8 @@ export class MultiTagSelect extends SignalWatcher(
); );
})} })}
<input <input
class="tag-select-input" class="${tagSelectInputStyle}"
${ref(this._selectInput)}
placeholder="Type here..." placeholder="Type here..."
.value="${this.text.value}" .value="${this.text.value}"
@input="${this._onInput}" @input="${this._onInput}"
@@ -332,10 +347,10 @@ export class MultiTagSelect extends SignalWatcher(
const style = styleMap({ const style = styleMap({
backgroundColor: color, backgroundColor: color,
}); });
return html` <div class="tag-container" style=${style}> return html` <div class="${tagContainerStyle}" style=${style}>
<div class="tag-text">${name}</div> <div data-testid="tag-name" class="${tagTextStyle}">${name}</div>
${onDelete ${onDelete
? html` <div class="tag-delete-icon" @click="${onDelete}"> ? html` <div class="${tagDeleteIconStyle}" @click="${onDelete}">
${CloseIcon()} ${CloseIcon()}
</div>` </div>`
: nothing} : nothing}
@@ -349,19 +364,19 @@ export class MultiTagSelect extends SignalWatcher(
'layer/insideBorder/border' 'layer/insideBorder/border'
)};margin: 4px 0;" )};margin: 4px 0;"
></div> ></div>
<div class="select-options-tips">Select tag or create one</div> <div class="${selectOptionsTipsStyle}">Select tag or create one</div>
<div class="select-options-container"> <div data-testid="tag-option-list" class="${selectOptionsContainerStyle}">
${repeat( ${repeat(
this.tagManager.filteredOptions$.value, this.tagManager.filteredOptions$.value,
select => select.id, select => select.id,
(select, index) => { (select, index) => {
const isSelected = index === this.selectedIndex; const isSelected = index === this.selectedIndex$.value;
const mouseenter = () => { const mouseenter = () => {
this.setSelectedOption(index); this.setSelectedOption(index);
}; };
const classes = classMap({ const classes = classMap({
'select-option': true, [selectOptionStyle]: true,
selected: isSelected, [selectedStyle]: isSelected,
}); });
const clickOption = (e: MouseEvent) => { const clickOption = (e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
@@ -374,21 +389,24 @@ export class MultiTagSelect extends SignalWatcher(
@mouseenter="${mouseenter}" @mouseenter="${mouseenter}"
@click="${select.select}" @click="${select.select}"
> >
<div class="select-option-content"> <div class="${selectOptionContentStyle}">
${select.isCreate ${select.isCreate
? html` <div class="select-option-new-icon">Create</div>` ? html` <div class="${selectOptionNewIconStyle}">
Create
</div>`
: html` : html`
<div <div
${dragHandler(select.id)} ${dragHandler(select.id)}
class="select-option-drag-handler" class="${selectOptionDragHandlerStyle}"
></div> ></div>
`} `}
${this.renderTag(select.value, select.color)} ${this.renderTag(select.value, select.color)}
</div> </div>
${!select.isCreate ${!select.isCreate
? html` <div ? html` <div
class="select-option-icon" class="${selectOptionIconStyle}"
@click="${clickOption}" @click="${clickOption}"
data-testid="option-more"
> >
${MoreHorizontalIcon()} ${MoreHorizontalIcon()}
</div>` </div>`
@@ -402,7 +420,7 @@ export class MultiTagSelect extends SignalWatcher(
} }
private setSelectedOption(index: number) { private setSelectedOption(index: number) {
this.selectedIndex = rangeWrap( this.selectedIndex$.value = rangeWrap(
index, index,
0, 0,
this.tagManager.filteredOptions$.value.length this.tagManager.filteredOptions$.value.length
@@ -410,28 +428,29 @@ export class MultiTagSelect extends SignalWatcher(
} }
protected override firstUpdated() { protected override firstUpdated() {
const disposables = this.disposables;
this.classList.add(tagSelectContainerStyle);
requestAnimationFrame(() => { requestAnimationFrame(() => {
this._selectInput.focus(); this._selectInput.value?.focus();
}); });
this._disposables.addFromEvent(this, 'click', () => { disposables.addFromEvent(this, 'click', () => {
this._selectInput.focus(); this._selectInput.value?.focus();
}); });
this._disposables.addFromEvent(this._selectInput, 'copy', e => { disposables.addFromEvent(this._selectInput.value, 'copy', e => {
e.stopPropagation(); e.stopPropagation();
}); });
this._disposables.addFromEvent(this._selectInput, 'cut', e => { disposables.addFromEvent(this._selectInput.value, 'cut', e => {
e.stopPropagation(); e.stopPropagation();
}); });
} }
override render() { override render() {
this.setSelectedOption(this.selectedIndex); this.setSelectedOption(this.selectedIndex$.value);
return html` ${this.renderInput()} ${this.renderTags()} `; return html` ${this.renderInput()} ${this.renderTags()} `;
} }
@query('.tag-select-input') private readonly _selectInput = createRef<HTMLInputElement>();
private accessor _selectInput!: HTMLInputElement;
@property() @property()
accessor mode: 'multi' | 'single' = 'multi'; accessor mode: 'multi' | 'single' = 'multi';
@@ -448,8 +467,7 @@ export class MultiTagSelect extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor options!: ReadonlySignal<SelectTag[]>; accessor options!: ReadonlySignal<SelectTag[]>;
@state() private readonly selectedIndex$ = signal(0);
private accessor selectedIndex = 0;
@property({ attribute: false }) @property({ attribute: false })
accessor value!: ReadonlySignal<string[]>; accessor value!: ReadonlySignal<string[]>;
@@ -464,7 +482,7 @@ declare global {
const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => { const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
const tagManager = new TagManager(ops); const tagManager = new TagManager(ops);
const onInput = (e: InputEvent) => { const onInput = (e: InputEvent) => {
tagManager.text.value = (e.target as HTMLInputElement).value; tagManager.text$.value = (e.target as HTMLInputElement).value;
}; };
return popMenu(target, { return popMenu(target, {
options: { options: {
@@ -491,12 +509,12 @@ const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
backgroundColor: option.color, backgroundColor: option.color,
width: 'max-content', width: 'max-content',
}); });
return html` <div class="tag-container" style=${style}> return html` <div class="${tagContainerStyle}" style=${style}>
<div class="tag-text">${option.value}</div> <div class="${tagTextStyle}">${option.value}</div>
</div>`; </div>`;
})} })}
<input <input
.value="${tagManager.text.value}" .value="${tagManager.text$.value}"
@input="${onInput}" @input="${onInput}"
placeholder="Type here..." placeholder="Type here..."
type="text" type="text"
@@ -522,8 +540,8 @@ const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
${option.isCreate ${option.isCreate
? html` <div style="margin-right: 8px;">Create</div>` ? html` <div style="margin-right: 8px;">Create</div>`
: ''} : ''}
<div class="tag-container" style=${style}> <div class="${tagContainerStyle}" style=${style}>
<div class="tag-text">${option.value}</div> <div class="${tagTextStyle}">${option.value}</div>
</div> </div>
</div> </div>
`; `;
@@ -60,7 +60,10 @@ export class MultiTagView extends WithDisposable(ShadowlessElement) {
const style = styleMap({ const style = styleMap({
backgroundColor: getColorByColor(option.color), backgroundColor: getColorByColor(option.color),
}); });
return html`<span class="select-selected" style=${style} return html`<span
data-testid="tag-selected"
class="select-selected"
style=${style}
>${option.value}</span >${option.value}</span
>`; >`;
})} })}
@@ -0,0 +1,147 @@
import { baseTheme } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const tagSelectContainerStyle = style({
position: 'absolute',
zIndex: 2,
color: cssVarV2('text/primary'),
border: `0.5px solid ${cssVarV2('layer/insideBorder/blackBorder')}`,
borderRadius: '8px',
background: cssVarV2('layer/background/primary'),
boxShadow: 'var(--affine-shadow-1)',
fontFamily: 'var(--affine-font-family)',
maxWidth: '400px',
padding: '8px',
display: 'flex',
flexDirection: 'column',
gap: '4px',
'@media': {
print: {
display: 'none',
},
},
});
export const tagSelectInputContainerStyle = style({
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: '6px',
padding: '4px',
});
export const tagSelectInputStyle = style({
flex: '1 1 0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
color: cssVarV2('text/primary'),
backgroundColor: 'transparent',
lineHeight: '22px',
fontSize: '14px',
outline: 'none',
'::placeholder': {
color: 'var(--affine-placeholder-color)',
},
});
export const selectOptionsTipsStyle = style({
padding: '4px',
color: cssVarV2('text/secondary'),
fontSize: '14px',
fontWeight: 500,
lineHeight: '22px',
userSelect: 'none',
});
export const selectOptionsContainerStyle = style({
maxHeight: '400px',
overflowY: 'auto',
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
gap: '4px',
});
export const selectOptionStyle = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '4px 4px 4px 0',
borderRadius: '4px',
cursor: 'pointer',
});
export const selectedStyle = style({
background: cssVarV2('layer/background/hoverOverlay'),
});
export const tagContainerStyle = style({
display: 'flex',
alignItems: 'center',
padding: '0 8px',
gap: '4px',
borderRadius: '4px',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
border: `1px solid ${cssVarV2('database/border')}`,
userSelect: 'none',
});
export const tagTextStyle = style({
fontSize: '14px',
lineHeight: '22px',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
export const tagDeleteIconStyle = style({
display: 'flex',
alignItems: 'center',
color: cssVarV2('chip/label/text'),
});
export const selectOptionContentStyle = style({
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
});
export const selectOptionIconStyle = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '20px',
borderRadius: '4px',
cursor: 'pointer',
visibility: 'hidden',
color: cssVarV2('icon/primary'),
marginLeft: '4px',
':hover': {
background: cssVarV2('layer/background/hoverOverlay'),
},
selectors: {
[`${selectedStyle} &`]: {
visibility: 'visible',
},
},
});
export const selectOptionDragHandlerStyle = style({
width: '4px',
height: '12px',
borderRadius: '1px',
backgroundColor: cssVarV2('button/grabber/default'),
marginRight: '4px',
cursor: '-webkit-grab',
flexShrink: 0,
});
export const selectOptionNewIconStyle = style({
fontSize: '14px',
lineHeight: '22px',
color: cssVarV2('text/primary'),
marginRight: '8px',
marginLeft: '4px',
});
@@ -1,251 +0,0 @@
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { baseTheme } from '@toeverything/theme';
import { css, unsafeCSS } from 'lit';
export const styles = css`
affine-multi-tag-select {
position: absolute;
z-index: 2;
color: ${unsafeCSSVarV2('text/primary')};
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/blackBorder')};
border-radius: 8px;
background: ${unsafeCSSVarV2('layer/background/primary')};
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
font-family: var(--affine-font-family);
max-width: 400px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
@media print {
affine-multi-tag-select {
display: none;
}
}
.tag-select-input-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 4px;
}
.tag-select-input {
flex: 1 1 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
color: ${unsafeCSSVarV2('text/primary')};
background-color: transparent;
line-height: 22px;
font-size: 14px;
outline: none;
}
.tag-select-input::placeholder {
color: var(--affine-placeholder-color);
}
.select-options-tips {
padding: 4px;
color: ${unsafeCSSVarV2('text/secondary')};
font-size: 14px;
font-weight: 500;
line-height: 22px;
user-select: none;
}
.select-options-container {
max-height: 400px;
overflow-y: auto;
user-select: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.select-option {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 4px 4px 0;
border-radius: 4px;
cursor: pointer;
}
.tag-container {
display: flex;
align-items: center;
padding: 0 8px;
gap: 4px;
border-radius: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
border: 1px solid ${unsafeCSSVarV2('database/border')};
user-select: none;
}
.tag-text {
font-size: 14px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-delete-icon {
display: flex;
align-items: center;
color: ${unsafeCSSVarV2('chip/label/text')};
}
.select-option.selected {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.select-option-content {
display: flex;
align-items: center;
overflow: hidden;
}
.select-option-icon {
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
border-radius: 4px;
cursor: pointer;
visibility: hidden;
color: ${unsafeCSSVarV2('icon/primary')};
margin-left: 4px;
}
.select-option.selected .select-option-icon {
visibility: visible;
}
.select-option-icon:hover {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.select-option-drag-handler {
width: 4px;
height: 12px;
border-radius: 1px;
background-color: ${unsafeCSSVarV2('button/grabber/default')};
margin-right: 4px;
cursor: -webkit-grab;
flex-shrink: 0;
}
.select-option-new-icon {
font-size: 14px;
line-height: 22px;
color: ${unsafeCSSVarV2('text/primary')};
margin-right: 8px;
margin-left: 4px;
}
// .select-selected-text {
// width: calc(100% - 16px);
// white-space: nowrap;
// text-overflow: ellipsis;
// overflow: hidden;
// }
//
// .select-selected > .close-icon {
// display: flex;
// align-items: center;
// }
//
// .select-selected > .close-icon:hover {
// cursor: pointer;
// }
//
// .select-selected > .close-icon > svg {
// fill: var(--affine-black-90);
// }
//
// .select-option-new {
// display: flex;
// flex-direction: row;
// align-items: center;
// height: 36px;
// padding: 4px;
// gap: 5px;
// border-radius: 4px;
// background: var(--affine-selected-color);
// }
//
// .select-option-new-text {
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
// height: 28px;
// padding: 2px 10px;
// border-radius: 4px;
// background: var(--affine-tag-red);
// }
//
// .select-option-new-icon {
// display: flex;
// align-items: center;
// gap: 6px;
// height: 28px;
// color: var(--affine-text-primary-color);
// margin-right: 8px;
// }
//
// .select-option-new-icon svg {
// width: 16px;
// height: 16px;
// }
//
// .select-option {
// position: relative;
// display: flex;
// justify-content: space-between;
// align-items: center;
// padding: 4px;
// border-radius: 4px;
// margin-bottom: 4px;
// cursor: pointer;
// }
//
// .select-option.selected {
// background: var(--affine-hover-color);
// }
//
// .select-option-text-container {
// width: 100%;
// overflow: hidden;
// display: flex;
// }
//
// .select-option-group-name {
// font-size: 9px;
// padding: 0 2px;
// border-radius: 2px;
// }
//
// .select-option-name {
// padding: 4px 8px;
// border-radius: 4px;
// white-space: nowrap;
// text-overflow: ellipsis;
// overflow: hidden;
// }
//
//
// .select-option-icon:hover {
// background: var(--affine-hover-color);
// }
//
// .select-option-icon svg {
// width: 16px;
// height: 16px;
// pointer-events: none;
// }
`;
@@ -237,18 +237,18 @@ export class RecordField extends SignalWatcher(
const props: CellRenderProps = { const props: CellRenderProps = {
cell: this.cell$.value, cell: this.cell$.value,
isEditing: this.editing, isEditing$: this.isEditing$,
selectCurrentCell: this.changeEditing, selectCurrentCell: this.changeEditing,
}; };
const renderer = this.column.renderer$.value; const renderer = this.column.renderer$.value;
if (!renderer) { if (!renderer) {
return; return;
} }
const { view, edit } = renderer; const { view } = renderer;
const contentClass = classMap({ const contentClass = classMap({
'field-content': true, 'field-content': true,
empty: !this.editing && this.cell$.value.isEmpty$.value, empty: !this.isEditing$.value && this.cell$.value.isEmpty$.value,
'is-editing': this.editing, 'is-editing': this.isEditing$.value,
'is-focus': this.isFocus, 'is-focus': this.isFocus,
}); });
return html` return html`
@@ -261,7 +261,7 @@ export class RecordField extends SignalWatcher(
</div> </div>
</div> </div>
<div @click="${this._click}" class="${contentClass}"> <div @click="${this._click}" class="${contentClass}">
${renderUniLit(this.editing && edit ? edit : view, props, { ${renderUniLit(view, props, {
ref: this._cell, ref: this._cell,
class: 'kanban-cell', class: 'kanban-cell',
})} })}
@@ -269,8 +269,7 @@ export class RecordField extends SignalWatcher(
`; `;
} }
@state() isEditing$ = signal(false);
accessor editing = false;
@state() @state()
accessor isFocus = false; accessor isFocus = false;
@@ -61,13 +61,11 @@ export class DetailSelection {
const cell = container.cell; const cell = container.cell;
if (selection.isEditing) { if (selection.isEditing) {
requestAnimationFrame(() => { cell?.beforeExitEditingMode();
cell?.onExitEditMode();
});
if (cell?.blurCell()) { if (cell?.blurCell()) {
container.blur(); container.blur();
} }
container.editing = false; container.isEditing$.value = false;
} else { } else {
container.blur(); container.blur();
} }
@@ -85,11 +83,13 @@ export class DetailSelection {
container.isFocus = true; container.isFocus = true;
const cell = container.cell; const cell = container.cell;
if (selection.isEditing) { if (selection.isEditing) {
cell?.onEnterEditMode();
if (cell?.focusCell()) { if (cell?.focusCell()) {
container.focus(); container.focus();
} }
container.editing = true; container.isEditing$.value = true;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
});
} else { } else {
container.focus(); container.focus();
} }
@@ -1,6 +1,7 @@
import { ShadowlessElement } from '@blocksuite/block-std'; import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed } from '@preact/signals-core'; import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { PropertyValues } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import type { Cell } from '../view-manager/cell.js'; import type { Cell } from '../view-manager/cell.js';
@@ -56,29 +57,37 @@ export abstract class BaseCellRenderer<
return true; return true;
} }
type: string | undefined;
protected override shouldUpdate(_changedProperties: PropertyValues): boolean {
return this.cell.property.type$.value === this.type;
}
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.type = this.cell.property.type$.value;
this.dataset.testid = this.type;
this.style.width = '100%'; this.style.width = '100%';
this._disposables.addFromEvent(this, 'click', e => { this._disposables.addFromEvent(this, 'click', e => {
if (this.isEditing) { if (this.isEditing$.value) {
e.stopPropagation(); e.stopPropagation();
} }
}); });
this._disposables.addFromEvent(this, 'copy', e => { this._disposables.addFromEvent(this, 'copy', e => {
if (!this.isEditing) return; if (!this.isEditing$.value) return;
e.stopPropagation(); e.stopPropagation();
this.onCopy(e); this.onCopy(e);
}); });
this._disposables.addFromEvent(this, 'cut', e => { this._disposables.addFromEvent(this, 'cut', e => {
if (!this.isEditing) return; if (!this.isEditing$.value) return;
e.stopPropagation(); e.stopPropagation();
this.onCut(e); this.onCut(e);
}); });
this._disposables.addFromEvent(this, 'paste', e => { this._disposables.addFromEvent(this, 'paste', e => {
if (!this.isEditing) return; if (!this.isEditing$.value) return;
e.stopPropagation(); e.stopPropagation();
this.onPaste(e); this.onPaste(e);
}); });
@@ -92,26 +101,32 @@ export abstract class BaseCellRenderer<
this.requestUpdate(); this.requestUpdate();
} }
onChange(value: Value | undefined): void { valueSetImmediate(value: Value | undefined): void {
this.cell.valueSet(value); this.cell.valueSet(value);
} }
valueSetNextTick(value: Value | undefined) {
requestAnimationFrame(() => {
this.cell.valueSet(value);
});
}
onCopy(_e: ClipboardEvent) {} onCopy(_e: ClipboardEvent) {}
onCut(_e: ClipboardEvent) {} onCut(_e: ClipboardEvent) {}
onEnterEditMode(): void { afterEnterEditingMode(): void {
// do nothing // do nothing
} }
onExitEditMode() { beforeExitEditingMode() {
// do nothing // do nothing
} }
onPaste(_e: ClipboardEvent) {} onPaste(_e: ClipboardEvent) {}
@property({ attribute: false }) @property({ attribute: false })
accessor isEditing!: boolean; accessor isEditing$!: ReadonlySignal<boolean>;
@property({ attribute: false }) @property({ attribute: false })
accessor selectCurrentCell!: (editing: boolean) => void; accessor selectCurrentCell!: (editing: boolean) => void;
@@ -1,4 +1,5 @@
import type { UniComponent } from '@blocksuite/affine-shared/types'; import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { ReadonlySignal } from '@preact/signals-core';
import type { Cell } from '../view-manager/cell.js'; import type { Cell } from '../view-manager/cell.js';
@@ -7,16 +8,15 @@ export interface CellRenderProps<
Value = unknown, Value = unknown,
> { > {
cell: Cell<Value, Data>; cell: Cell<Value, Data>;
isEditing: boolean; isEditing$: ReadonlySignal<boolean>;
selectCurrentCell: (editing: boolean) => void; selectCurrentCell: (editing: boolean) => void;
} }
export interface DataViewCellLifeCycle { export interface DataViewCellLifeCycle {
beforeEnterEditMode(): boolean; beforeEnterEditMode(): boolean;
beforeExitEditingMode(): void;
onEnterEditMode(): void; afterEnterEditingMode(): void;
onExitEditMode(): void;
focusCell(): boolean; focusCell(): boolean;
@@ -35,5 +35,4 @@ export type CellRenderer<
Value = unknown, Value = unknown,
> = { > = {
view: DataViewCellComponent<Data, Value>; view: DataViewCellComponent<Data, Value>;
edit?: DataViewCellComponent<Data, Value>;
}; };
+6 -42
View File
@@ -14,31 +14,13 @@ import { GroupSetting } from './core/group-by/setting.js';
import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js'; import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js';
import { AnyRender } from './core/utils/uni-component/render-template.js'; import { AnyRender } from './core/utils/uni-component/render-template.js';
import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js'; import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js';
import { import { DateCell } from './property-presets/date/cell-renderer.js';
DateCell,
DateCellEditing,
} from './property-presets/date/cell-renderer.js';
import { TextCell as ImageTextCell } from './property-presets/image/cell-renderer.js'; import { TextCell as ImageTextCell } from './property-presets/image/cell-renderer.js';
import { import { MultiSelectCell } from './property-presets/multi-select/cell-renderer.js';
MultiSelectCell, import { NumberCell } from './property-presets/number/cell-renderer.js';
MultiSelectCellEditing, import { ProgressCell } from './property-presets/progress/cell-renderer.js';
} from './property-presets/multi-select/cell-renderer.js'; import { SelectCell } from './property-presets/select/cell-renderer.js';
import { import { TextCell } from './property-presets/text/cell-renderer.js';
NumberCell,
NumberCellEditing,
} from './property-presets/number/cell-renderer.js';
import {
ProgressCell,
ProgressCellEditing,
} from './property-presets/progress/cell-renderer.js';
import {
SelectCell,
SelectCellEditing,
} from './property-presets/select/cell-renderer.js';
import {
TextCell,
TextCellEditing,
} from './property-presets/text/cell-renderer.js';
import { DataViewKanban, DataViewTable } from './view-presets/index.js'; import { DataViewKanban, DataViewTable } from './view-presets/index.js';
import { MobileKanbanCard } from './view-presets/kanban/mobile/card.js'; import { MobileKanbanCard } from './view-presets/kanban/mobile/card.js';
import { MobileKanbanCell } from './view-presets/kanban/mobile/cell.js'; import { MobileKanbanCell } from './view-presets/kanban/mobile/cell.js';
@@ -83,16 +65,8 @@ import { DataViewHeaderViews } from './widget-presets/views-bar/views-view.js';
export function effects() { export function effects() {
customElements.define('affine-database-progress-cell', ProgressCell); customElements.define('affine-database-progress-cell', ProgressCell);
customElements.define(
'affine-database-progress-cell-editing',
ProgressCellEditing
);
customElements.define('data-view-header-tools', DataViewHeaderTools); customElements.define('data-view-header-tools', DataViewHeaderTools);
customElements.define('affine-database-number-cell', NumberCell); customElements.define('affine-database-number-cell', NumberCell);
customElements.define(
'affine-database-number-cell-editing',
NumberCellEditing
);
customElements.define( customElements.define(
'affine-database-cell-container', 'affine-database-cell-container',
DatabaseCellContainer DatabaseCellContainer
@@ -102,24 +76,14 @@ export function effects() {
customElements.define('any-render', AnyRender); customElements.define('any-render', AnyRender);
customElements.define('affine-database-image-cell', ImageTextCell); customElements.define('affine-database-image-cell', ImageTextCell);
customElements.define('affine-database-date-cell', DateCell); customElements.define('affine-database-date-cell', DateCell);
customElements.define('affine-database-date-cell-editing', DateCellEditing);
customElements.define( customElements.define(
'data-view-properties-setting', 'data-view-properties-setting',
DataViewPropertiesSettingView DataViewPropertiesSettingView
); );
customElements.define('affine-database-checkbox-cell', CheckboxCell); customElements.define('affine-database-checkbox-cell', CheckboxCell);
customElements.define('affine-database-text-cell', TextCell); customElements.define('affine-database-text-cell', TextCell);
customElements.define('affine-database-text-cell-editing', TextCellEditing);
customElements.define('affine-database-select-cell', SelectCell); customElements.define('affine-database-select-cell', SelectCell);
customElements.define(
'affine-database-select-cell-editing',
SelectCellEditing
);
customElements.define('affine-database-multi-select-cell', MultiSelectCell); customElements.define('affine-database-multi-select-cell', MultiSelectCell);
customElements.define(
'affine-database-multi-select-cell-editing',
MultiSelectCellEditing
);
customElements.define('affine-data-view-record-field', RecordField); customElements.define('affine-data-view-record-field', RecordField);
customElements.define('data-view-drag-to-fill', DragToFillElement); customElements.define('data-view-drag-to-fill', DragToFillElement);
customElements.define('affine-data-view-table-group', TableGroup); customElements.define('affine-data-view-table-group', TableGroup);
@@ -75,7 +75,7 @@ export class CheckboxCell extends BaseCellRenderer<boolean> {
override beforeEnterEditMode() { override beforeEnterEditMode() {
const checked = !this.value; const checked = !this.value;
this.onChange(checked); this.valueSetImmediate(checked);
if (checked) { if (checked) {
playCheckAnimation(this._checkbox, { left: 2 }).catch(console.error); playCheckAnimation(this._checkbox, { left: 2 }).catch(console.error);
} }
@@ -0,0 +1,33 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const dateCellStyle = 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)',
height: 'var(--data-view-cell-text-line-height)',
});
export const dateValueContainerStyle = style({
padding: '12px',
backgroundColor: 'var(--layer-background-primary)',
borderRadius: '12px',
color: 'var(--text-secondary)',
fontSize: '17px',
lineHeight: '22px',
height: '46px',
});
export const datePickerContainerStyle = style({
padding: '12px',
backgroundColor: 'var(--layer-background-primary)',
borderRadius: '12px',
});
@@ -4,66 +4,23 @@ import {
} from '@blocksuite/affine-components/context-menu'; } from '@blocksuite/affine-components/context-menu';
import { DatePicker } from '@blocksuite/affine-components/date-picker'; import { DatePicker } from '@blocksuite/affine-components/date-picker';
import { createLitPortal } from '@blocksuite/affine-components/portal'; import { createLitPortal } from '@blocksuite/affine-components/portal';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { IS_MOBILE } from '@blocksuite/global/env'; import { IS_MOBILE } from '@blocksuite/global/env';
import { flip, offset } from '@floating-ui/dom'; import { flip, offset } from '@floating-ui/dom';
import { signal } from '@preact/signals-core'; import { computed, signal } from '@preact/signals-core';
import { baseTheme } from '@toeverything/theme';
import { format } from 'date-fns/format'; import { format } from 'date-fns/format';
import { css, html, unsafeCSS } from 'lit'; import { html } from 'lit';
import { BaseCellRenderer } from '../../core/property/index.js'; import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import {
dateCellStyle,
datePickerContainerStyle,
dateValueContainerStyle,
} from './cell-renderer.css.js';
import { datePropertyModelConfig } from './define.js'; import { datePropertyModelConfig } from './define.js';
export class DateCell extends BaseCellRenderer<number> { export class DateCell extends BaseCellRenderer<number> {
static override styles = css`
affine-database-date-cell {
width: 100%;
}
.affine-database-date {
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);
height: var(--data-view-cell-text-line-height);
}
input.affine-database-date[type='date']::-webkit-calendar-picker-indicator {
display: none;
}
`;
override render() {
const value = this.value ? format(this.value, 'yyyy/MM/dd') : '';
if (!value) {
return '';
}
return html` <div class="affine-database-date date">${value}</div>`;
}
}
export class DateCellEditing extends BaseCellRenderer<number> {
static override styles = css`
affine-database-date-cell-editing {
width: 100%;
cursor: text;
}
.affine-database-date:focus {
outline: none;
}
`;
private _prevPortalAbortController: AbortController | null = null; private _prevPortalAbortController: AbortController | null = null;
private readonly openDatePicker = () => { private readonly openDatePicker = () => {
@@ -96,18 +53,8 @@ export class DateCellEditing extends BaseCellRenderer<number> {
}, },
items: [ items: [
() => () =>
html`<div html` <div class="${dateValueContainerStyle}">
style=" ${this.formattedTempValue$.value}
padding: 12px;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
border-radius: 12px;
color: ${unsafeCSSVarV2('text/secondary')};
font-size: 17px;
line-height: 22px;
height: 46px;
"
>
${this.dateString}
</div>`, </div>`,
() => { () => {
const datePicker = new DatePicker(); const datePicker = new DatePicker();
@@ -123,11 +70,7 @@ height: 46px;
abortController.abort(); abortController.abort();
}; };
requestAnimationFrame(() => datePicker.focusDateCell()); requestAnimationFrame(() => datePicker.focusDateCell());
return html`<div return html` <div class="${datePickerContainerStyle}">
style="padding: 12px;background-color: ${unsafeCSSVarV2(
'layer/background/primary'
)};border-radius: 12px"
>
${datePicker} ${datePicker}
</div>`; </div>`;
}, },
@@ -179,36 +122,39 @@ height: 46px;
return; return;
} }
this.onChange(tempValue?.getTime()); const time = tempValue?.getTime();
this.valueSetNextTick(time);
this.tempValue$.value = undefined; this.tempValue$.value = undefined;
}; };
tempValue$ = signal<Date>(); tempValue$ = signal<Date | undefined>();
get dateString() { format(value?: Date) {
const value = this.tempValue;
return value ? format(value, 'yyyy/MM/dd') : ''; return value ? format(value, 'yyyy/MM/dd') : '';
} }
get tempValue() { formattedTempValue$ = computed(() => {
return this.tempValue$.value; return this.format(this.tempValue$.value);
} });
formattedValue$ = computed(() => {
return (
this.formattedTempValue$.value ||
this.format(this.value ? new Date(this.value) : undefined)
);
});
override firstUpdated() { override afterEnterEditingMode() {
this.openDatePicker(); this.openDatePicker();
} }
override onExitEditMode() { override beforeExitEditingMode() {
this.updateValue(); this.updateValue();
this._prevPortalAbortController?.abort(); this._prevPortalAbortController?.abort();
} }
override render() { override render() {
return html` <div return html` <div class="${dateCellStyle} date">
class="affine-database-date date" ${this.formattedValue$.value}
@click="${this.openDatePicker}"
>
${this.dateString}
</div>`; </div>`;
} }
} }
@@ -217,6 +163,5 @@ export const datePropertyConfig = datePropertyModelConfig.createPropertyMeta({
icon: createIcon('DateTimeIcon'), icon: createIcon('DateTimeIcon'),
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(DateCell), view: createFromBaseCellRenderer(DateCell),
edit: createFromBaseCellRenderer(DateCellEditing),
}, },
}); });
@@ -0,0 +1,17 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const multiSelectStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
height: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
});
@@ -1,53 +1,33 @@
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu'; import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
import { computed, signal } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import { popTagSelect } from '../../core/component/tags/multi-tag-select.js'; import { popTagSelect } from '../../core/component/tags/multi-tag-select.js';
import type { SelectTag } from '../../core/index.js'; import type { SelectTag } from '../../core/index.js';
import { BaseCellRenderer } from '../../core/property/index.js'; import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { stopPropagation } from '../../core/utils/event.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import type { SelectPropertyData } from '../select/define.js'; import type { SelectPropertyData } from '../select/define.js';
import { multiSelectStyle } from './cell-renderer.css.js';
import { multiSelectPropertyModelConfig } from './define.js'; import { multiSelectPropertyModelConfig } from './define.js';
export class MultiSelectCell extends BaseCellRenderer< export class MultiSelectCell extends BaseCellRenderer<
string[], string[],
SelectPropertyData SelectPropertyData
> { > {
override render() { closePopup?: () => void;
return html`
<affine-multi-tag-view
.value="${Array.isArray(this.value) ? this.value : []}"
.options="${this.property.data$.value.options}"
></affine-multi-tag-view>
`;
}
}
export class MultiSelectCellEditing extends BaseCellRenderer<
string[],
SelectPropertyData
> {
private readonly popTagSelect = () => { private readonly popTagSelect = () => {
const value = signal(this._value); this.closePopup = popTagSelect(popupTargetFromElement(this), {
this._disposables.add({ name: this.cell.property.name$.value,
dispose: popTagSelect( options: this.options$,
popupTargetFromElement( onOptionsChange: this._onOptionsChange,
this.querySelector('affine-multi-tag-view') ?? this value: this._value$,
), onChange: v => {
{ this.valueSetImmediate(v);
name: this.cell.property.name$.value, },
options: this.options$, onComplete: this._editComplete,
onOptionsChange: this._onOptionsChange, minWidth: 400,
value: value,
onChange: v => {
this._onChange(v);
value.value = v;
},
onComplete: this._editComplete,
minWidth: 400,
}
),
}); });
}; };
@@ -55,10 +35,6 @@ export class MultiSelectCellEditing extends BaseCellRenderer<
this.selectCurrentCell(false); this.selectCurrentCell(false);
}; };
_onChange = (ids: string[]) => {
this.onChange(ids);
};
_onOptionsChange = (options: SelectTag[]) => { _onOptionsChange = (options: SelectTag[]) => {
this.property.dataUpdate(data => { this.property.dataUpdate(data => {
return { return {
@@ -71,21 +47,32 @@ export class MultiSelectCellEditing extends BaseCellRenderer<
options$ = computed(() => { options$ = computed(() => {
return this.property.data$.value.options; return this.property.data$.value.options;
}); });
_value$ = computed(() => {
get _value() {
return this.value ?? []; return this.value ?? [];
});
override afterEnterEditingMode() {
if (!this.closePopup) {
this.popTagSelect();
}
} }
override firstUpdated() { override beforeExitEditingMode() {
this.popTagSelect(); this.closePopup?.();
this.closePopup = undefined;
} }
override render() { override render() {
return html` return html`
<affine-multi-tag-view <div
.value="${this._value}" class="${multiSelectStyle}"
.options="${this.options$.value}" @pointerdown="${this.isEditing$.value ? stopPropagation : undefined}"
></affine-multi-tag-view> >
<affine-multi-tag-view
.value="${this._value$.value}"
.options="${this.options$.value}"
></affine-multi-tag-view>
</div>
`; `;
} }
} }
@@ -95,6 +82,5 @@ export const multiSelectPropertyConfig =
icon: createIcon('MultiSelectIcon'), icon: createIcon('MultiSelectIcon'),
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(MultiSelectCell), view: createFromBaseCellRenderer(MultiSelectCell),
edit: createFromBaseCellRenderer(MultiSelectCellEditing),
}, },
}); });
@@ -0,0 +1,37 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const numberStyle = style({
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
wordBreak: 'break-all',
});
export const numberInputStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
textAlign: 'right',
':focus': {
outline: 'none',
},
});
@@ -1,12 +1,12 @@
import { IS_MAC } from '@blocksuite/global/env'; import { IS_MAC } from '@blocksuite/global/env';
import { baseTheme } from '@toeverything/theme'; import { html } from 'lit';
import { css, html, unsafeCSS } from 'lit';
import { query } from 'lit/decorators.js'; import { query } from 'lit/decorators.js';
import { BaseCellRenderer } from '../../core/property/index.js'; import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { stopPropagation } from '../../core/utils/event.js'; import { stopPropagation } from '../../core/utils/event.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import { numberInputStyle, numberStyle } from './cell-renderer.css.js';
import { numberPropertyModelConfig } from './define.js'; import { numberPropertyModelConfig } from './define.js';
import type { NumberPropertyDataType } from './types.js'; import type { NumberPropertyDataType } from './types.js';
import { import {
@@ -19,93 +19,23 @@ export class NumberCell extends BaseCellRenderer<
number, number,
NumberPropertyDataType NumberPropertyDataType
> { > {
static override styles = css` @query('input')
affine-database-number-cell { private accessor _inputEle!: HTMLInputElement;
display: block;
width: 100%;
}
.affine-database-number { private _getFormattedString(value: number | undefined = this.value) {
overflow: hidden;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
word-break: break-all;
}
`;
private _getFormattedString() {
const enableNewFormatting = const enableNewFormatting =
this.view.featureFlags$.value.enable_number_formatting; this.view.featureFlags$.value.enable_number_formatting;
const decimals = this.property.data$.value.decimal ?? 0; const decimals = this.property.data$.value.decimal ?? 0;
const formatMode = (this.property.data$.value.format ?? const formatMode = (this.property.data$.value.format ??
'number') as NumberFormat; 'number') as NumberFormat;
return this.value != undefined return value != undefined
? enableNewFormatting ? enableNewFormatting
? formatNumber(this.value, formatMode, decimals) ? formatNumber(value, formatMode, decimals)
: this.value.toString() : value.toString()
: ''; : '';
} }
override render() {
return html` <div class="affine-database-number number">
${this._getFormattedString()}
</div>`;
}
}
export class NumberCellEditing extends BaseCellRenderer<
number,
NumberPropertyDataType
> {
static override styles = css`
affine-database-number-cell-editing {
display: block;
width: 100%;
cursor: text;
}
.affine-database-number {
display: flex;
align-items: center;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
text-align: right;
}
.affine-database-number:focus {
outline: none;
}
`;
private readonly _getFormattedString = (value: number) => {
const enableNewFormatting =
this.view.featureFlags$.value.enable_number_formatting;
const decimals = this.property.data$.value.decimal ?? 0;
const formatMode = (this.property.data$.value.format ??
'number') as NumberFormat;
return enableNewFormatting
? formatNumber(value, formatMode, decimals)
: value.toString();
};
private readonly _keydown = (e: KeyboardEvent) => { private readonly _keydown = (e: KeyboardEvent) => {
const ctrlKey = IS_MAC ? e.metaKey : e.ctrlKey; const ctrlKey = IS_MAC ? e.metaKey : e.ctrlKey;
@@ -121,9 +51,9 @@ export class NumberCellEditing extends BaseCellRenderer<
} }
}; };
private readonly _setValue = (str: string = this._inputEle.value) => { private readonly _setValue = (str: string = this._inputEle?.value) => {
if (!str) { if (!str) {
this.onChange(undefined); this.valueSetNextTick(undefined);
return; return;
} }
@@ -131,17 +61,23 @@ export class NumberCellEditing extends BaseCellRenderer<
this.view.featureFlags$.value.enable_number_formatting; this.view.featureFlags$.value.enable_number_formatting;
const value = enableNewFormatting ? parseNumber(str) : parseFloat(str); const value = enableNewFormatting ? parseNumber(str) : parseFloat(str);
if (isNaN(value)) { if (isNaN(value)) {
this._inputEle.value = this.value if (this._inputEle) {
? this._getFormattedString(this.value) this._inputEle.value = this.value
: ''; ? this._getFormattedString(this.value)
: '';
}
return; return;
} }
this._inputEle.value = this._getFormattedString(value); if (this._inputEle) {
this.onChange(value); this._inputEle.value = this._getFormattedString(value);
}
this.valueSetNextTick(value);
}; };
focusEnd = () => { focusEnd = () => {
if (!this._inputEle) return;
const end = this._inputEle.value.length; const end = this._inputEle.value.length;
this._inputEle.focus(); this._inputEle.focus();
this._inputEle.setSelectionRange(end, end); this._inputEle.setSelectionRange(end, end);
@@ -152,38 +88,39 @@ export class NumberCellEditing extends BaseCellRenderer<
} }
_focus() { _focus() {
if (!this.isEditing) { if (!this.isEditing$.value) {
this.selectCurrentCell(true); this.selectCurrentCell(true);
} }
} }
override firstUpdated() { override afterEnterEditingMode() {
requestAnimationFrame(() => { this.focusEnd();
this.focusEnd();
});
} }
override onExitEditMode() { override beforeExitEditingMode() {
this._setValue(); this._setValue();
} }
override render() { override render() {
const formatted = this.value ? this._getFormattedString(this.value) : ''; if (this.isEditing$.value) {
const formatted = this.value ? this._getFormattedString(this.value) : '';
return html`<input return html`<input
type="text" type="text"
autocomplete="off" autocomplete="off"
.value="${formatted}" .value="${formatted}"
@keydown="${this._keydown}" @keydown="${this._keydown}"
@blur="${this._blur}" @blur="${this._blur}"
@focus="${this._focus}" @focus="${this._focus}"
class="affine-database-number number" class="${numberInputStyle} number"
@pointerdown="${stopPropagation}" @pointerdown="${stopPropagation}"
/>`; />`;
} else {
return html` <div class="${numberStyle} number">
${this._getFormattedString()}
</div>`;
}
} }
@query('input')
private accessor _inputEle!: HTMLInputElement;
} }
export const numberPropertyConfig = export const numberPropertyConfig =
@@ -191,6 +128,5 @@ export const numberPropertyConfig =
icon: createIcon('NumberIcon'), icon: createIcon('NumberIcon'),
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(NumberCell), view: createFromBaseCellRenderer(NumberCell),
edit: createFromBaseCellRenderer(NumberCellEditing),
}, },
}); });
@@ -0,0 +1,57 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const progressCellStyle = style({
display: 'block',
width: '100%',
padding: '0 4px',
userSelect: 'none',
});
export const progressContainerStyle = style({
display: 'flex',
alignItems: 'center',
height: 'var(--data-view-cell-text-line-height)',
gap: '4px',
});
export const progressBarStyle = style({
position: 'relative',
width: '100%',
});
export const progressBgStyle = style({
overflow: 'hidden',
width: '100%',
height: '10px',
borderRadius: '22px',
});
export const progressFgStyle = style({
height: '100%',
});
export const progressDragHandleStyle = style({
position: 'absolute',
top: '0',
left: '0',
transform: 'translate(0px, -1px)',
width: '6px',
height: '12px',
borderRadius: '2px',
opacity: '1',
cursor: 'ew-resize',
background: 'var(--affine-primary-color)',
transition: 'opacity 0.2s ease-in-out',
});
export const progressNumberStyle = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '18px',
width: '25px',
color: 'var(--affine-text-secondary-color)',
fontSize: '14px',
fontFamily: baseTheme.fontSansFamily,
});
@@ -1,4 +1,4 @@
import { css, html } from 'lit'; import { html } from 'lit';
import { query, state } from 'lit/decorators.js'; import { query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
@@ -6,69 +6,17 @@ import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { startDrag } from '../../core/utils/drag.js'; import { startDrag } from '../../core/utils/drag.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import {
progressBarStyle,
progressBgStyle,
progressCellStyle,
progressContainerStyle,
progressDragHandleStyle,
progressFgStyle,
progressNumberStyle,
} from './cell-renderer.css.js';
import { progressPropertyModelConfig } from './define.js'; import { progressPropertyModelConfig } from './define.js';
const styles = css`
affine-database-progress-cell-editing {
display: block;
width: 100%;
padding: 0 4px;
}
affine-database-progress-cell {
display: block;
width: 100%;
padding: 0 4px;
}
.affine-database-progress {
display: flex;
align-items: center;
height: var(--data-view-cell-text-line-height);
gap: 4px;
}
.affine-database-progress-bar {
position: relative;
width: 104px;
}
.affine-database-progress-bg {
overflow: hidden;
width: 100%;
height: 10px;
border-radius: 22px;
}
.affine-database-progress-fg {
height: 100%;
}
.affine-database-progress-drag-handle {
position: absolute;
top: 0;
left: 0;
transform: translate(0px, -1px);
width: 6px;
height: 12px;
border-radius: 2px;
opacity: 1;
cursor: ew-resize;
background: var(--affine-primary-color);
transition: opacity 0.2s ease-in-out;
}
.progress-number {
display: flex;
justify-content: center;
align-items: center;
height: 18px;
width: 25px;
color: var(--affine-text-secondary-color);
font-size: 14px;
}
`;
const progressColors = { const progressColors = {
empty: 'var(--affine-black-10)', empty: 'var(--affine-black-10)',
processing: 'var(--affine-processing-color)', processing: 'var(--affine-processing-color)',
@@ -76,38 +24,9 @@ const progressColors = {
}; };
export class ProgressCell extends BaseCellRenderer<number> { export class ProgressCell extends BaseCellRenderer<number> {
static override styles = styles;
protected override render() {
const progress = this.value ?? 0;
let backgroundColor = progressColors.processing;
if (progress === 100) {
backgroundColor = progressColors.success;
}
const fgStyles = styleMap({
width: `${progress}%`,
backgroundColor,
});
const bgStyles = styleMap({
backgroundColor:
progress === 0 ? progressColors.empty : 'var(--affine-hover-color)',
});
return html` <div class="affine-database-progress">
<div class="affine-database-progress-bar">
<div class="affine-database-progress-bg" style=${bgStyles}>
<div class="affine-database-progress-fg" style=${fgStyles}></div>
</div>
</div>
<div class="progress-number progress">${progress}</div>
</div>`;
}
}
export class ProgressCellEditing extends BaseCellRenderer<number> {
static override styles = styles;
startDrag = (event: MouseEvent) => { startDrag = (event: MouseEvent) => {
if (!this.isEditing$.value) return;
const bgRect = this._progressBg.getBoundingClientRect(); const bgRect = this._progressBg.getBoundingClientRect();
const min = bgRect.left; const min = bgRect.left;
const max = bgRect.right; const max = bgRect.right;
@@ -135,7 +54,9 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
}; };
get _value() { get _value() {
return this.tempValue ?? this.value ?? 0; return this.isEditing$.value
? (this.tempValue ?? this.value ?? 0)
: (this.value ?? 0);
} }
_onChange(value?: number) { _onChange(value?: number) {
@@ -146,13 +67,17 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
const disposables = this._disposables; const disposables = this._disposables;
disposables.addFromEvent(this._progressBg, 'pointerdown', this.startDrag); disposables.addFromEvent(this._progressBg, 'pointerdown', this.startDrag);
disposables.addFromEvent(window, 'keydown', evt => { disposables.addFromEvent(window, 'keydown', evt => {
if (evt.key === 'ArrowDown') { if (!this.isEditing$.value) {
return;
}
if (evt.key === 'ArrowDown' || evt.key === 'ArrowLeft') {
evt.preventDefault(); evt.preventDefault();
this._onChange(Math.max(0, this._value - 1)); this._onChange(Math.max(0, this._value - 1));
return; return;
} }
if (evt.key === 'ArrowUp') { if (evt.key === 'ArrowUp' || evt.key === 'ArrowRight') {
evt.preventDefault(); evt.preventDefault();
this._onChange(Math.min(100, this._value + 1)); this._onChange(Math.min(100, this._value + 1));
return; return;
@@ -160,20 +85,25 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
}); });
} }
preventDefault(e: ClipboardEvent) {
e.stopPropagation();
}
override onCopy(_e: ClipboardEvent) { override onCopy(_e: ClipboardEvent) {
_e.preventDefault(); this.preventDefault(_e);
} }
override onCut(_e: ClipboardEvent) { override onCut(_e: ClipboardEvent) {
_e.preventDefault(); this.preventDefault(_e);
} }
override onExitEditMode() { override beforeExitEditingMode() {
this.onChange(this._value); const value = this._value;
this.valueSetNextTick(value);
} }
override onPaste(_e: ClipboardEvent) { override onPaste(_e: ClipboardEvent) {
_e.preventDefault(); this.preventDefault(_e);
} }
protected override render() { protected override render() {
@@ -190,25 +120,37 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
backgroundColor: backgroundColor:
progress === 0 ? progressColors.empty : 'var(--affine-hover-color)', progress === 0 ? progressColors.empty : 'var(--affine-hover-color)',
}); });
const handleStyles = styleMap({
left: `calc(${progress}% - 3px)`,
});
return html` <div class="affine-database-progress"> return html`
<div class="affine-database-progress-bar"> <div class="${progressCellStyle}">
<div class="affine-database-progress-bg" style=${bgStyles}> <div class="${progressContainerStyle}">
<div class="affine-database-progress-fg" style=${fgStyles}></div> <div class="${progressBarStyle}">
<div <div
class="affine-database-progress-drag-handle" class="${progressBgStyle}"
style=${handleStyles} data-testid="progress-background"
></div> style=${bgStyles}
>
<div class="${progressFgStyle}" style=${fgStyles}></div>
${this.isEditing$.value
? html` <div
class="${progressDragHandleStyle}"
data-testid="progress-drag-handle"
style=${styleMap({
left: `calc(${progress}% - 3px)`,
})}
></div>`
: ''}
</div>
</div>
<span class="${progressNumberStyle}" data-testid="progress"
>${progress}</span
>
</div> </div>
</div> </div>
<div class="progress-number progress">${progress}</div> `;
</div>`;
} }
@query('.affine-database-progress-bg') @query(`.${progressBgStyle}`)
private accessor _progressBg!: HTMLElement; private accessor _progressBg!: HTMLElement;
@state() @state()
@@ -220,6 +162,5 @@ export const progressPropertyConfig =
icon: createIcon('ProgressIcon'), icon: createIcon('ProgressIcon'),
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(ProgressCell), view: createFromBaseCellRenderer(ProgressCell),
edit: createFromBaseCellRenderer(ProgressCellEditing),
}, },
}); });
@@ -0,0 +1,18 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const selectStyle = style({
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
wordBreak: 'break-all',
});
@@ -1,5 +1,5 @@
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu'; import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
import { computed, signal } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import { popTagSelect } from '../../core/component/tags/multi-tag-select.js'; import { popTagSelect } from '../../core/component/tags/multi-tag-select.js';
@@ -7,48 +7,26 @@ import type { SelectTag } from '../../core/index.js';
import { BaseCellRenderer } from '../../core/property/index.js'; import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import { selectStyle } from './cell-renderer.css.js';
import { import {
type SelectPropertyData, type SelectPropertyData,
selectPropertyModelConfig, selectPropertyModelConfig,
} from './define.js'; } from './define.js';
export class SelectCell extends BaseCellRenderer<string[], SelectPropertyData> { export class SelectCell extends BaseCellRenderer<string, SelectPropertyData> {
override render() { closePopup?: () => void;
const value = this.value ? [this.value] : [];
return html`
<affine-multi-tag-view
.value="${value}"
.options="${this.property.data$.value.options}"
></affine-multi-tag-view>
`;
}
}
export class SelectCellEditing extends BaseCellRenderer<
string,
SelectPropertyData
> {
private readonly popTagSelect = () => { private readonly popTagSelect = () => {
const value = signal(this._value); this.closePopup = popTagSelect(popupTargetFromElement(this), {
this._disposables.add({ name: this.cell.property.name$.value,
dispose: popTagSelect( mode: 'single',
popupTargetFromElement( options: this.options$,
this.querySelector('affine-multi-tag-view') ?? this onOptionsChange: this._onOptionsChange,
), value: this._value$,
{ onChange: v => {
name: this.cell.property.name$.value, this.valueSetImmediate(v[0]);
mode: 'single', },
options: this.options$, onComplete: this._editComplete,
onOptionsChange: this._onOptionsChange, minWidth: 400,
value: signal(this._value),
onChange: v => {
this._onChange(v);
value.value = v;
},
onComplete: this._editComplete,
minWidth: 400,
}
),
}); });
}; };
@@ -56,10 +34,6 @@ export class SelectCellEditing extends BaseCellRenderer<
this.selectCurrentCell(false); this.selectCurrentCell(false);
}; };
_onChange = ([id]: string[]) => {
this.onChange(id);
};
_onOptionsChange = (options: SelectTag[]) => { _onOptionsChange = (options: SelectTag[]) => {
this.property.dataUpdate(data => { this.property.dataUpdate(data => {
return { return {
@@ -73,21 +47,30 @@ export class SelectCellEditing extends BaseCellRenderer<
return this.property.data$.value.options; return this.property.data$.value.options;
}); });
get _value() { _value$ = computed(() => {
const value = this.value; const value = this.value;
return value ? [value] : []; return value ? [value] : [];
});
override afterEnterEditingMode() {
if (!this.closePopup) {
this.popTagSelect();
}
} }
override firstUpdated() { override beforeExitEditingMode() {
this.popTagSelect(); this.closePopup?.();
this.closePopup = undefined;
} }
override render() { override render() {
return html` return html`
<affine-multi-tag-view <div class="${selectStyle}">
.value="${this._value}" <affine-multi-tag-view
.options="${this.options$.value}" .value="${this._value$.value}"
></affine-multi-tag-view> .options="${this.options$.value}"
></affine-multi-tag-view>
</div>
`; `;
} }
} }
@@ -97,6 +80,5 @@ export const selectPropertyConfig =
icon: createIcon('SingleSelectIcon'), icon: createIcon('SingleSelectIcon'),
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(SelectCell), view: createFromBaseCellRenderer(SelectCell),
edit: createFromBaseCellRenderer(SelectCellEditing),
}, },
}); });
@@ -0,0 +1,42 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const textStyle = style({
display: 'flex',
alignItems: 'center',
height: '100%',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const textInputStyle = style({
display: 'flex',
alignItems: 'center',
height: '100%',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
cursor: 'text',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
':focus': {
outline: 'none',
},
});
@@ -1,74 +1,15 @@
import { baseTheme } from '@toeverything/theme'; import { html } from 'lit';
import { css, html, unsafeCSS } from 'lit';
import { query } from 'lit/decorators.js'; import { query } from 'lit/decorators.js';
import { BaseCellRenderer } from '../../core/property/index.js'; import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import { textInputStyle, textStyle } from './cell-renderer.css.js';
import { textPropertyModelConfig } from './define.js'; import { textPropertyModelConfig } from './define.js';
export class TextCell extends BaseCellRenderer<string> { export class TextCell extends BaseCellRenderer<string> {
static override styles = css` @query('input')
affine-database-text-cell { private accessor _inputEle!: HTMLInputElement;
display: block;
width: 100%;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.affine-database-text {
display: flex;
align-items: center;
height: 100%;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
}
`;
override render() {
return html` <div class="affine-database-text">${this.value ?? ''}</div>`;
}
}
export class TextCellEditing extends BaseCellRenderer<string> {
static override styles = css`
affine-database-text-cell-editing {
display: block;
width: 100%;
height: 100%;
cursor: text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.affine-database-text {
display: flex;
align-items: center;
height: 100%;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
}
.affine-database-text:focus {
outline: none;
}
`;
private readonly _keydown = (e: KeyboardEvent) => { private readonly _keydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.isComposing) { if (e.key === 'Enter' && !e.isComposing) {
@@ -79,35 +20,40 @@ export class TextCellEditing extends BaseCellRenderer<string> {
} }
}; };
private readonly _setValue = (str: string = this._inputEle.value) => { private readonly _setValue = (str: string = this._inputEle?.value) => {
this._inputEle.value = `${this.value ?? ''}`; if (this._inputEle) {
this.onChange(str); this._inputEle.value = `${this.value ?? ''}`;
}
this.valueSetNextTick(str);
}; };
focusEnd = () => { focusEnd = () => {
if (!this._inputEle) return;
const end = this._inputEle.value.length; const end = this._inputEle.value.length;
this._inputEle.focus(); this._inputEle.focus();
this._inputEle.setSelectionRange(end, end); this._inputEle.setSelectionRange(end, end);
}; };
override firstUpdated() { override afterEnterEditingMode() {
this.focusEnd(); this.focusEnd();
} }
override onExitEditMode() { override beforeExitEditingMode() {
this._setValue(); this._setValue();
} }
override render() { override render() {
return html`<input if (this.isEditing$.value) {
.value="${this.value ?? ''}" return html`<input
@keydown="${this._keydown}" .value="${this.value ?? ''}"
class="affine-database-text" @keydown="${this._keydown}"
/>`; class="${textInputStyle}"
/>`;
} else {
return html`<div class="${textStyle}">${this.value ?? ''}</div>`;
}
} }
@query('input')
private accessor _inputEle!: HTMLInputElement;
} }
export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({ export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({
@@ -115,6 +61,5 @@ export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(TextCell), view: createFromBaseCellRenderer(TextCell),
edit: createFromBaseCellRenderer(TextCellEditing),
}, },
}); });
@@ -5,7 +5,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, effect, signal } from '@preact/signals-core'; import { computed, effect, signal } from '@preact/signals-core';
import { css } from 'lit'; import { css } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { import type {
@@ -52,7 +52,7 @@ export class MobileKanbanCell extends SignalWatcher(
private readonly _cell = signal<DataViewCellLifeCycle>(); private readonly _cell = signal<DataViewCellLifeCycle>();
isEditing$ = computed(() => { isSelectionEditing$ = computed(() => {
const selection = this.kanban?.props.selection$.value; const selection = this.kanban?.props.selection$.value;
if (selection?.selectionType !== 'cell') { if (selection?.selectionType !== 'cell') {
return false; return false;
@@ -108,19 +108,21 @@ export class MobileKanbanCell extends SignalWatcher(
if (this.column.readonly$.value) return; if (this.column.readonly$.value) return;
this.disposables.add( this.disposables.add(
effect(() => { effect(() => {
const isEditing = this.isEditing$.value; const isEditing = this.isSelectionEditing$.value;
if (isEditing) { if (isEditing) {
this.isEditing = true; this.isEditing$.value = true;
this._cell.value?.onEnterEditMode(); requestAnimationFrame(() => {
this._cell.value?.afterEnterEditingMode();
});
} else { } else {
this._cell.value?.onExitEditMode(); this._cell.value?.beforeExitEditingMode();
this.isEditing = false; this.isEditing$.value = false;
} }
}) })
); );
this._disposables.addFromEvent(this, 'click', e => { this._disposables.addFromEvent(this, 'click', e => {
e.stopPropagation(); e.stopPropagation();
if (!this.isEditing) { if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value); this.selectCurrentCell(!this.column.readonly$.value);
} }
}); });
@@ -129,16 +131,16 @@ export class MobileKanbanCell extends SignalWatcher(
override render() { override render() {
const props: CellRenderProps = { const props: CellRenderProps = {
cell: this.column.cellGet(this.cardId), cell: this.column.cellGet(this.cardId),
isEditing: this.isEditing, isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell, selectCurrentCell: this.selectCurrentCell,
}; };
const renderer = this.column.renderer$.value; const renderer = this.column.renderer$.value;
if (!renderer) return; if (!renderer) return;
const { view, edit } = renderer; const { view } = renderer;
this.view.lockRows(this.isEditing); this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.isEditing}`; this.dataset['editing'] = `${this.isEditing$.value}`;
return html` ${this.renderIcon()} return html` ${this.renderIcon()}
${renderUniLit(this.isEditing && edit ? edit : view, props, { ${renderUniLit(view, props, {
ref: this._cell, ref: this._cell,
class: 'mobile-kanban-cell', class: 'mobile-kanban-cell',
style: { display: 'block', flex: '1', overflow: 'hidden' }, style: { display: 'block', flex: '1', overflow: 'hidden' },
@@ -167,8 +169,7 @@ export class MobileKanbanCell extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor groupKey!: string; accessor groupKey!: string;
@state() isEditing$ = signal(false);
accessor isEditing = false;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: KanbanSingleView; accessor view!: KanbanSingleView;
@@ -109,7 +109,7 @@ export class KanbanCell extends SignalWatcher(
if (!selectionElement) return; if (!selectionElement) return;
if (e.shiftKey) return; if (e.shiftKey) return;
if (!this.editing) { if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value); this.selectCurrentCell(!this.column.readonly$.value);
} }
}); });
@@ -130,22 +130,22 @@ export class KanbanCell extends SignalWatcher(
override render() { override render() {
const props: CellRenderProps = { const props: CellRenderProps = {
cell: this.column.cellGet(this.cardId), cell: this.column.cellGet(this.cardId),
isEditing: this.editing, isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell, selectCurrentCell: this.selectCurrentCell,
}; };
const renderer = this.column.renderer$.value; const renderer = this.column.renderer$.value;
if (!renderer) return; if (!renderer) return;
const { view, edit } = renderer; const { view } = renderer;
this.view.lockRows(this.editing); this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.editing}`; this.dataset['editing'] = `${this.isEditing$.value}`;
this.style.border = this.isFocus this.style.border = this.isFocus
? '1px solid var(--affine-primary-color)' ? '1px solid var(--affine-primary-color)'
: ''; : '';
this.style.boxShadow = this.editing this.style.boxShadow = this.isEditing$.value
? '0px 0px 0px 2px rgba(30, 150, 235, 0.30)' ? '0px 0px 0px 2px rgba(30, 150, 235, 0.30)'
: ''; : '';
return html` ${this.renderIcon()} return html` ${this.renderIcon()}
${renderUniLit(this.editing && edit ? edit : view, props, { ${renderUniLit(view, props, {
ref: this._cell, ref: this._cell,
class: 'kanban-cell', class: 'kanban-cell',
style: { display: 'block', flex: '1', overflow: 'hidden' }, style: { display: 'block', flex: '1', overflow: 'hidden' },
@@ -168,8 +168,7 @@ export class KanbanCell extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor contentOnly = false; accessor contentOnly = false;
@state() isEditing$ = signal(false);
accessor editing = false;
@property({ attribute: false }) @property({ attribute: false })
accessor groupKey!: string; accessor groupKey!: string;
@@ -153,7 +153,7 @@ export class KanbanDragController implements ReactiveController {
const target = event.target; const target = event.target;
if (target instanceof Element) { if (target instanceof Element) {
const cell = target.closest('affine-data-view-kanban-cell'); const cell = target.closest('affine-data-view-kanban-cell');
if (cell?.editing) { if (cell?.isEditing$.value) {
return; return;
} }
cell?.selectCurrentCell(false); cell?.selectCurrentCell(false);
@@ -96,13 +96,11 @@ export class KanbanSelectionController implements ReactiveController {
const cell = container?.cell; const cell = container?.cell;
if (selection.isEditing) { if (selection.isEditing) {
requestAnimationFrame(() => { cell?.beforeExitEditingMode();
cell?.onExitEditMode();
});
if (cell?.blurCell()) { if (cell?.blurCell()) {
container.blur(); container.blur();
} }
container.editing = false; container.isEditing$.value = false;
} else { } else {
container.blur(); container.blur();
} }
@@ -142,11 +140,13 @@ export class KanbanSelectionController implements ReactiveController {
container.isFocus = true; container.isFocus = true;
const cell = container?.cell; const cell = container?.cell;
if (selection.isEditing) { if (selection.isEditing) {
cell?.onEnterEditMode();
if (cell?.focusCell()) { if (cell?.focusCell()) {
container.focus(); container.focus();
} }
container.editing = true; container.isEditing$.value = true;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
});
} else { } else {
container.focus(); container.focus();
} }
@@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, effect, signal } from '@preact/signals-core'; import { computed, effect, signal } from '@preact/signals-core';
import { css } from 'lit'; import { css } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { import {
type CellRenderProps, type CellRenderProps,
@@ -47,7 +47,7 @@ export class MobileTableCell extends SignalWatcher(
return this.column.cellGet(this.rowId); return this.column.cellGet(this.rowId);
}); });
isEditing$ = computed(() => { isSelectionEditing$ = computed(() => {
const selection = this.table?.props.selection$.value; const selection = this.table?.props.selection$.value;
if (selection?.selectionType !== 'area') { if (selection?.selectionType !== 'area') {
return false; return false;
@@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher(
return this.closest('mobile-table-group')?.group?.key; return this.closest('mobile-table-group')?.group?.key;
} }
private get readonly() {
return this.column.readonly$.value;
}
private get table() { private get table() {
return this.closest('mobile-data-view-table'); return this.closest('mobile-data-view-table');
} }
@@ -110,18 +106,21 @@ export class MobileTableCell extends SignalWatcher(
if (this.column.readonly$.value) return; if (this.column.readonly$.value) return;
this.disposables.add( this.disposables.add(
effect(() => { effect(() => {
const isEditing = this.isEditing$.value; const isEditing = this.isSelectionEditing$.value;
if (isEditing) { if (isEditing) {
this.isEditing = true; this.isEditing$.value = true;
this._cell.value?.onEnterEditMode(); const cell = this._cell.value;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
});
} else { } else {
this._cell.value?.onExitEditMode(); this._cell.value?.beforeExitEditingMode();
this.isEditing = false; this.isEditing$.value = false;
} }
}) })
); );
this.disposables.addFromEvent(this, 'click', () => { this.disposables.addFromEvent(this, 'click', () => {
if (!this.isEditing) { if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value); this.selectCurrentCell(!this.column.readonly$.value);
} }
}); });
@@ -132,17 +131,16 @@ export class MobileTableCell extends SignalWatcher(
if (!renderer) { if (!renderer) {
return; return;
} }
const { edit, view } = renderer; const { view } = renderer;
const uni = !this.readonly && this.isEditing && edit != null ? edit : view; this.view.lockRows(this.isEditing$.value);
this.view.lockRows(this.isEditing); this.dataset['editing'] = `${this.isEditing$.value}`;
this.dataset['editing'] = `${this.isEditing}`;
const props: CellRenderProps = { const props: CellRenderProps = {
cell: this.cell$.value, cell: this.cell$.value,
isEditing: this.isEditing, isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell, selectCurrentCell: this.selectCurrentCell,
}; };
return renderUniLit(uni, props, { return renderUniLit(view, props, {
ref: this._cell, ref: this._cell,
style: { style: {
display: 'contents', display: 'contents',
@@ -156,8 +154,7 @@ export class MobileTableCell extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor columnIndex!: number; accessor columnIndex!: number;
@state() isEditing$ = signal(false);
accessor isEditing = false;
@property({ attribute: false }) @property({ attribute: false })
accessor rowIndex!: number; accessor rowIndex!: number;
@@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, signal } from '@preact/signals-core'; import { computed, signal } from '@preact/signals-core';
import { css } from 'lit'; import { css } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { renderUniLit } from '../../../core/index.js'; import { renderUniLit } from '../../../core/index.js';
import type { import type {
@@ -88,10 +88,6 @@ export class DatabaseCellContainer extends SignalWatcher(
return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key; return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key;
} }
private get readonly() {
return this.column.readonly$.value;
}
private get selectionView() { private get selectionView() {
return this.closest('affine-database-table')?.selectionController; return this.closest('affine-database-table')?.selectionController;
} }
@@ -104,7 +100,7 @@ export class DatabaseCellContainer extends SignalWatcher(
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._disposables.addFromEvent(this, 'click', () => { this._disposables.addFromEvent(this, 'click', () => {
if (!this.isEditing) { if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value); this.selectCurrentCell(!this.column.readonly$.value);
} }
}); });
@@ -128,17 +124,16 @@ export class DatabaseCellContainer extends SignalWatcher(
if (!renderer) { if (!renderer) {
return; return;
} }
const { edit, view } = renderer; const { view } = renderer;
const uni = !this.readonly && this.isEditing && edit != null ? edit : view; this.view.lockRows(this.isEditing$.value);
this.view.lockRows(this.isEditing); this.dataset['editing'] = `${this.isEditing$.value}`;
this.dataset['editing'] = `${this.isEditing}`;
const props: CellRenderProps = { const props: CellRenderProps = {
cell: this.cell$.value, cell: this.cell$.value,
isEditing: this.isEditing, isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell, selectCurrentCell: this.selectCurrentCell,
}; };
return renderUniLit(uni, props, { return renderUniLit(view, props, {
ref: this._cell, ref: this._cell,
style: { style: {
display: 'contents', display: 'contents',
@@ -152,8 +147,7 @@ export class DatabaseCellContainer extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor columnIndex!: number; accessor columnIndex!: number;
@state() isEditing$ = signal(false);
accessor isEditing = false;
@property({ attribute: false }) @property({ attribute: false })
accessor rowIndex!: number; accessor rowIndex!: number;
@@ -192,11 +192,9 @@ export class TableSelectionController implements ReactiveController {
if (container) { if (container) {
const cell = container.cell; const cell = container.cell;
if (old.isEditing) { if (old.isEditing) {
requestAnimationFrame(() => { cell?.beforeExitEditingMode();
cell?.onExitEditMode();
});
cell?.blurCell(); cell?.blurCell();
container.isEditing = false; container.isEditing$.value = false;
} }
} }
} }
@@ -211,9 +209,11 @@ export class TableSelectionController implements ReactiveController {
if (container) { if (container) {
const cell = container.cell; const cell = container.cell;
if (newSelection.isEditing) { if (newSelection.isEditing) {
cell?.onEnterEditMode(); container.isEditing$.value = true;
container.isEditing = true; requestAnimationFrame(() => {
cell?.focusCell(); cell?.afterEnterEditingMode();
cell?.focusCell();
});
} }
} }
} }
+22 -19
View File
@@ -10,7 +10,7 @@ import {
type VLine, type VLine,
} from '@blocksuite/inline'; } from '@blocksuite/inline';
import { Text } from '@blocksuite/store'; import { Text } from '@blocksuite/store';
import { effect } from '@preact/signals-core'; import { effect, signal } from '@preact/signals-core';
import { css, html, type TemplateResult } from 'lit'; import { css, html, type TemplateResult } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
@@ -57,7 +57,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
#verticalScrollContainer: HTMLElement | null = null; #verticalScrollContainer: HTMLElement | null = null;
private _inlineEditor: AffineInlineEditor | null = null; private readonly _inlineEditor$ = signal<AffineInlineEditor | null>(null);
private readonly _onCopy = (e: ClipboardEvent) => { private readonly _onCopy = (e: ClipboardEvent) => {
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor;
@@ -144,7 +144,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
// It will listen ctrl+z/ctrl+shift+z and call undoManager.undo/redo, keydown event will not // It will listen ctrl+z/ctrl+shift+z and call undoManager.undo/redo, keydown event will not
get inlineEditor() { get inlineEditor() {
return this._inlineEditor; return this._inlineEditor$.value;
} }
get inlineEditorContainer() { get inlineEditorContainer() {
@@ -152,7 +152,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
} }
private _init() { private _init() {
if (this._inlineEditor) { if (this.inlineEditor) {
console.error('Inline editor already exists.'); console.error('Inline editor already exists.');
return; return;
} }
@@ -162,22 +162,25 @@ export class RichText extends WithDisposable(ShadowlessElement) {
} }
// init inline editor // init inline editor
this._inlineEditor = new InlineEditor<AffineTextAttributes>(this._yText, { this._inlineEditor$.value = new InlineEditor<AffineTextAttributes>(
isEmbed: delta => this.embedChecker(delta), this._yText,
hooks: { {
beforeinput: onVBeforeinput, isEmbed: delta => this.embedChecker(delta),
compositionEnd: onVCompositionEnd, hooks: {
}, beforeinput: onVBeforeinput,
inlineRangeProvider: this.inlineRangeProvider, compositionEnd: onVCompositionEnd,
vLineRenderer: this.vLineRenderer, },
}); inlineRangeProvider: this.inlineRangeProvider,
vLineRenderer: this.vLineRenderer,
}
);
const inlineEditor = this._inlineEditor$.value;
if (this.attributesSchema) { if (this.attributesSchema) {
this._inlineEditor.setAttributeSchema(this.attributesSchema); inlineEditor.setAttributeSchema(this.attributesSchema);
} }
if (this.attributeRenderer) { if (this.attributeRenderer) {
this._inlineEditor.setAttributeRenderer(this.attributeRenderer); inlineEditor.setAttributeRenderer(this.attributeRenderer);
} }
const inlineEditor = this._inlineEditor;
const markdownMatches = this.markdownMatches; const markdownMatches = this.markdownMatches;
if (markdownMatches) { if (markdownMatches) {
@@ -291,7 +294,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
if (this.inlineEditor?.mounted) { if (this.inlineEditor?.mounted) {
this.inlineEditor.unmount(); this.inlineEditor.unmount();
} }
this._inlineEditor = null; this._inlineEditor$.value = null;
} }
override connectedCallback() { override connectedCallback() {
@@ -384,8 +387,8 @@ export class RichText extends WithDisposable(ShadowlessElement) {
this._init(); this._init();
return; return;
} }
if (this._inlineEditor && changedProperties.has('readonly')) { if (this.inlineEditor && changedProperties.has('readonly')) {
this._inlineEditor.setReadonly(this.readonly); this.inlineEditor.setReadonly(this.readonly);
} }
} }
+36 -70
View File
@@ -1,7 +1,4 @@
import type { import type { RichTextCell } from '@blocksuite/affine/blocks/database';
RichTextCell,
RichTextCellEditing,
} from '@blocksuite/affine/blocks/database';
import { ZERO_WIDTH_SPACE } from '@blocksuite/affine/inline'; import { ZERO_WIDTH_SPACE } from '@blocksuite/affine/inline';
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
@@ -77,7 +74,7 @@ export function clickColumnType(page: Page, columnType: string) {
export function getDatabaseBodyRows(page: Page) { export function getDatabaseBodyRows(page: Page) {
const rowContainer = page.locator('.affine-database-block-rows'); const rowContainer = page.locator('.affine-database-block-rows');
return rowContainer.locator('.database-row'); return rowContainer.locator('data-view-table-row');
} }
export function getDatabaseBodyRow(page: Page, rowIndex = 0) { export function getDatabaseBodyRow(page: Page, rowIndex = 0) {
@@ -85,28 +82,17 @@ export function getDatabaseBodyRow(page: Page, rowIndex = 0) {
return rows.nth(rowIndex); return rows.nth(rowIndex);
} }
export function getDatabaseTableContainer(page: Page) {
const container = page.locator('.affine-database-table-container');
return container;
}
export async function assertDatabaseTitleColumnText( export async function assertDatabaseTitleColumnText(
page: Page, page: Page,
title: string, title: string,
index = 0 rowIndex = 0,
columnIndex = 0
) { ) {
const text = await page.evaluate(index => { const selectCell1 = getDatabaseCell(page, {
const rowContainer = document.querySelector('.affine-database-block-rows'); rowIndex: rowIndex,
const row = rowContainer?.querySelector( columnIndex: columnIndex,
`.database-row:nth-child(${index + 1})` });
); const text = await selectCell1.innerText();
const titleColumnCell = row?.querySelector('.database-cell:nth-child(1)');
const titleSpan = titleColumnCell?.querySelector(
'.data-view-header-area-rich-text'
) as HTMLElement;
if (!titleSpan) throw new Error('Cannot find database title column editor');
return titleSpan.innerText;
}, index);
if (title === '') { if (title === '') {
expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_SPACE}])$`)); expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_SPACE}])$`));
@@ -115,49 +101,34 @@ export async function assertDatabaseTitleColumnText(
} }
} }
export function getDatabaseBodyCell( export function getDatabaseCell(
page: Page, page: Page,
{ {
rowIndex, rowIndex,
columnType,
columnIndex, columnIndex,
}: { }: {
rowIndex: number; rowIndex?: number;
columnIndex: number; columnType?: string;
columnIndex?: number;
} }
) { ) {
const row = getDatabaseBodyRow(page, rowIndex); const row = getDatabaseBodyRow(page, rowIndex);
const cell = row.locator('.database-cell').nth(columnIndex); const index = columnIndex ?? 0;
return cell; const columns = columnType
? row.getByTestId(columnType)
: row.locator('affine-database-cell-container');
return columns.nth(index);
} }
export function getDatabaseBodyCellContent( export const getDatabaseColumnCells = (page: Page, columnIndex: number) => {
page: Page, return page.locator(
{ `affine-database-cell-container[data-column-index="${columnIndex}"]`
rowIndex, );
columnIndex, };
cellClass,
}: {
rowIndex: number;
columnIndex: number;
cellClass: string;
}
) {
const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex });
const cellContent = cell.locator(`.${cellClass}`);
return cellContent;
}
export function getFirstColumnCell(page: Page, cellClass: string) {
const cellContent = getDatabaseBodyCellContent(page, {
rowIndex: 0,
columnIndex: 1,
cellClass,
});
return cellContent;
}
export async function clickSelectOption(page: Page, index = 0) { export async function clickSelectOption(page: Page, index = 0) {
await page.locator('.select-option-icon').nth(index).click(); await page.getByTestId('option-more').nth(index).click();
} }
export async function performSelectColumnTagAction( export async function performSelectColumnTagAction(
@@ -207,14 +178,10 @@ export async function assertDatabaseCellRichTexts(
`affine-database-cell-container[data-row-index='${rowIndex}'][data-column-index='${columnIndex}']` `affine-database-cell-container[data-row-index='${rowIndex}'][data-column-index='${columnIndex}']`
); );
const cellEditing = cellContainer.locator(
'affine-database-rich-text-cell-editing'
);
const cell = cellContainer.locator('affine-database-rich-text-cell'); const cell = cellContainer.locator('affine-database-rich-text-cell');
const richText = (await cellEditing.count()) === 0 ? cell : cellEditing; const actualTexts = await cell.evaluate(ele => {
const actualTexts = await richText.evaluate(ele => { return (ele as RichTextCell).inlineEditor$.value?.yTextString;
return (ele as RichTextCellEditing).inlineEditor?.yTextString;
}); });
expect(actualTexts).toEqual(text); expect(actualTexts).toEqual(text);
} }
@@ -263,13 +230,11 @@ export async function assertDatabaseCellLink(
const cell = row?.querySelector( const cell = row?.querySelector(
`.database-cell:nth-child(${columnIndex + 1})` `.database-cell:nth-child(${columnIndex + 1})`
); );
const richText = const richText = cell?.querySelector<RichTextCell>(
cell?.querySelector<RichTextCell>('affine-database-link-cell') ?? 'affine-database-link-cell'
cell?.querySelector<RichTextCellEditing>( );
'affine-database-link-cell-editing'
);
if (!richText) throw new Error('Missing database rich text cell'); if (!richText) throw new Error('Missing database rich text cell');
return richText.inlineEditor!.yText.toString(); return richText.inlineEditor$.value!.yText.toString();
}, },
{ rowIndex, columnIndex } { rowIndex, columnIndex }
); );
@@ -381,7 +346,7 @@ export async function assertCellsSelection(
const focusBox = await getBoundingBox(focus); const focusBox = await getBoundingBox(focus);
const [rowIndex, columnIndex] = start; const [rowIndex, columnIndex] = start;
const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex }); const cell = getDatabaseCell(page, { rowIndex, columnIndex: columnIndex });
const cellBox = await getBoundingBox(cell); const cellBox = await getBoundingBox(cell);
expect(focusBox).toEqual({ expect(focusBox).toEqual({
x: cellBox.x, x: cellBox.x,
@@ -407,7 +372,7 @@ export async function assertCellsSelection(
let x = 0; let x = 0;
let y = 0; let y = 0;
for (let i = rowIndexStart; i <= rowIndexEnd; i++) { for (let i = rowIndexStart; i <= rowIndexEnd; i++) {
const cell = getDatabaseBodyCell(page, { const cell = getDatabaseCell(page, {
rowIndex: i, rowIndex: i,
columnIndex: columnIndexStart, columnIndex: columnIndexStart,
}); });
@@ -419,7 +384,7 @@ export async function assertCellsSelection(
} }
for (let j = columnIndexStart; j <= columnIndexEnd; j++) { for (let j = columnIndexStart; j <= columnIndexEnd; j++) {
const cell = getDatabaseBodyCell(page, { const cell = getDatabaseCell(page, {
rowIndex: rowIndexStart, rowIndex: rowIndexStart,
columnIndex: j, columnIndex: j,
}); });
@@ -468,7 +433,7 @@ export async function focusKanbanCardHeader(page: Page, index = 0) {
export async function clickKanbanCardHeader(page: Page, index = 0) { export async function clickKanbanCardHeader(page: Page, index = 0) {
const cardHeader = page.locator('data-view-header-area-text').nth(index); const cardHeader = page.locator('data-view-header-area-text').nth(index);
await cardHeader.click(); await cardHeader.click();
await cardHeader.click(); await pressEnter(page);
} }
export async function assertKanbanCardHeaderText( export async function assertKanbanCardHeaderText(
@@ -563,6 +528,7 @@ export function getKanbanCard(
const card = group.locator('affine-data-view-kanban-card').nth(cardIndex); const card = group.locator('affine-data-view-kanban-card').nth(cardIndex);
return card; return card;
} }
export const moveToCenterOf = async (page: Page, locator: Locator) => { export const moveToCenterOf = async (page: Page, locator: Locator) => {
const box = (await locator.boundingBox())!; const box = (await locator.boundingBox())!;
expect(box).toBeDefined(); expect(box).toBeDefined();
@@ -24,7 +24,7 @@ import { assertRichTexts } from '../utils/asserts.js';
import { test } from '../utils/playwright.js'; import { test } from '../utils/playwright.js';
import { import {
assertDatabaseTitleColumnText, assertDatabaseTitleColumnText,
getDatabaseBodyCell, getDatabaseCell,
getElementStyle, getElementStyle,
initDatabaseColumn, initDatabaseColumn,
switchColumnType, switchColumnType,
@@ -92,12 +92,12 @@ test.describe('copy&paste when selecting', () => {
await initDatabaseRowWithData(page, ''); await initDatabaseRowWithData(page, '');
await initDatabaseRowWithData(page, ''); await initDatabaseRowWithData(page, '');
const startCell = getDatabaseBodyCell(page, { const startCell = getDatabaseCell(page, {
rowIndex: 0, rowIndex: 0,
columnIndex: 0, columnIndex: 0,
}); });
const startCellBox = await getBoundingBox(startCell); const startCellBox = await getBoundingBox(startCell);
const endCell = getDatabaseBodyCell(page, { rowIndex: 1, columnIndex: 1 }); const endCell = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1 });
const endCellBox = await getBoundingBox(endCell); const endCellBox = await getBoundingBox(endCell);
const startX = startCellBox.x + startCellBox.width / 2; const startX = startCellBox.x + startCellBox.width / 2;
const startY = startCellBox.y + startCellBox.height / 2; const startY = startCellBox.y + startCellBox.height / 2;
@@ -120,11 +120,11 @@ test.describe('copy&paste when selecting', () => {
await assertDatabaseTitleColumnText(page, 'text1', 2); await assertDatabaseTitleColumnText(page, 'text1', 2);
await assertDatabaseTitleColumnText(page, 'text2', 3); await assertDatabaseTitleColumnText(page, 'text2', 3);
const selectCell21 = getDatabaseBodyCell(page, { const selectCell21 = getDatabaseCell(page, {
rowIndex: 2, rowIndex: 2,
columnIndex: 1, columnIndex: 1,
}); });
const selectCell31 = getDatabaseBodyCell(page, { const selectCell31 = getDatabaseCell(page, {
rowIndex: 3, rowIndex: 3,
columnIndex: 1, columnIndex: 1,
}); });
@@ -145,7 +145,7 @@ test.describe('copy&paste when selecting', () => {
await waitNextFrame(page, 100); await waitNextFrame(page, 100);
await initDatabaseColumn(page); await initDatabaseColumn(page);
await switchColumnType(page, 'Number', 2); await switchColumnType(page, 'Number', 2);
const numberCell = getDatabaseBodyCell(page, { const numberCell = getDatabaseCell(page, {
rowIndex: 0, rowIndex: 0,
columnIndex: 2, columnIndex: 2,
}); });
@@ -162,12 +162,12 @@ test.describe('copy&paste when selecting', () => {
await waitNextFrame(page); await waitNextFrame(page);
await assertDatabaseTitleColumnText(page, 'text1', 1); await assertDatabaseTitleColumnText(page, 'text1', 1);
const selectCell = getDatabaseBodyCell(page, { const selectCell = getDatabaseCell(page, {
rowIndex: 1, rowIndex: 1,
columnIndex: 1, columnIndex: 1,
}); });
expect(await selectCell.innerText()).toBe('abc'); expect(await selectCell.innerText()).toBe('abc');
const selectNumberCell = getDatabaseBodyCell(page, { const selectNumberCell = getDatabaseCell(page, {
rowIndex: 1, rowIndex: 1,
columnIndex: 2, columnIndex: 2,
}); });
@@ -27,8 +27,8 @@ import {
changeColumnType, changeColumnType,
clickDatabaseOutside, clickDatabaseOutside,
clickSelectOption, clickSelectOption,
getDatabaseCell,
getDatabaseHeaderColumn, getDatabaseHeaderColumn,
getFirstColumnCell,
initDatabaseColumn, initDatabaseColumn,
performColumnAction, performColumnAction,
performSelectColumnTagAction, performSelectColumnTagAction,
@@ -67,7 +67,10 @@ test.describe('column operations', () => {
const { text: title1 } = await getDatabaseHeaderColumn(page, 1); const { text: title1 } = await getDatabaseHeaderColumn(page, 1);
expect(title1).toBe('Column 1'); expect(title1).toBe('Column 1');
const selected = getFirstColumnCell(page, 'select-selected'); const selected = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'multi-select',
});
expect(await selected.innerText()).toBe('123'); expect(await selected.innerText()).toBe('123');
await initDatabaseColumn(page, 'abc'); await initDatabaseColumn(page, 'abc');
@@ -198,7 +201,10 @@ test.describe('switch column type', () => {
await pressEscape(page); await pressEscape(page);
await changeColumnType(page, 1, 'Number'); await changeColumnType(page, 1, 'Number');
const cell = getFirstColumnCell(page, 'number'); const cell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'number',
});
await assertDatabaseCellNumber(page, { await assertDatabaseCellNumber(page, {
text: '', text: '',
}); });
@@ -217,10 +223,11 @@ test.describe('switch column type', () => {
await pressEscape(page); await pressEscape(page);
await switchColumnType(page, 'Text'); await switchColumnType(page, 'Text');
// For now, rich-text will only be initialized on click const cell = getDatabaseCell(page, {
// Therefore, for the time being, here is to detect whether there is '.affine-database-rich-text' rowIndex: 0,
const cell = getFirstColumnCell(page, 'affine-database-rich-text'); columnType: 'rich-text',
expect(await cell.count()).toBe(1); });
await cell.isVisible();
await pressEnter(page); await pressEnter(page);
await type(page, '123'); await type(page, '123');
await pressEscape(page); await pressEscape(page);
@@ -239,35 +246,39 @@ test.describe('switch column type', () => {
await type(page, 'abc'); await type(page, 'abc');
await pressEnter(page); await pressEnter(page);
await pressEscape(page); await pressEscape(page);
const cell = getFirstColumnCell(page, 'select-selected'); const cell = getDatabaseCell(page, {
expect(await cell.count()).toBe(2); rowIndex: 0,
columnIndex: 1,
});
const tags = cell.getByTestId('tag-selected');
expect(await tags.count()).toBe(2);
await switchColumnType(page, 'Select', 1); await switchColumnType(page, 'Select', 1);
expect(await cell.count()).toBe(1); expect(await tags.count()).toBe(1);
expect(await cell.innerText()).toBe('123'); expect(await tags.innerText()).toBe('123');
await pressEnter(page); await pressEnter(page);
await type(page, 'def'); await type(page, 'def');
await pressEnter(page); await pressEnter(page);
expect(await cell.innerText()).toBe('def'); expect(await tags.innerText()).toBe('def');
await switchColumnType(page, 'Multi-select'); await switchColumnType(page, 'Multi-select');
await pressEnter(page); await pressEnter(page);
await type(page, '666'); await type(page, '666');
await pressEnter(page); await pressEnter(page);
await pressEscape(page); await pressEscape(page);
expect(await cell.count()).toBe(2); expect(await tags.count()).toBe(2);
expect(await cell.nth(0).innerText()).toBe('def'); expect(await tags.nth(0).innerText()).toBe('def');
expect(await cell.nth(1).innerText()).toBe('666'); expect(await tags.nth(1).innerText()).toBe('666');
await switchColumnType(page, 'Select'); await switchColumnType(page, 'Select');
expect(await cell.count()).toBe(1); expect(await tags.count()).toBe(1);
expect(await cell.innerText()).toBe('def'); expect(await tags.innerText()).toBe('def');
await pressEnter(page); await pressEnter(page);
await type(page, '888'); await type(page, '888');
await pressEnter(page); await pressEnter(page);
expect(await cell.innerText()).toBe('888'); expect(await tags.innerText()).toBe('888');
}); });
test('switch between number and rich-text', async ({ page }) => { test('switch between number and rich-text', async ({ page }) => {
@@ -302,12 +313,18 @@ test.describe('switch column type', () => {
await switchColumnType(page, 'Number'); await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123', true); await initDatabaseDynamicRowWithData(page, '123', true);
const cell = getFirstColumnCell(page, 'number'); const cell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'number',
});
expect((await cell.textContent())?.trim()).toBe('123'); expect((await cell.textContent())?.trim()).toBe('123');
await switchColumnType(page, 'Select'); await switchColumnType(page, 'Select');
await initDatabaseDynamicRowWithData(page, 'abc'); await initDatabaseDynamicRowWithData(page, 'abc');
const selectCell = getFirstColumnCell(page, 'select-selected'); const selectCell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'select',
});
expect(await selectCell.innerText()).toBe('abc'); expect(await selectCell.innerText()).toBe('abc');
await switchColumnType(page, 'Number'); await switchColumnType(page, 'Number');
@@ -325,7 +342,10 @@ test.describe('switch column type', () => {
await pressEscape(page); await pressEscape(page);
await changeColumnType(page, 1, 'Checkbox'); await changeColumnType(page, 1, 'Checkbox');
const checkbox = getFirstColumnCell(page, 'checkbox'); const checkbox = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'checkbox',
}).locator('.checkbox');
await expect(checkbox).not.toHaveClass('checked'); await expect(checkbox).not.toHaveClass('checked');
await waitNextFrame(page, 500); await waitNextFrame(page, 500);
@@ -345,7 +365,10 @@ test.describe('switch column type', () => {
await pressEscape(page); await pressEscape(page);
await changeColumnType(page, 1, 'Checkbox'); await changeColumnType(page, 1, 'Checkbox');
let checkbox = getFirstColumnCell(page, 'checkbox'); let checkbox = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'checkbox',
});
await expect(checkbox).not.toHaveClass('checked'); await expect(checkbox).not.toHaveClass('checked');
// checked // checked
@@ -357,7 +380,10 @@ test.describe('switch column type', () => {
await clickDatabaseOutside(page); await clickDatabaseOutside(page);
await waitNextFrame(page, 100); await waitNextFrame(page, 100);
await changeColumnType(page, 1, 'Checkbox'); await changeColumnType(page, 1, 'Checkbox');
checkbox = getFirstColumnCell(page, 'checkbox'); checkbox = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'checkbox',
}).locator('.checkbox');
await expect(checkbox).toHaveClass(/checked/); await expect(checkbox).toHaveClass(/checked/);
// not checked // not checked
@@ -369,7 +395,10 @@ test.describe('switch column type', () => {
await clickDatabaseOutside(page); await clickDatabaseOutside(page);
await waitNextFrame(page, 100); await waitNextFrame(page, 100);
await changeColumnType(page, 1, 'Checkbox'); await changeColumnType(page, 1, 'Checkbox');
checkbox = getFirstColumnCell(page, 'checkbox'); checkbox = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'checkbox',
});
await expect(checkbox).not.toHaveClass('checked'); await expect(checkbox).not.toHaveClass('checked');
}); });
@@ -382,10 +411,14 @@ test.describe('switch column type', () => {
await pressEscape(page); await pressEscape(page);
await switchColumnType(page, 'Progress'); await switchColumnType(page, 'Progress');
const progress = getFirstColumnCell(page, 'progress'); const cell = getDatabaseCell(page, {
expect(await progress.textContent()).toBe('0'); rowIndex: 0,
columnType: 'progress',
});
const progress = cell.getByTestId('progress');
expect((await progress.textContent())?.trim()).toBe('0');
await waitNextFrame(page, 500); await waitNextFrame(page, 500);
const progressBg = page.locator('.affine-database-progress-bg'); const progressBg = cell.getByTestId('progress-background');
const { const {
x: progressBgX, x: progressBgX,
y: progressBgY, y: progressBgY,
@@ -393,7 +426,7 @@ test.describe('switch column type', () => {
} = await getBoundingBox(progressBg); } = await getBoundingBox(progressBg);
await page.mouse.move(progressBgX, progressBgY); await page.mouse.move(progressBgX, progressBgY);
await page.mouse.click(progressBgX, progressBgY); await page.mouse.click(progressBgX, progressBgY);
const dragHandle = page.locator('.affine-database-progress-drag-handle'); const dragHandle = cell.getByTestId('progress-drag-handle');
const { const {
x: dragX, x: dragX,
y: dragY, y: dragY,
@@ -427,19 +460,25 @@ test.describe('switch column type', () => {
await switchColumnType(page, 'Link'); await switchColumnType(page, 'Link');
const linkText = 'http://example.com'; const linkText = 'http://example.com';
const cell = getFirstColumnCell(page, 'affine-database-link'); const cell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'link',
});
await pressEnter(page); await pressEnter(page);
await type(page, linkText); await type(page, linkText);
await pressEscape(page); await pressEscape(page);
const link = cell.locator('affine-database-link-node > a'); const link = cell.getByTestId('property-link-a');
const linkContent = link.locator('.link-node-text');
await expect(link).toHaveAttribute('href', linkText); await expect(link).toHaveAttribute('href', linkText);
expect(await linkContent.textContent()).toBe(linkText); expect(await link.textContent()).toBe(linkText);
// not link text // not link text
await cell.hover(); await cell.hover();
const linkEdit = getFirstColumnCell(page, 'affine-database-link-icon'); const linkEdit = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'link',
}).getByTestId('edit-link-button');
await linkEdit.click(); await linkEdit.click();
await waitNextFrame(page);
await selectAllByKeyboard(page); await selectAllByKeyboard(page);
await type(page, 'abc'); await type(page, 'abc');
await pressEnter(page); await pressEnter(page);
@@ -461,7 +500,7 @@ test.describe('select column tag action', () => {
await pressArrowRight(page); await pressArrowRight(page);
await type(page, '4567abc00'); await type(page, '4567abc00');
await pressEnter(page); await pressEnter(page);
const options = page.locator('.select-options-container .tag-text'); const options = page.getByTestId('tag-option-list').getByTestId('tag-name');
expect(await options.nth(0).innerText()).toBe('abc4567abc00'); expect(await options.nth(0).innerText()).toBe('abc4567abc00');
expect(await options.nth(1).innerText()).toBe('123'); expect(await options.nth(1).innerText()).toBe('123');
}); });
@@ -479,7 +518,7 @@ test.describe('select column tag action', () => {
// esc // esc
await pressEscape(page); await pressEscape(page);
await pressEscape(page); await pressEscape(page);
const options = page.locator('.select-options-container .tag-text'); const options = page.getByTestId('tag-option-list').getByTestId('tag-name');
const option1 = options.nth(0); const option1 = options.nth(0);
expect(await option1.innerText()).toBe('123456'); expect(await option1.innerText()).toBe('123456');
}); });
@@ -40,9 +40,8 @@ import {
clickDatabaseOutside, clickDatabaseOutside,
focusDatabaseHeader, focusDatabaseHeader,
focusDatabaseSearch, focusDatabaseSearch,
getDatabaseBodyCell, getDatabaseCell,
getDatabaseHeaderColumn, getDatabaseHeaderColumn,
getFirstColumnCell,
initDatabaseColumn, initDatabaseColumn,
switchColumnType, switchColumnType,
} from './actions.js'; } from './actions.js';
@@ -66,9 +65,9 @@ test('edit database block title and create new rows', async ({ page }) => {
await assertBlockProps(page, '2', { await assertBlockProps(page, '2', {
title: 'hello', title: 'hello',
}); });
await undoByClick(page); await undoByKeyboard(page);
await assertBlockProps(page, '2', { await assertBlockProps(page, '2', {
title: 'Database 1', title: '',
}); });
await initDatabaseRowWithData(page, ''); await initDatabaseRowWithData(page, '');
await initDatabaseRowWithData(page, ''); await initDatabaseRowWithData(page, '');
@@ -103,7 +102,10 @@ test('should modify the value when the input loses focus', async ({ page }) => {
await initDatabaseDynamicRowWithData(page, '1', true); await initDatabaseDynamicRowWithData(page, '1', true);
await clickDatabaseOutside(page); await clickDatabaseOutside(page);
const cell = getFirstColumnCell(page, 'number'); const cell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'number',
});
const text = await cell.textContent(); const text = await cell.textContent();
expect(text?.trim()).toBe('1'); expect(text?.trim()).toBe('1');
}); });
@@ -116,13 +118,17 @@ test('should rich-text column support soft enter', async ({ page }) => {
await switchColumnType(page, 'Text'); await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '123', true); await initDatabaseDynamicRowWithData(page, '123', true);
const cell = getFirstColumnCell(page, 'affine-database-rich-text'); // const cell = getDatabaseCell(page, {
await cell.click(); // rowIndex: 0,
// columnType: 'rich-text',
// });
// await cell.click();
await pressEnter(page);
await pressArrowLeft(page); await pressArrowLeft(page);
await pressEnter(page); await pressEnter(page);
await assertDatabaseCellRichTexts(page, { text: '123' }); await assertDatabaseCellRichTexts(page, { text: '123' });
await cell.click(); await pressEnter(page);
await pressArrowRight(page); await pressArrowRight(page);
await pressArrowLeft(page); await pressArrowLeft(page);
await pressShiftEnter(page); await pressShiftEnter(page);
@@ -139,10 +145,13 @@ test('should the multi-select mode work correctly', async ({ page }) => {
await pressEscape(page); await pressEscape(page);
await initDatabaseDynamicRowWithData(page, '2'); await initDatabaseDynamicRowWithData(page, '2');
await pressEscape(page); await pressEscape(page);
const cell = getFirstColumnCell(page, 'select-selected'); const tags = getDatabaseCell(page, {
expect(await cell.count()).toBe(2); rowIndex: 0,
expect(await cell.nth(0).innerText()).toBe('1'); columnType: 'multi-select',
expect(await cell.nth(1).innerText()).toBe('2'); }).getByTestId('tag-selected');
expect(await tags.count()).toBe(2);
expect(await tags.nth(0).innerText()).toBe('1');
expect(await tags.nth(1).innerText()).toBe('2');
}); });
test('should database search work', async ({ page }) => { test('should database search work', async ({ page }) => {
@@ -375,7 +384,10 @@ test('should title column support quick changing of column type', async ({
await typeIcon.click(); await typeIcon.click();
await waitNextFrame(page); await waitNextFrame(page);
await clickColumnType(page, 'Select'); await clickColumnType(page, 'Select');
const cell = getFirstColumnCell(page, 'select-selected'); const cell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'select',
});
expect(await cell.count()).toBe(1); expect(await cell.count()).toBe(1);
}); });
@@ -513,12 +525,19 @@ test.describe('readonly mode', () => {
await switchColumnType(page, 'Text'); await switchColumnType(page, 'Text');
await initDatabaseDynamicRowWithData(page, '', true); await initDatabaseDynamicRowWithData(page, '', true);
const cell = getFirstColumnCell(page, 'affine-database-rich-text'); const cell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'rich-text',
});
await cell.click(); await cell.click();
await pressEnter(page);
await type(page, '123'); await type(page, '123');
await pressEnter(page);
await assertDatabaseCellRichTexts(page, { text: '123' }); await assertDatabaseCellRichTexts(page, { text: '123' });
await switchReadonly(page); await switchReadonly(page);
await waitNextFrame(page);
await pressEnter(page);
await pressBackspace(page); await pressBackspace(page);
await type(page, '789'); await type(page, '789');
await assertDatabaseCellRichTexts(page, { text: '123' }); await assertDatabaseCellRichTexts(page, { text: '123' });
@@ -610,7 +629,10 @@ test.describe('readonly mode', () => {
const database = page.locator('affine-database'); const database = page.locator('affine-database');
await expect(database).toBeVisible(); await expect(database).toBeVisible();
const cell = getFirstColumnCell(page, 'affine-database-rich-text'); const cell = getDatabaseCell(page, {
rowIndex: 0,
columnType: 'rich-text',
});
await cell.click(); await cell.click();
const focusBorder = database.locator( const focusBorder = database.locator(
@@ -635,11 +657,11 @@ test.describe('readonly mode', () => {
const database = page.locator('affine-database'); const database = page.locator('affine-database');
await expect(database).toBeVisible(); await expect(database).toBeVisible();
const startCell = getDatabaseBodyCell(page, { const startCell = getDatabaseCell(page, {
rowIndex: 0, rowIndex: 0,
columnIndex: 0, columnIndex: 0,
}); });
const endCell = getDatabaseBodyCell(page, { const endCell = getDatabaseCell(page, {
rowIndex: 0, rowIndex: 0,
columnIndex: 1, columnIndex: 1,
}); });
@@ -33,7 +33,7 @@ import {
assertRowsSelection, assertRowsSelection,
clickKanbanCardHeader, clickKanbanCardHeader,
focusKanbanCardHeader, focusKanbanCardHeader,
getDatabaseBodyCell, getDatabaseCell,
getKanbanCard, getKanbanCard,
initDatabaseColumn, initDatabaseColumn,
switchColumnType, switchColumnType,
@@ -62,7 +62,7 @@ test.describe('focus', () => {
await switchColumnType(page, 'Number'); await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123', true); await initDatabaseDynamicRowWithData(page, '123', true);
const selectColumn = getDatabaseBodyCell(page, { const selectColumn = getDatabaseCell(page, {
rowIndex: 1, rowIndex: 1,
columnIndex: 1, columnIndex: 1,
}); });
@@ -105,7 +105,6 @@ test.describe('row-level selection', () => {
await initDatabaseColumn(page); await initDatabaseColumn(page);
await initDatabaseRowWithData(page, 'title'); await initDatabaseRowWithData(page, 'title');
await pressEscape(page);
await waitNextFrame(page, 100); await waitNextFrame(page, 100);
await assertCellsSelection(page, { await assertCellsSelection(page, {
start: [0, 0], start: [0, 0],
@@ -123,7 +122,6 @@ test.describe('row-level selection', () => {
await initDatabaseColumn(page); await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '123', true); await initDatabaseDynamicRowWithData(page, '123', true);
await pressEscape(page);
await waitNextFrame(page, 100); await waitNextFrame(page, 100);
await pressEscape(page); await pressEscape(page);
await assertRowsSelection(page, [0, 0]); await assertRowsSelection(page, [0, 0]);
@@ -134,12 +132,11 @@ test.describe('row-level selection', () => {
await initEmptyDatabaseState(page); await initEmptyDatabaseState(page);
await initDatabaseColumn(page); await initDatabaseColumn(page);
await initDatabaseDynamicRowWithData(page, '', true);
await pressEscape(page);
await switchColumnType(page, 'Number'); await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, 'asd', true);
await initDatabaseDynamicRowWithData(page, '123', true); await initDatabaseDynamicRowWithData(page, '123', true);
const selectColumn = getDatabaseBodyCell(page, { const selectColumn = getDatabaseCell(page, {
rowIndex: 1, rowIndex: 1,
columnIndex: 1, columnIndex: 1,
}); });
@@ -250,11 +247,11 @@ test.describe('cell-level selection', () => {
await switchColumnType(page, 'Number'); await switchColumnType(page, 'Number');
await initDatabaseDynamicRowWithData(page, '123', true); await initDatabaseDynamicRowWithData(page, '123', true);
const startCell = getDatabaseBodyCell(page, { const startCell = getDatabaseCell(page, {
rowIndex: 0, rowIndex: 0,
columnIndex: 0, columnIndex: 0,
}); });
const endCell = getDatabaseBodyCell(page, { const endCell = getDatabaseCell(page, {
rowIndex: 1, rowIndex: 1,
columnIndex: 1, columnIndex: 1,
}); });
@@ -293,11 +290,11 @@ test.describe('cell-level selection', () => {
await initDatabaseDynamicRowWithData(page, '123', false); await initDatabaseDynamicRowWithData(page, '123', false);
await pressEscape(page); await pressEscape(page);
const startCell = getDatabaseBodyCell(page, { const startCell = getDatabaseCell(page, {
rowIndex: 0, rowIndex: 0,
columnIndex: 0, columnIndex: 0,
}); });
const endCell = getDatabaseBodyCell(page, { const endCell = getDatabaseCell(page, {
rowIndex: 1, rowIndex: 1,
columnIndex: 1, columnIndex: 1,
}); });
@@ -319,12 +316,12 @@ test.describe('cell-level selection', () => {
await pressBackspace(page); await pressBackspace(page);
await assertDatabaseTitleColumnText(page, '', 0); await assertDatabaseTitleColumnText(page, '', 0);
await assertDatabaseTitleColumnText(page, '', 1); await assertDatabaseTitleColumnText(page, '', 1);
const selectCell1 = getDatabaseBodyCell(page, { const selectCell1 = getDatabaseCell(page, {
rowIndex: 0, rowIndex: 0,
columnIndex: 1, columnIndex: 1,
}); });
expect(await selectCell1.innerText()).toBe(''); expect(await selectCell1.innerText()).toBe('');
const selectCell2 = getDatabaseBodyCell(page, { const selectCell2 = getDatabaseCell(page, {
rowIndex: 1, rowIndex: 1,
columnIndex: 1, columnIndex: 1,
}); });
@@ -450,6 +450,7 @@ export async function initDatabaseRowWithData(page: Page, data: string) {
await initDatabaseRow(page); await initDatabaseRow(page);
await waitNextFrame(page, 50); await waitNextFrame(page, 50);
await type(page, data); await type(page, data);
await pressEscape(page);
} }
export const getAddRow = (page: Page): Locator => { export const getAddRow = (page: Page): Locator => {
return page.locator('.data-view-table-group-add-row'); return page.locator('.data-view-table-group-add-row');
@@ -473,6 +474,7 @@ export async function initDatabaseDynamicRowWithData(
await pressEnter(page); await pressEnter(page);
await waitNextFrame(page); await waitNextFrame(page);
await type(page, data); await type(page, data);
await waitNextFrame(page);
await pressEnter(page); await pressEnter(page);
} }
@@ -1,3 +1,4 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { test } from '@affine-test/kit/playwright'; import { test } from '@affine-test/kit/playwright';
import { import {
openHomePage, openHomePage,
@@ -319,10 +320,11 @@ test('can show database backlink info', async ({ page }) => {
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.keyboard.press('ArrowRight'); await page.keyboard.press('ArrowRight');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await waitNextFrame(page);
await waitNextFrame(page);
await page.keyboard.type('Done'); await page.keyboard.type('Done');
await page await waitNextFrame(page);
.locator('affine-multi-tag-select .select-option:has-text("Done")') await page.keyboard.press('Enter');
.click();
// go back to title cell // go back to title cell
await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft');
+2
View File
@@ -2358,6 +2358,7 @@ __metadata:
"@preact/signals-core": "npm:^1.8.0" "@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.12" "@toeverything/theme": "npm:^1.1.12"
"@types/mdast": "npm:^4.0.4" "@types/mdast": "npm:^4.0.4"
"@vanilla-extract/css": "npm:^1.17.0"
date-fns: "npm:^4.0.0" date-fns: "npm:^4.0.0"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1" minimatch: "npm:^10.0.1"
@@ -3203,6 +3204,7 @@ __metadata:
"@preact/signals-core": "npm:^1.8.0" "@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.12" "@toeverything/theme": "npm:^1.1.12"
"@types/lodash-es": "npm:^4.17.12" "@types/lodash-es": "npm:^4.17.12"
"@vanilla-extract/css": "npm:^1.17.0"
date-fns: "npm:^4.0.0" date-fns: "npm:^4.0.0"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21" lodash-es: "npm:^4.17.21"