From db707dff7f2fb0630217e968ec15b8e2c59e43d0 Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:24:44 +0000 Subject: [PATCH] refactor(editor): remove edit view of database block properties (#10748) --- .../affine/blocks/block-database/package.json | 1 + .../blocks/block-database/src/effects.ts | 23 +- .../src/properties/link/cell-renderer.css.ts | 99 ++++++ .../src/properties/link/cell-renderer.ts | 317 +++++++----------- .../properties/link/components/link-node.ts | 41 --- .../properties/rich-text/cell-renderer.css.ts | 20 ++ .../src/properties/rich-text/cell-renderer.ts | 295 +++++----------- .../src/properties/title/cell-renderer.css.ts | 31 ++ .../src/properties/title/cell-renderer.ts | 9 +- .../src/properties/title/text.ts | 285 +++++----------- blocksuite/affine/data-view/package.json | 1 + .../core/component/tags/multi-tag-select.ts | 136 ++++---- .../src/core/component/tags/multi-tag-view.ts | 5 +- .../src/core/component/tags/styles.css.ts | 147 ++++++++ .../src/core/component/tags/styles.ts | 251 -------------- .../affine/data-view/src/core/detail/field.ts | 13 +- .../data-view/src/core/detail/selection.ts | 12 +- .../data-view/src/core/property/base-cell.ts | 33 +- .../data-view/src/core/property/manager.ts | 9 +- blocksuite/affine/data-view/src/effects.ts | 48 +-- .../checkbox/cell-renderer.ts | 2 +- .../date/cell-renderer.css.ts | 33 ++ .../property-presets/date/cell-renderer.ts | 109 ++---- .../multi-select/cell-renderer.css.ts | 17 + .../multi-select/cell-renderer.ts | 80 ++--- .../number/cell-renderer.css.ts | 37 ++ .../property-presets/number/cell-renderer.ts | 148 +++----- .../progress/cell-renderer.css.ts | 57 ++++ .../progress/cell-renderer.ts | 173 ++++------ .../select/cell-renderer.css.ts | 18 + .../property-presets/select/cell-renderer.ts | 80 ++--- .../text/cell-renderer.css.ts | 42 +++ .../property-presets/text/cell-renderer.ts | 99 ++---- .../src/view-presets/kanban/mobile/cell.ts | 31 +- .../src/view-presets/kanban/pc/cell.ts | 17 +- .../view-presets/kanban/pc/controller/drag.ts | 2 +- .../kanban/pc/controller/selection.ts | 12 +- .../src/view-presets/table/mobile/cell.ts | 37 +- .../src/view-presets/table/pc/cell.ts | 22 +- .../table/pc/controller/selection.ts | 14 +- blocksuite/affine/rich-text/src/rich-text.ts | 41 +-- .../tests-legacy/e2e/database/actions.ts | 106 ++---- .../e2e/database/clipboard.spec.ts | 16 +- .../tests-legacy/e2e/database/column.spec.ts | 109 ++++-- .../e2e/database/database.spec.ts | 56 +++- .../e2e/database/selection.spec.ts | 23 +- .../tests-legacy/e2e/utils/actions/misc.ts | 2 + .../affine-local/e2e/page-properties.spec.ts | 8 +- yarn.lock | 2 + 49 files changed, 1387 insertions(+), 1782 deletions(-) create mode 100644 blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.css.ts delete mode 100644 blocksuite/affine/blocks/block-database/src/properties/link/components/link-node.ts create mode 100644 blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.css.ts create mode 100644 blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.css.ts create mode 100644 blocksuite/affine/data-view/src/core/component/tags/styles.css.ts delete mode 100644 blocksuite/affine/data-view/src/core/component/tags/styles.ts create mode 100644 blocksuite/affine/data-view/src/property-presets/date/cell-renderer.css.ts create mode 100644 blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.css.ts create mode 100644 blocksuite/affine/data-view/src/property-presets/number/cell-renderer.css.ts create mode 100644 blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.css.ts create mode 100644 blocksuite/affine/data-view/src/property-presets/select/cell-renderer.css.ts create mode 100644 blocksuite/affine/data-view/src/property-presets/text/cell-renderer.css.ts diff --git a/blocksuite/affine/blocks/block-database/package.json b/blocksuite/affine/blocks/block-database/package.json index 9bd39d0a16..78e03420d2 100644 --- a/blocksuite/affine/blocks/block-database/package.json +++ b/blocksuite/affine/blocks/block-database/package.json @@ -30,6 +30,7 @@ "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.12", "@types/mdast": "^4.0.4", + "@vanilla-extract/css": "^1.17.0", "date-fns": "^4.0.0", "lit": "^3.2.0", "minimatch": "^10.0.1", diff --git a/blocksuite/affine/blocks/block-database/src/effects.ts b/blocksuite/affine/blocks/block-database/src/effects.ts index f2c8368d47..dd3541c0f0 100644 --- a/blocksuite/affine/blocks/block-database/src/effects.ts +++ b/blocksuite/affine/blocks/block-database/src/effects.ts @@ -4,37 +4,20 @@ import { DatabaseBlockComponent } from './database-block'; import { DatabaseDndPreviewBlockComponent } from './database-dnd-preview-block'; import { BlockRenderer } from './detail-panel/block-renderer'; import { NoteRenderer } from './detail-panel/note-renderer'; -import { LinkCell, LinkCellEditing } from './properties/link/cell-renderer'; -import { LinkNode } from './properties/link/components/link-node'; -import { - RichTextCell, - RichTextCellEditing, -} from './properties/rich-text/cell-renderer'; +import { LinkCell } from './properties/link/cell-renderer'; +import { RichTextCell } from './properties/rich-text/cell-renderer'; import { IconCell } from './properties/title/icon'; -import { - HeaderAreaTextCell, - HeaderAreaTextCellEditing, -} from './properties/title/text'; +import { HeaderAreaTextCell } from './properties/title/text'; export function effects() { customElements.define('affine-database-title', DatabaseTitle); customElements.define('data-view-header-area-icon', IconCell); customElements.define('affine-database-link-cell', LinkCell); - customElements.define('affine-database-link-cell-editing', LinkCellEditing); customElements.define('data-view-header-area-text', HeaderAreaTextCell); - customElements.define( - 'data-view-header-area-text-editing', - HeaderAreaTextCellEditing - ); customElements.define('affine-database-rich-text-cell', RichTextCell); - customElements.define( - 'affine-database-rich-text-cell-editing', - RichTextCellEditing - ); customElements.define('center-peek', CenterPeek); customElements.define('database-datasource-note-renderer', NoteRenderer); customElements.define('database-datasource-block-renderer', BlockRenderer); - customElements.define('affine-database-link-node', LinkNode); customElements.define('affine-database', DatabaseBlockComponent); customElements.define( diff --git a/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.css.ts b/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.css.ts new file mode 100644 index 0000000000..7b7c33eb87 --- /dev/null +++ b/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.css.ts @@ -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', +}); diff --git a/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts b/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts index 50f595cbfd..f5d7cbf4c2 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts @@ -1,6 +1,5 @@ import { RefNodeSlotsProvider } from '@blocksuite/affine-rich-text'; import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services'; -import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { isValidUrl, normalizeUrl, @@ -12,106 +11,69 @@ import { createIcon, } from '@blocksuite/data-view'; import { EditIcon } from '@blocksuite/icons/lit'; -import { baseTheme } from '@toeverything/theme'; -import { css, nothing, unsafeCSS } from 'lit'; -import { query, state } from 'lit/decorators.js'; -import { html } from 'lit/static-html.js'; +import { computed } from '@preact/signals-core'; +import { html, nothing, type PropertyValues } from 'lit'; +import { createRef, ref } from 'lit/directives/ref.js'; import { HostContextKey } from '../../context/host-context.js'; +import { + inlineLinkNodeStyle, + linkCellStyle, + linkContainerStyle, + linkedDocStyle, + linkEditingStyle, + linkIconContainerStyle, + linkIconStyle, + normalTextStyle, + showLinkIconStyle, +} from './cell-renderer.css.js'; import { linkPropertyModelConfig } from './define.js'; export class LinkCell extends BaseCellRenderer { - static override styles = css` - affine-database-link-cell { - width: 100%; - user-select: none; - position: relative; - } - - affine-database-link-cell:hover .affine-database-link-icon { - visibility: visible; - } - - .affine-database-link { - display: flex; - position: relative; - align-items: center; - width: 100%; - height: 100%; - outline: none; - overflow: hidden; - font-size: var(--data-view-cell-text-size); - line-height: var(--data-view-cell-text-line-height); - word-break: break-all; - } - - affine-database-link-node { - flex: 1; - word-break: break-all; - } - - .affine-database-link-icon { - position: absolute; - right: 8px; - top: 8px; - display: flex; - align-items: center; - visibility: hidden; - cursor: pointer; - background: ${unsafeCSSVarV2('button/iconButtonSolid')}; - color: ${unsafeCSSVarV2('icon/primary')}; - box-shadow: var(--affine-button-shadow); - border-radius: 4px; - font-size: 14px; - padding: 2px; - } - - .affine-database-link-icon:hover { - background: var(--affine-hover-color); - } - - .data-view-link-column-linked-doc { - text-decoration: underline; - text-decoration-color: var(--affine-divider-color); - transition: text-decoration-color 0.2s ease-out; - cursor: pointer; - } - - .data-view-link-column-linked-doc:hover { - text-decoration-color: var(--affine-icon-color); - } - `; - - private readonly _onClick = (event: Event) => { - event.stopPropagation(); - const value = this.value ?? ''; - - if (!value || !isValidUrl(value)) { - this.selectCurrentCell(true); - return; - } - - if (isValidUrl(value)) { - const target = event.target as HTMLElement; - const link = target.querySelector('.link-node'); - if (link) { - event.preventDefault(); - link.click(); - } - return; - } - }; + protected override firstUpdated(_changedProperties: PropertyValues) { + super.firstUpdated(_changedProperties); + this.classList.add(linkCellStyle); + } private readonly _onEdit = (e: Event) => { e.stopPropagation(); this.selectCurrentCell(true); + this.selectCurrentCell(true); }; - private preValue?: string; + private readonly _focusEnd = () => { + const ele = this._container.value; + if (!ele) { + return; + } + const end = ele?.value.length; + ele?.focus(); + ele?.setSelectionRange(end, end); + }; + + private readonly _onKeydown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.isComposing) { + this.selectCurrentCell(false); + } + }; + + private readonly _setValue = ( + value: string = this._container.value?.value ?? '' + ) => { + let url = value; + if (isValidUrl(value)) { + url = normalizeUrl(value); + } + + this.valueSetNextTick(url); + if (this._container.value) { + this._container.value.value = url; + } + }; openDoc = (e: MouseEvent) => { e.stopPropagation(); - if (!this.docId) { + if (!this.docId$.value) { return; } const std = this.std; @@ -120,7 +82,7 @@ export class LinkCell extends BaseCellRenderer { } std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({ - pageId: this.docId, + pageId: this.docId$.value, host: std.host, }); }; @@ -130,128 +92,95 @@ export class LinkCell extends BaseCellRenderer { return host?.std; } - override render() { - const linkText = this.value ?? ''; - const docName = - this.docId && this.std?.workspace.getDoc(this.docId)?.meta?.title; - return html` - - ${docName || linkText - ? html` ` - : nothing} - `; - } - - override updated() { - if (this.value !== this.preValue) { - const std = this.std; - this.preValue = this.value; - if (!this.value || !isValidUrl(this.value)) { - this.docId = undefined; - return; - } - - this.docId = - std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(this.value)?.docId ?? - undefined; + docId$ = computed(() => { + if (!this.value || !isValidUrl(this.value)) { + return; } - } + return this.parseDocUrl(this.value)?.docId; + }); - @state() - accessor docId: string | undefined = undefined; -} + private readonly _container = createRef(); -export class LinkCellEditing extends BaseCellRenderer { - static override styles = css` - affine-database-link-cell-editing { - width: 100%; - cursor: text; - } - - .affine-database-link-editing { - display: flex; - align-items: center; - width: 100%; - padding: 0; - border: none; - font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; - color: var(--affine-text-primary-color); - font-weight: 400; - background-color: transparent; - font-size: var(--data-view-cell-text-size); - line-height: var(--data-view-cell-text-line-height); - word-break: break-all; - } - - .affine-database-link-editing:focus { - outline: none; - } - `; - - private readonly _focusEnd = () => { - const end = this._container.value.length; - this._container.focus(); - this._container.setSelectionRange(end, end); - }; - - private readonly _onKeydown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && !e.isComposing) { - this._setValue(); - setTimeout(() => { - this.selectCurrentCell(false); - }); - } - }; - - private readonly _setValue = (value: string = this._container.value) => { - let url = value; - if (isValidUrl(value)) { - url = normalizeUrl(value); - } - - this.onChange(url); - this._container.value = url; - }; - - override firstUpdated() { + override afterEnterEditingMode() { this._focusEnd(); } - override onExitEditMode() { + override beforeExitEditingMode() { this._setValue(); } - override render() { - const linkText = this.value ?? ''; - - return html``; + parseDocUrl(url: string) { + return this.std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(url); } - @query('.affine-database-link-editing') - private accessor _container!: HTMLInputElement; + docName$ = computed(() => { + const title = + this.docId$.value && + this.std?.workspace.getDoc(this.docId$.value)?.meta?.title; + if (title == null) { + return; + } + return title || 'Untitled'; + }); + + renderLink() { + const linkText = this.value ?? ''; + const docName = this.docName$.value; + const isDoc = !!docName; + const isLink = !!linkText; + const hasLink = isDoc || isLink; + return html` +
+
+ ${isDoc + ? html`${docName}` + : isValidUrl(linkText) + ? html`${linkText}` + : html`${linkText}`} +
+ ${hasLink + ? html`
+
+ ${EditIcon()} +
+
` + : nothing} +
+ `; + } + + override render() { + if (this.isEditing$.value) { + const linkText = this.value ?? ''; + return html``; + } else { + return this.renderLink(); + } + } } export const linkColumnConfig = linkPropertyModelConfig.createPropertyMeta({ icon: createIcon('LinkIcon'), cellRenderer: { view: createFromBaseCellRenderer(LinkCell), - edit: createFromBaseCellRenderer(LinkCellEditing), }, }); diff --git a/blocksuite/affine/blocks/block-database/src/properties/link/components/link-node.ts b/blocksuite/affine/blocks/block-database/src/properties/link/components/link-node.ts deleted file mode 100644 index ae34b5f5f2..0000000000 --- a/blocksuite/affine/blocks/block-database/src/properties/link/components/link-node.ts +++ /dev/null @@ -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`${this.link}`; - } - - return html`${this.link}`; - } - - @property({ attribute: false }) - accessor link!: string; -} - -declare global { - interface HTMLElementTagNameMap { - 'affine-database-link-node': LinkNode; - } -} diff --git a/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.css.ts b/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.css.ts new file mode 100644 index 0000000000..8e2792e415 --- /dev/null +++ b/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.css.ts @@ -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', +}); diff --git a/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts b/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts index 533e78f030..f21a9e7b8a 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts @@ -21,13 +21,16 @@ import { IS_MAC } from '@blocksuite/global/env'; import type { DeltaInsert } from '@blocksuite/inline'; import type { BlockSnapshot } from '@blocksuite/store'; import { Text } from '@blocksuite/store'; -import { css } from 'lit'; -import { query } from 'lit/decorators.js'; -import { keyed } from 'lit/directives/keyed.js'; +import { computed, effect, signal } from '@preact/signals-core'; +import { ref } from 'lit/directives/ref.js'; import { html } from 'lit/static-html.js'; import { HostContextKey } from '../../context/host-context.js'; import type { DatabaseBlockComponent } from '../../database-block.js'; +import { + richTextCellStyle, + richTextContainerStyle, +} from './cell-renderer.css.js'; import { richTextPropertyModelConfig } from './define.js'; function toggleStyle( @@ -78,61 +81,10 @@ function toggleStyle( inlineEditor.syncInlineRange(); } -abstract class BaseRichTextCell extends BaseCellRenderer { - static override styles = css` - affine-database-rich-text-cell, - affine-database-rich-text-cell-editing { - display: flex; - align-items: center; - width: 100%; - user-select: none; - } - - .affine-database-rich-text { - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; - height: 100%; - outline: none; - font-size: var(--data-view-cell-text-size); - line-height: var(--data-view-cell-text-line-height); - word-break: break-all; - } - - .affine-database-rich-text v-line { - display: flex !important; - align-items: center; - height: 100%; - width: 100%; - } - - .affine-database-rich-text v-line > div { - flex-grow: 1; - } - - .data-view-header-area-icon { - height: max-content; - display: flex; - align-items: center; - margin-right: 8px; - padding: 2px; - border-radius: 4px; - margin-top: 2px; - background-color: var(--affine-background-secondary-color); - } - - .data-view-header-area-icon svg { - width: 14px; - height: 14px; - fill: var(--affine-icon-color); - color: var(--affine-icon-color); - } - `; - - get inlineEditor() { - return this.richText?.inlineEditor; - } +export class RichTextCell extends BaseCellRenderer { + inlineEditor$ = computed(() => { + return this.richText$.value?.inlineEditor; + }); get inlineManager() { return this.view @@ -146,57 +98,11 @@ abstract class BaseRichTextCell extends BaseCellRenderer { return databaseBlock?.topContenteditableElement; } - get attributeRenderer() { - return this.inlineManager?.getRenderer(); - } - - get attributesSchema() { - return this.inlineManager?.getSchema(); - } - get host() { return this.view.contextGet(HostContextKey); } - @query('rich-text') - accessor richText!: RichText; - - @query('.affine-database-rich-text') - accessor _richTextElement!: HTMLElement; -} - -export class RichTextCell extends BaseRichTextCell { - static override styles = css` - affine-database-rich-text-cell { - display: flex; - align-items: center; - width: 100%; - user-select: none; - } - - .affine-database-rich-text { - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; - height: 100%; - outline: none; - font-size: var(--data-view-cell-text-size); - line-height: var(--data-view-cell-text-line-height); - word-break: break-all; - } - - .affine-database-rich-text v-line { - display: flex !important; - align-items: center; - height: 100%; - width: 100%; - } - - .affine-database-rich-text v-line > div { - flex-grow: 1; - } - `; + private readonly richText$ = signal(); private changeUserSelectAccordToReadOnly() { if (this && this instanceof HTMLElement) { @@ -204,61 +110,6 @@ export class RichTextCell extends BaseRichTextCell { } } - override connectedCallback() { - super.connectedCallback(); - this.changeUserSelectAccordToReadOnly(); - } - - override render() { - if (!this.value || !(this.value instanceof Text)) { - return html`
`; - } - return keyed( - this.value, - html`` - ); - } -} - -export class RichTextCellEditing extends BaseRichTextCell { - static override styles = css` - affine-database-rich-text-cell-editing { - display: flex; - align-items: center; - width: 100%; - min-width: 1px; - cursor: text; - } - - .affine-database-rich-text { - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; - height: 100%; - outline: none; - } - - .affine-database-rich-text v-line { - display: flex !important; - align-items: center; - height: 100%; - width: 100%; - } - - .affine-database-rich-text v-line > div { - flex-grow: 1; - } - `; - private readonly _handleKeyDown = (event: KeyboardEvent) => { if (event.key !== 'Escape') { if (event.key === 'Tab') { @@ -280,7 +131,7 @@ export class RichTextCellEditing extends BaseRichTextCell { return; } - const inlineEditor = this.inlineEditor; + const inlineEditor = this.inlineEditor$.value; if (!inlineEditor) return; switch (event.key) { @@ -331,17 +182,17 @@ export class RichTextCellEditing extends BaseRichTextCell { private readonly _initYText = (text?: string) => { const yText = new Text(text); - this.onChange(yText); + this.valueSetImmediate(yText); }; private readonly _onSoftEnter = () => { - if (this.value && this.inlineEditor) { - const inlineRange = this.inlineEditor.getInlineRange(); + if (this.value && this.inlineEditor$.value) { + const inlineRange = this.inlineEditor$.value.getInlineRange(); if (!inlineRange) return; - const text = new Text(this.inlineEditor.yText); + const text = new Text(this.inlineEditor$.value.yText); text.replace(inlineRange.index, inlineRange.length, '\n'); - this.inlineEditor.setInlineRange({ + this.inlineEditor$.value.setInlineRange({ index: inlineRange.index + 1, length: 0, }); @@ -349,7 +200,7 @@ export class RichTextCellEditing extends BaseRichTextCell { }; private readonly _onCopy = (e: ClipboardEvent) => { - const inlineEditor = this.inlineEditor; + const inlineEditor = this.inlineEditor$.value; if (!inlineEditor) return; const inlineRange = inlineEditor.getInlineRange(); @@ -366,7 +217,7 @@ export class RichTextCellEditing extends BaseRichTextCell { }; private readonly _onCut = (e: ClipboardEvent) => { - const inlineEditor = this.inlineEditor; + const inlineEditor = this.inlineEditor$.value; if (!inlineEditor) return; const inlineRange = inlineEditor.getInlineRange(); @@ -388,7 +239,9 @@ export class RichTextCellEditing extends BaseRichTextCell { }; private readonly _onPaste = (e: ClipboardEvent) => { - const inlineEditor = this.inlineEditor; + e.preventDefault(); + e.stopPropagation(); + const inlineEditor = this.inlineEditor$.value; if (!inlineEditor) return; const inlineRange = inlineEditor.getInlineRange(); @@ -419,8 +272,7 @@ export class RichTextCellEditing extends BaseRichTextCell { ?.getData('text/plain') ?.replace(/\r?\n|\r/g, '\n'); if (!text) return; - e.preventDefault(); - e.stopPropagation(); + if (isValidUrl(text)) { const std = this.std; const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text); @@ -459,6 +311,7 @@ export class RichTextCellEditing extends BaseRichTextCell { }); } } else { + console.log(text); inlineEditor.insertText(inlineRange, text); inlineEditor.setInlineRange({ index: inlineRange.index + text.length, @@ -469,67 +322,78 @@ export class RichTextCellEditing extends BaseRichTextCell { override connectedCallback() { super.connectedCallback(); - if (!this.value || typeof this.value === 'string') { - this._initYText(this.value); - } + this.classList.add(richTextCellStyle); + + this.changeUserSelectAccordToReadOnly(); const selectAll = (e: KeyboardEvent) => { if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) { e.stopPropagation(); e.preventDefault(); - this.inlineEditor?.selectAll(); + this.inlineEditor$.value?.selectAll(); } }; this.addEventListener('keydown', selectAll); this.disposables.addFromEvent(this, 'keydown', selectAll); + this.disposables.add( + effect(() => { + const editor = this.inlineEditor$.value; + if (editor) { + const disposable = editor.slots.keydown.on(this._handleKeyDown); + return () => disposable.dispose(); + } + return; + }) + ); + this.disposables.add( + effect(() => { + const richText = this.richText$.value; + if (richText) { + richText.addEventListener('copy', this._onCopy, true); + richText.addEventListener('cut', this._onCut, true); + richText.addEventListener('paste', this._onPaste, true); + return () => { + richText.removeEventListener('copy', this._onCopy); + richText.removeEventListener('cut', this._onCut); + richText.removeEventListener('paste', this._onPaste); + }; + } + return; + }) + ); } - override firstUpdated() { - this.richText?.updateComplete - .then(() => { - const inlineEditor = this.inlineEditor; - if (!inlineEditor) return; + override beforeEnterEditMode() { + if (!this.value || typeof this.value === 'string') { + this._initYText(this.value); + } + return true; + } - this.disposables.add( - inlineEditor.slots.keydown.on(this._handleKeyDown) - ); - - this.disposables.addFromEvent( - this._richTextElement!, - 'copy', - this._onCopy - ); - this.disposables.addFromEvent( - this._richTextElement!, - 'cut', - this._onCut - ); - this.disposables.addFromEvent( - this._richTextElement!, - 'paste', - this._onPaste - ); - - inlineEditor.focusEnd(); - }) - .catch(console.error); + override afterEnterEditingMode() { + this.inlineEditor$.value?.focusEnd(); } override render() { - return html``; + } + return html` + .yText="${this.value}" + .inlineEventSource="${this.topContenteditableElement}" + .attributesSchema="${this.inlineManager?.getSchema()}" + .attributeRenderer="${this.inlineManager?.getRenderer()}" + .embedChecker="${this.inlineManager?.embedChecker}" + .markdownMatches="${this.inlineManager?.markdownMatches}" + .readonly="${!this.isEditing$.value || this.readonly}" + .verticalScrollContainerGetter="${() => this.topContenteditableElement?.host ? getViewportElement(this.topContenteditableElement.host) - : null} - class="affine-database-rich-text inline-editor" + : null}" + class="${richTextContainerStyle} inline-editor" >`; } @@ -538,7 +402,7 @@ export class RichTextCellEditing extends BaseRichTextCell { } insertDelta = (delta: DeltaInsert) => { - const inlineEditor = this.inlineEditor; + const inlineEditor = this.inlineEditor$.value; const range = inlineEditor?.getInlineRange(); if (!range || !delta.insert) { return; @@ -553,7 +417,7 @@ export class RichTextCellEditing extends BaseRichTextCell { declare global { interface HTMLElementTagNameMap { - 'affine-database-rich-text-cell-editing': RichTextCellEditing; + 'affine-database-rich-text-cell': RichTextCell; } } @@ -563,6 +427,5 @@ export const richTextColumnConfig = cellRenderer: { view: createFromBaseCellRenderer(RichTextCell), - edit: createFromBaseCellRenderer(RichTextCellEditing), }, }); diff --git a/blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.css.ts b/blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.css.ts new file mode 100644 index 0000000000..edfe5c0482 --- /dev/null +++ b/blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.css.ts @@ -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)', +}); diff --git a/blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.ts b/blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.ts index 645d88d7fc..d0831d8b1c 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/title/cell-renderer.ts @@ -7,7 +7,7 @@ import { import { TableSingleView } from '@blocksuite/data-view/view-presets'; import { titlePropertyModelConfig } from './define.js'; -import { HeaderAreaTextCell, HeaderAreaTextCellEditing } from './text.js'; +import { HeaderAreaTextCell } from './text.js'; export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({ icon: createIcon('TitleIcon'), @@ -19,12 +19,5 @@ export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({ showIcon: props.cell.view instanceof TableSingleView, }) ), - edit: uniMap( - createFromBaseCellRenderer(HeaderAreaTextCellEditing), - (props: CellRenderProps) => ({ - ...props, - showIcon: props.cell.view instanceof TableSingleView, - }) - ), }, }); diff --git a/blocksuite/affine/blocks/block-database/src/properties/title/text.ts b/blocksuite/affine/blocks/block-database/src/properties/title/text.ts index c60574d722..1151cb008d 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/title/text.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/title/text.ts @@ -1,4 +1,3 @@ -import type { RootBlockModel } from '@blocksuite/affine-model'; import { DefaultInlineManagerExtension, type RichText, @@ -16,106 +15,31 @@ import { IS_MAC } from '@blocksuite/global/env'; import { LinkedPageIcon } from '@blocksuite/icons/lit'; import type { DeltaInsert } from '@blocksuite/inline'; import type { BlockSnapshot, Text } from '@blocksuite/store'; -import { computed, effect, signal } from '@preact/signals-core'; -import { css, type TemplateResult } from 'lit'; -import { property, query } from 'lit/decorators.js'; +import { signal } from '@preact/signals-core'; +import { property } from 'lit/decorators.js'; +import { createRef, ref } from 'lit/directives/ref.js'; import { html } from 'lit/static-html.js'; import { HostContextKey } from '../../context/host-context.js'; import type { DatabaseBlockComponent } from '../../database-block.js'; import { getSingleDocIdFromText } from '../../utils/title-doc.js'; +import { + headerAreaIconStyle, + titleCellStyle, + titleRichTextStyle, +} from './cell-renderer.css.js'; -const styles = css` - data-view-header-area-text { - width: 100%; - display: flex; - } - - data-view-header-area-text rich-text { - pointer-events: none; - user-select: none; - } - - data-view-header-area-text-editing { - width: 100%; - display: flex; - cursor: text; - } - - .data-view-header-area-rich-text { - display: flex; - flex-direction: column; - justify-content: center; - width: 100%; - height: 100%; - outline: none; - word-break: break-all; - font-size: var(--data-view-cell-text-size); - line-height: var(--data-view-cell-text-line-height); - } - - .data-view-header-area-rich-text v-line { - display: flex !important; - align-items: center; - height: 100%; - width: 100%; - } - - .data-view-header-area-rich-text v-line > div { - flex-grow: 1; - } - - .data-view-header-area-icon { - height: max-content; - display: flex; - align-items: center; - margin-right: 8px; - padding: 2px; - border-radius: 4px; - margin-top: 2px; - background-color: var(--affine-background-secondary-color); - } - - .data-view-header-area-icon svg { - width: 14px; - height: 14px; - fill: var(--affine-icon-color); - color: var(--affine-icon-color); - } -`; - -abstract class BaseTextCell extends BaseCellRenderer { - static override styles = styles; - +export class HeaderAreaTextCell extends BaseCellRenderer { activity = true; docId$ = signal(); - isLinkedDoc$ = computed(() => false); - - linkedDocTitle$ = computed(() => { - if (!this.docId$.value) { - return this.value; - } - const doc = this.host?.std.workspace.getDoc(this.docId$.value); - const root = doc?.root as RootBlockModel; - return root.title; - }); - - get attributeRenderer() { - return this.inlineManager?.getRenderer(); - } - - get attributesSchema() { - return this.inlineManager?.getSchema(); - } - get host() { return this.view.contextGet(HostContextKey); } get inlineEditor() { - return this.richText.inlineEditor; + return this.richText.value?.inlineEditor; } get inlineManager() { @@ -128,80 +52,10 @@ abstract class BaseTextCell extends BaseCellRenderer { return databaseBlock?.topContenteditableElement; } - override connectedCallback() { - super.connectedCallback(); - const yText = this.value?.yText; - if (yText) { - const cb = () => { - const id = getSingleDocIdFromText(this.value); - this.docId$.value = id; - }; - cb(); - if (this.activity) { - yText.observe(cb); - this.disposables.add(() => { - yText.unobserve(cb); - }); - } - } + get std() { + return this.view.contextGet(HostContextKey)?.std; } - protected override render(): unknown { - return html`${this.renderIcon()}${this.renderBlockText()}`; - } - - abstract renderBlockText(): TemplateResult; - - renderIcon() { - if (this.docId$.value) { - return html`
- ${LinkedPageIcon()} -
`; - } - 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`
${icon}
`; - } - - abstract renderLinkedDoc(): TemplateResult; - - @query('rich-text') - accessor richText!: RichText; - - @property({ attribute: false }) - accessor showIcon = false; -} - -export class HeaderAreaTextCell extends BaseTextCell { - override renderBlockText() { - return html` `; - } - - override renderLinkedDoc(): TemplateResult { - return html` `; - } -} - -export class HeaderAreaTextCellEditing extends BaseTextCell { private readonly _onCopy = (e: ClipboardEvent) => { const inlineEditor = this.inlineEditor; if (!inlineEditor) return; @@ -318,8 +172,6 @@ export class HeaderAreaTextCellEditing extends BaseTextCell { } }; - override activity = false; - insertDelta = (delta: DeltaInsert) => { const inlineEditor = this.inlineEditor; const range = inlineEditor?.getInlineRange(); @@ -333,12 +185,25 @@ export class HeaderAreaTextCellEditing extends BaseTextCell { }); }; - private get std() { - return this.host?.std; - } - override connectedCallback() { super.connectedCallback(); + this.classList.add(titleCellStyle); + + const yText = this.value?.yText; + if (yText) { + const cb = () => { + const id = getSingleDocIdFromText(this.value); + this.docId$.value = id; + }; + cb(); + if (this.activity) { + yText.observe(cb); + this.disposables.add(() => { + yText.unobserve(cb); + }); + } + } + const selectAll = (e: KeyboardEvent) => { if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) { e.stopPropagation(); @@ -346,82 +211,86 @@ export class HeaderAreaTextCellEditing extends BaseTextCell { this.inlineEditor?.selectAll(); } }; + this.addEventListener('keydown', selectAll); - this.disposables.add(() => { - this.removeEventListener('keydown', selectAll); - }); + this.disposables.addFromEvent(this, 'keydown', selectAll); } override firstUpdated(props: Map) { super.firstUpdated(props); - if (!this.isLinkedDoc$.value) { - this.disposables.addFromEvent(this.richText, 'copy', this._onCopy); - this.disposables.addFromEvent(this.richText, 'cut', this._onCut); - this.disposables.addFromEvent(this.richText, 'paste', this._onPaste); - } - this.richText.updateComplete + this.richText.value?.updateComplete .then(() => { - this.inlineEditor?.focusEnd(); - - this.disposables.add( - effect(() => { - const inlineRange = this.inlineEditor?.inlineRange$.value; - if (inlineRange) { - if (!this.isEditing) { - this.selectCurrentCell(true); - } - } else { - if (this.isEditing) { - this.selectCurrentCell(false); - } - } - }) + this.disposables.addFromEvent( + this.richText.value, + 'copy', + this._onCopy + ); + this.disposables.addFromEvent(this.richText.value, 'cut', this._onCut); + this.disposables.addFromEvent( + this.richText.value, + 'paste', + this._onPaste ); }) .catch(console.error); } - override renderBlockText() { + override afterEnterEditingMode() { + this.inlineEditor?.focusEnd(); + } + + protected override render(): unknown { + return html`${this.renderIcon()}${this.renderBlockText()}`; + } + + renderBlockText() { return html` `; } - override renderLinkedDoc(): TemplateResult { - return html` `; + renderIcon() { + if (!this.showIcon) { + return; + } + if (this.docId$.value) { + return html`
+ ${LinkedPageIcon({})} +
`; + } + 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`
${icon}
`; } + + private readonly richText = createRef(); + + @property({ attribute: false }) + accessor showIcon = false; } declare global { interface HTMLElementTagNameMap { 'data-view-header-area-text': HeaderAreaTextCell; - 'data-view-header-area-text-editing': HeaderAreaTextCellEditing; } } diff --git a/blocksuite/affine/data-view/package.json b/blocksuite/affine/data-view/package.json index 3cb80ec634..40db67fae8 100644 --- a/blocksuite/affine/data-view/package.json +++ b/blocksuite/affine/data-view/package.json @@ -25,6 +25,7 @@ "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.12", "@types/lodash-es": "^4.17.12", + "@vanilla-extract/css": "^1.17.0", "date-fns": "^4.0.0", "lit": "^3.2.0", "lodash-es": "^4.17.21", diff --git a/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts b/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts index f40076ee0e..ab20bc5afe 100644 --- a/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts +++ b/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts @@ -20,8 +20,9 @@ import { flip, offset, shift } from '@floating-ui/dom'; import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import { cssVarV2 } from '@toeverything/theme/v2'; 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 { createRef, ref } from 'lit/directives/ref.js'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; import { html } from 'lit/static-html.js'; @@ -37,7 +38,22 @@ import { import { verticalListSortingStrategy } from '../../utils/wc-dnd/sort/strategies/index.js'; import { arrayMove } from '../../utils/wc-dnd/utils/array-move.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 = { value: string; @@ -70,23 +86,23 @@ class TagManager { ); }; - color = signal(getTagColor()); + color$ = signal(getTagColor()); createOption = () => { - const value = this.text.value.trim(); + const value = this.text$.value.trim(); if (value === '') return; const id = nanoid(); this.ops.onOptionsChange([ { id: id, value: value, - color: this.color.value, + color: this.color$.value, }, ...this.ops.options.value, ]); this.selectTag(id); - this.text.value = ''; - this.color.value = getTagColor(); + this.text$.value = ''; + this.color$.value = getTagColor(); if (this.isSingleMode) { this.ops.onComplete?.(); } @@ -101,12 +117,12 @@ class TagManager { filteredOptions$ = computed(() => { let matched = false; const options: RenderOption[] = []; - for (const option of this.options.value) { + for (const option of this.options$.value) { if ( - !this.text.value || + !this.text$.value || option.value .toLocaleLowerCase() - .includes(this.text.value.toLocaleLowerCase()) + .includes(this.text$.value.toLocaleLowerCase()) ) { options.push({ ...option, @@ -114,15 +130,15 @@ class TagManager { select: () => this.selectTag(option.id), }); } - if (option.value === this.text.value) { + if (option.value === this.text$.value) { matched = true; } } - if (this.text.value && !matched) { + if (this.text$.value && !matched) { options.push({ id: 'create', - color: this.color.value, - value: this.text.value, + color: this.color$.value, + value: this.text$.value, isCreate: true, select: this.createOption, }); @@ -136,37 +152,37 @@ class TagManager { ); }); - text = signal(''); + text$ = signal(''); get isSingleMode() { return this.ops.mode === 'single'; } - get options() { + get options$() { return this.ops.options; } - get value() { + get value$() { return this.ops.value; } constructor(private readonly ops: TagManagerOptions) {} 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) { - return this.value.value.includes(id); + return this.value$.value.includes(id); } selectTag(id: string) { if (this.isSelected(id)) { return; } - const newValue = this.isSingleMode ? [id] : [...this.value.value, id]; + const newValue = this.isSingleMode ? [id] : [...this.value$.value, id]; this.ops.onChange(newValue); - this.text.value = ''; + this.text$.value = ''; if (this.isSingleMode) { requestAnimationFrame(() => { this.ops.onComplete?.(); @@ -178,8 +194,6 @@ class TagManager { export class MultiTagSelect extends SignalWatcher( WithDisposable(ShadowlessElement) ) { - static override styles = styles; - private readonly _clickItemOption = (e: MouseEvent, id: string) => { e.stopPropagation(); const option = this.options.value.find(v => v.id === id); @@ -236,7 +250,7 @@ export class MultiTagSelect extends SignalWatcher( }; 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) => { @@ -251,10 +265,10 @@ export class MultiTagSelect extends SignalWatcher( this.selectedTag$.value?.select(); } else if (event.key === 'ArrowUp') { event.preventDefault(); - this.setSelectedOption(this.selectedIndex - 1); + this.setSelectedOption(this.selectedIndex$.value - 1); } else if (event.key === 'ArrowDown') { event.preventDefault(); - this.setSelectedOption(this.selectedIndex + 1); + this.setSelectedOption(this.selectedIndex$.value + 1); } else if (event.key === 'Escape') { this.onComplete(); } @@ -263,7 +277,7 @@ export class MultiTagSelect extends SignalWatcher( private readonly tagManager = new TagManager(this); private readonly selectedTag$ = computed(() => { - return this.tagManager.filteredOptions$.value[this.selectedIndex]; + return this.tagManager.filteredOptions$.value[this.selectedIndex$.value]; }); sortContext = createSortContext({ @@ -301,12 +315,12 @@ export class MultiTagSelect extends SignalWatcher( }); private get text() { - return this.tagManager.text; + return this.tagManager.text$; } private renderInput() { return html` -
+
${this.value.value.map(id => { const option = this.tagManager.optionsMap$.value.get(id); if (!option) { @@ -317,7 +331,8 @@ export class MultiTagSelect extends SignalWatcher( ); })} -
${name}
+ return html`
+
${name}
${onDelete - ? html`
+ ? html`
${CloseIcon()}
` : nothing} @@ -349,19 +364,19 @@ export class MultiTagSelect extends SignalWatcher( 'layer/insideBorder/border' )};margin: 4px 0;" >
-
Select tag or create one
-
+
Select tag or create one
+
${repeat( this.tagManager.filteredOptions$.value, select => select.id, (select, index) => { - const isSelected = index === this.selectedIndex; + const isSelected = index === this.selectedIndex$.value; const mouseenter = () => { this.setSelectedOption(index); }; const classes = classMap({ - 'select-option': true, - selected: isSelected, + [selectOptionStyle]: true, + [selectedStyle]: isSelected, }); const clickOption = (e: MouseEvent) => { e.stopPropagation(); @@ -374,21 +389,24 @@ export class MultiTagSelect extends SignalWatcher( @mouseenter="${mouseenter}" @click="${select.select}" > -
+
${select.isCreate - ? html`
Create
` + ? html`
+ Create +
` : html`
`} ${this.renderTag(select.value, select.color)}
${!select.isCreate ? html`
${MoreHorizontalIcon()}
` @@ -402,7 +420,7 @@ export class MultiTagSelect extends SignalWatcher( } private setSelectedOption(index: number) { - this.selectedIndex = rangeWrap( + this.selectedIndex$.value = rangeWrap( index, 0, this.tagManager.filteredOptions$.value.length @@ -410,28 +428,29 @@ export class MultiTagSelect extends SignalWatcher( } protected override firstUpdated() { + const disposables = this.disposables; + this.classList.add(tagSelectContainerStyle); requestAnimationFrame(() => { - this._selectInput.focus(); + this._selectInput.value?.focus(); }); - this._disposables.addFromEvent(this, 'click', () => { - this._selectInput.focus(); + disposables.addFromEvent(this, 'click', () => { + this._selectInput.value?.focus(); }); - this._disposables.addFromEvent(this._selectInput, 'copy', e => { + disposables.addFromEvent(this._selectInput.value, 'copy', e => { e.stopPropagation(); }); - this._disposables.addFromEvent(this._selectInput, 'cut', e => { + disposables.addFromEvent(this._selectInput.value, 'cut', e => { e.stopPropagation(); }); } override render() { - this.setSelectedOption(this.selectedIndex); + this.setSelectedOption(this.selectedIndex$.value); return html` ${this.renderInput()} ${this.renderTags()} `; } - @query('.tag-select-input') - private accessor _selectInput!: HTMLInputElement; + private readonly _selectInput = createRef(); @property() accessor mode: 'multi' | 'single' = 'multi'; @@ -448,8 +467,7 @@ export class MultiTagSelect extends SignalWatcher( @property({ attribute: false }) accessor options!: ReadonlySignal; - @state() - private accessor selectedIndex = 0; + private readonly selectedIndex$ = signal(0); @property({ attribute: false }) accessor value!: ReadonlySignal; @@ -464,7 +482,7 @@ declare global { const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => { const tagManager = new TagManager(ops); const onInput = (e: InputEvent) => { - tagManager.text.value = (e.target as HTMLInputElement).value; + tagManager.text$.value = (e.target as HTMLInputElement).value; }; return popMenu(target, { options: { @@ -491,12 +509,12 @@ const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => { backgroundColor: option.color, width: 'max-content', }); - return html`
-
${option.value}
+ return html`
+
${option.value}
`; })} { ${option.isCreate ? html`
Create
` : ''} -
-
${option.value}
+
+
${option.value}
`; diff --git a/blocksuite/affine/data-view/src/core/component/tags/multi-tag-view.ts b/blocksuite/affine/data-view/src/core/component/tags/multi-tag-view.ts index d9b5f690d2..77ae43eba0 100644 --- a/blocksuite/affine/data-view/src/core/component/tags/multi-tag-view.ts +++ b/blocksuite/affine/data-view/src/core/component/tags/multi-tag-view.ts @@ -60,7 +60,10 @@ export class MultiTagView extends WithDisposable(ShadowlessElement) { const style = styleMap({ backgroundColor: getColorByColor(option.color), }); - return html`${option.value}`; })} diff --git a/blocksuite/affine/data-view/src/core/component/tags/styles.css.ts b/blocksuite/affine/data-view/src/core/component/tags/styles.css.ts new file mode 100644 index 0000000000..cdc44fcfdf --- /dev/null +++ b/blocksuite/affine/data-view/src/core/component/tags/styles.css.ts @@ -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', +}); diff --git a/blocksuite/affine/data-view/src/core/component/tags/styles.ts b/blocksuite/affine/data-view/src/core/component/tags/styles.ts deleted file mode 100644 index 6d710c1de8..0000000000 --- a/blocksuite/affine/data-view/src/core/component/tags/styles.ts +++ /dev/null @@ -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; - // } -`; diff --git a/blocksuite/affine/data-view/src/core/detail/field.ts b/blocksuite/affine/data-view/src/core/detail/field.ts index 34c7327833..9fc02e04d7 100644 --- a/blocksuite/affine/data-view/src/core/detail/field.ts +++ b/blocksuite/affine/data-view/src/core/detail/field.ts @@ -237,18 +237,18 @@ export class RecordField extends SignalWatcher( const props: CellRenderProps = { cell: this.cell$.value, - isEditing: this.editing, + isEditing$: this.isEditing$, selectCurrentCell: this.changeEditing, }; const renderer = this.column.renderer$.value; if (!renderer) { return; } - const { view, edit } = renderer; + const { view } = renderer; const contentClass = classMap({ 'field-content': true, - empty: !this.editing && this.cell$.value.isEmpty$.value, - 'is-editing': this.editing, + empty: !this.isEditing$.value && this.cell$.value.isEmpty$.value, + 'is-editing': this.isEditing$.value, 'is-focus': this.isFocus, }); return html` @@ -261,7 +261,7 @@ export class RecordField extends SignalWatcher(
- ${renderUniLit(this.editing && edit ? edit : view, props, { + ${renderUniLit(view, props, { ref: this._cell, class: 'kanban-cell', })} @@ -269,8 +269,7 @@ export class RecordField extends SignalWatcher( `; } - @state() - accessor editing = false; + isEditing$ = signal(false); @state() accessor isFocus = false; diff --git a/blocksuite/affine/data-view/src/core/detail/selection.ts b/blocksuite/affine/data-view/src/core/detail/selection.ts index 2493b6111f..e16ca31e74 100644 --- a/blocksuite/affine/data-view/src/core/detail/selection.ts +++ b/blocksuite/affine/data-view/src/core/detail/selection.ts @@ -61,13 +61,11 @@ export class DetailSelection { const cell = container.cell; if (selection.isEditing) { - requestAnimationFrame(() => { - cell?.onExitEditMode(); - }); + cell?.beforeExitEditingMode(); if (cell?.blurCell()) { container.blur(); } - container.editing = false; + container.isEditing$.value = false; } else { container.blur(); } @@ -85,11 +83,13 @@ export class DetailSelection { container.isFocus = true; const cell = container.cell; if (selection.isEditing) { - cell?.onEnterEditMode(); if (cell?.focusCell()) { container.focus(); } - container.editing = true; + container.isEditing$.value = true; + requestAnimationFrame(() => { + cell?.afterEnterEditingMode(); + }); } else { container.focus(); } diff --git a/blocksuite/affine/data-view/src/core/property/base-cell.ts b/blocksuite/affine/data-view/src/core/property/base-cell.ts index f2c7d1dd9c..18a88a6a16 100644 --- a/blocksuite/affine/data-view/src/core/property/base-cell.ts +++ b/blocksuite/affine/data-view/src/core/property/base-cell.ts @@ -1,6 +1,7 @@ import { ShadowlessElement } from '@blocksuite/block-std'; 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 type { Cell } from '../view-manager/cell.js'; @@ -56,29 +57,37 @@ export abstract class BaseCellRenderer< return true; } + type: string | undefined; + + protected override shouldUpdate(_changedProperties: PropertyValues): boolean { + return this.cell.property.type$.value === this.type; + } + override connectedCallback() { super.connectedCallback(); + this.type = this.cell.property.type$.value; + this.dataset.testid = this.type; this.style.width = '100%'; this._disposables.addFromEvent(this, 'click', e => { - if (this.isEditing) { + if (this.isEditing$.value) { e.stopPropagation(); } }); this._disposables.addFromEvent(this, 'copy', e => { - if (!this.isEditing) return; + if (!this.isEditing$.value) return; e.stopPropagation(); this.onCopy(e); }); this._disposables.addFromEvent(this, 'cut', e => { - if (!this.isEditing) return; + if (!this.isEditing$.value) return; e.stopPropagation(); this.onCut(e); }); this._disposables.addFromEvent(this, 'paste', e => { - if (!this.isEditing) return; + if (!this.isEditing$.value) return; e.stopPropagation(); this.onPaste(e); }); @@ -92,26 +101,32 @@ export abstract class BaseCellRenderer< this.requestUpdate(); } - onChange(value: Value | undefined): void { + valueSetImmediate(value: Value | undefined): void { this.cell.valueSet(value); } + valueSetNextTick(value: Value | undefined) { + requestAnimationFrame(() => { + this.cell.valueSet(value); + }); + } + onCopy(_e: ClipboardEvent) {} onCut(_e: ClipboardEvent) {} - onEnterEditMode(): void { + afterEnterEditingMode(): void { // do nothing } - onExitEditMode() { + beforeExitEditingMode() { // do nothing } onPaste(_e: ClipboardEvent) {} @property({ attribute: false }) - accessor isEditing!: boolean; + accessor isEditing$!: ReadonlySignal; @property({ attribute: false }) accessor selectCurrentCell!: (editing: boolean) => void; diff --git a/blocksuite/affine/data-view/src/core/property/manager.ts b/blocksuite/affine/data-view/src/core/property/manager.ts index d4345d4ba4..657d002b1a 100644 --- a/blocksuite/affine/data-view/src/core/property/manager.ts +++ b/blocksuite/affine/data-view/src/core/property/manager.ts @@ -1,4 +1,5 @@ import type { UniComponent } from '@blocksuite/affine-shared/types'; +import type { ReadonlySignal } from '@preact/signals-core'; import type { Cell } from '../view-manager/cell.js'; @@ -7,16 +8,15 @@ export interface CellRenderProps< Value = unknown, > { cell: Cell; - isEditing: boolean; + isEditing$: ReadonlySignal; selectCurrentCell: (editing: boolean) => void; } export interface DataViewCellLifeCycle { beforeEnterEditMode(): boolean; + beforeExitEditingMode(): void; - onEnterEditMode(): void; - - onExitEditMode(): void; + afterEnterEditingMode(): void; focusCell(): boolean; @@ -35,5 +35,4 @@ export type CellRenderer< Value = unknown, > = { view: DataViewCellComponent; - edit?: DataViewCellComponent; }; diff --git a/blocksuite/affine/data-view/src/effects.ts b/blocksuite/affine/data-view/src/effects.ts index fff93b3f80..3c8b527cd9 100644 --- a/blocksuite/affine/data-view/src/effects.ts +++ b/blocksuite/affine/data-view/src/effects.ts @@ -14,31 +14,13 @@ import { GroupSetting } from './core/group-by/setting.js'; import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js'; import { AnyRender } from './core/utils/uni-component/render-template.js'; import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js'; -import { - DateCell, - DateCellEditing, -} from './property-presets/date/cell-renderer.js'; +import { DateCell } from './property-presets/date/cell-renderer.js'; import { TextCell as ImageTextCell } from './property-presets/image/cell-renderer.js'; -import { - MultiSelectCell, - MultiSelectCellEditing, -} from './property-presets/multi-select/cell-renderer.js'; -import { - 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 { MultiSelectCell } from './property-presets/multi-select/cell-renderer.js'; +import { NumberCell } from './property-presets/number/cell-renderer.js'; +import { ProgressCell } from './property-presets/progress/cell-renderer.js'; +import { SelectCell } from './property-presets/select/cell-renderer.js'; +import { TextCell } from './property-presets/text/cell-renderer.js'; import { DataViewKanban, DataViewTable } from './view-presets/index.js'; import { MobileKanbanCard } from './view-presets/kanban/mobile/card.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() { customElements.define('affine-database-progress-cell', ProgressCell); - customElements.define( - 'affine-database-progress-cell-editing', - ProgressCellEditing - ); customElements.define('data-view-header-tools', DataViewHeaderTools); customElements.define('affine-database-number-cell', NumberCell); - customElements.define( - 'affine-database-number-cell-editing', - NumberCellEditing - ); customElements.define( 'affine-database-cell-container', DatabaseCellContainer @@ -102,24 +76,14 @@ export function effects() { customElements.define('any-render', AnyRender); customElements.define('affine-database-image-cell', ImageTextCell); customElements.define('affine-database-date-cell', DateCell); - customElements.define('affine-database-date-cell-editing', DateCellEditing); customElements.define( 'data-view-properties-setting', DataViewPropertiesSettingView ); customElements.define('affine-database-checkbox-cell', CheckboxCell); 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-editing', - SelectCellEditing - ); 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('data-view-drag-to-fill', DragToFillElement); customElements.define('affine-data-view-table-group', TableGroup); diff --git a/blocksuite/affine/data-view/src/property-presets/checkbox/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/checkbox/cell-renderer.ts index d0f4874965..707254548b 100644 --- a/blocksuite/affine/data-view/src/property-presets/checkbox/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/checkbox/cell-renderer.ts @@ -75,7 +75,7 @@ export class CheckboxCell extends BaseCellRenderer { override beforeEnterEditMode() { const checked = !this.value; - this.onChange(checked); + this.valueSetImmediate(checked); if (checked) { playCheckAnimation(this._checkbox, { left: 2 }).catch(console.error); } diff --git a/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.css.ts b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.css.ts new file mode 100644 index 0000000000..b4ae5af0e7 --- /dev/null +++ b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.css.ts @@ -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', +}); diff --git a/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts index e4a131b9b6..800bece17a 100644 --- a/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts @@ -4,66 +4,23 @@ import { } from '@blocksuite/affine-components/context-menu'; import { DatePicker } from '@blocksuite/affine-components/date-picker'; import { createLitPortal } from '@blocksuite/affine-components/portal'; -import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { IS_MOBILE } from '@blocksuite/global/env'; import { flip, offset } from '@floating-ui/dom'; -import { signal } from '@preact/signals-core'; -import { baseTheme } from '@toeverything/theme'; +import { computed, signal } from '@preact/signals-core'; import { format } from 'date-fns/format'; -import { css, html, unsafeCSS } from 'lit'; +import { html } from 'lit'; import { BaseCellRenderer } from '../../core/property/index.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createIcon } from '../../core/utils/uni-icon.js'; +import { + dateCellStyle, + datePickerContainerStyle, + dateValueContainerStyle, +} from './cell-renderer.css.js'; import { datePropertyModelConfig } from './define.js'; export class DateCell extends BaseCellRenderer { - 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`
${value}
`; - } -} - -export class DateCellEditing extends BaseCellRenderer { - 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 readonly openDatePicker = () => { @@ -96,18 +53,8 @@ export class DateCellEditing extends BaseCellRenderer { }, items: [ () => - html`
- ${this.dateString} + html`
+ ${this.formattedTempValue$.value}
`, () => { const datePicker = new DatePicker(); @@ -123,11 +70,7 @@ height: 46px; abortController.abort(); }; requestAnimationFrame(() => datePicker.focusDateCell()); - return html`
+ return html`
${datePicker}
`; }, @@ -179,36 +122,39 @@ height: 46px; return; } - this.onChange(tempValue?.getTime()); + const time = tempValue?.getTime(); + this.valueSetNextTick(time); this.tempValue$.value = undefined; }; - tempValue$ = signal(); + tempValue$ = signal(); - get dateString() { - const value = this.tempValue; + format(value?: Date) { return value ? format(value, 'yyyy/MM/dd') : ''; } - get tempValue() { - return this.tempValue$.value; - } + formattedTempValue$ = computed(() => { + 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(); } - override onExitEditMode() { + override beforeExitEditingMode() { this.updateValue(); this._prevPortalAbortController?.abort(); } override render() { - return html`
- ${this.dateString} + return html`
+ ${this.formattedValue$.value}
`; } } @@ -217,6 +163,5 @@ export const datePropertyConfig = datePropertyModelConfig.createPropertyMeta({ icon: createIcon('DateTimeIcon'), cellRenderer: { view: createFromBaseCellRenderer(DateCell), - edit: createFromBaseCellRenderer(DateCellEditing), }, }); diff --git a/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.css.ts b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.css.ts new file mode 100644 index 0000000000..a4286fd49e --- /dev/null +++ b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.css.ts @@ -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', +}); diff --git a/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts index 5e67b252e8..f110a3e1f6 100644 --- a/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts @@ -1,53 +1,33 @@ 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 { popTagSelect } from '../../core/component/tags/multi-tag-select.js'; import type { SelectTag } from '../../core/index.js'; import { BaseCellRenderer } from '../../core/property/index.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; +import { stopPropagation } from '../../core/utils/event.js'; import { createIcon } from '../../core/utils/uni-icon.js'; import type { SelectPropertyData } from '../select/define.js'; +import { multiSelectStyle } from './cell-renderer.css.js'; import { multiSelectPropertyModelConfig } from './define.js'; export class MultiSelectCell extends BaseCellRenderer< string[], SelectPropertyData > { - override render() { - return html` - - `; - } -} - -export class MultiSelectCellEditing extends BaseCellRenderer< - string[], - SelectPropertyData -> { + closePopup?: () => void; private readonly popTagSelect = () => { - const value = signal(this._value); - this._disposables.add({ - dispose: popTagSelect( - popupTargetFromElement( - this.querySelector('affine-multi-tag-view') ?? this - ), - { - name: this.cell.property.name$.value, - options: this.options$, - onOptionsChange: this._onOptionsChange, - value: value, - onChange: v => { - this._onChange(v); - value.value = v; - }, - onComplete: this._editComplete, - minWidth: 400, - } - ), + this.closePopup = popTagSelect(popupTargetFromElement(this), { + name: this.cell.property.name$.value, + options: this.options$, + onOptionsChange: this._onOptionsChange, + value: this._value$, + onChange: v => { + this.valueSetImmediate(v); + }, + onComplete: this._editComplete, + minWidth: 400, }); }; @@ -55,10 +35,6 @@ export class MultiSelectCellEditing extends BaseCellRenderer< this.selectCurrentCell(false); }; - _onChange = (ids: string[]) => { - this.onChange(ids); - }; - _onOptionsChange = (options: SelectTag[]) => { this.property.dataUpdate(data => { return { @@ -71,21 +47,32 @@ export class MultiSelectCellEditing extends BaseCellRenderer< options$ = computed(() => { return this.property.data$.value.options; }); - - get _value() { + _value$ = computed(() => { return this.value ?? []; + }); + + override afterEnterEditingMode() { + if (!this.closePopup) { + this.popTagSelect(); + } } - override firstUpdated() { - this.popTagSelect(); + override beforeExitEditingMode() { + this.closePopup?.(); + this.closePopup = undefined; } override render() { return html` - +
+ +
`; } } @@ -95,6 +82,5 @@ export const multiSelectPropertyConfig = icon: createIcon('MultiSelectIcon'), cellRenderer: { view: createFromBaseCellRenderer(MultiSelectCell), - edit: createFromBaseCellRenderer(MultiSelectCellEditing), }, }); diff --git a/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.css.ts b/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.css.ts new file mode 100644 index 0000000000..a387df76ea --- /dev/null +++ b/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.css.ts @@ -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', + }, +}); diff --git a/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts index dd21a20052..f0163fd814 100644 --- a/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts @@ -1,12 +1,12 @@ import { IS_MAC } from '@blocksuite/global/env'; -import { baseTheme } from '@toeverything/theme'; -import { css, html, unsafeCSS } from 'lit'; +import { html } from 'lit'; import { query } from 'lit/decorators.js'; import { BaseCellRenderer } from '../../core/property/index.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { stopPropagation } from '../../core/utils/event.js'; import { createIcon } from '../../core/utils/uni-icon.js'; +import { numberInputStyle, numberStyle } from './cell-renderer.css.js'; import { numberPropertyModelConfig } from './define.js'; import type { NumberPropertyDataType } from './types.js'; import { @@ -19,93 +19,23 @@ export class NumberCell extends BaseCellRenderer< number, NumberPropertyDataType > { - static override styles = css` - affine-database-number-cell { - display: block; - width: 100%; - } + @query('input') + private accessor _inputEle!: HTMLInputElement; - .affine-database-number { - 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() { + private _getFormattedString(value: number | undefined = this.value) { 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 this.value != undefined + return value != undefined ? enableNewFormatting - ? formatNumber(this.value, formatMode, decimals) - : this.value.toString() + ? formatNumber(value, formatMode, decimals) + : value.toString() : ''; } - override render() { - return html`
- ${this._getFormattedString()} -
`; - } -} - -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) => { 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) { - this.onChange(undefined); + this.valueSetNextTick(undefined); return; } @@ -131,17 +61,23 @@ export class NumberCellEditing extends BaseCellRenderer< this.view.featureFlags$.value.enable_number_formatting; const value = enableNewFormatting ? parseNumber(str) : parseFloat(str); if (isNaN(value)) { - this._inputEle.value = this.value - ? this._getFormattedString(this.value) - : ''; + if (this._inputEle) { + this._inputEle.value = this.value + ? this._getFormattedString(this.value) + : ''; + } return; } - this._inputEle.value = this._getFormattedString(value); - this.onChange(value); + if (this._inputEle) { + this._inputEle.value = this._getFormattedString(value); + } + this.valueSetNextTick(value); }; focusEnd = () => { + if (!this._inputEle) return; + const end = this._inputEle.value.length; this._inputEle.focus(); this._inputEle.setSelectionRange(end, end); @@ -152,38 +88,39 @@ export class NumberCellEditing extends BaseCellRenderer< } _focus() { - if (!this.isEditing) { + if (!this.isEditing$.value) { this.selectCurrentCell(true); } } - override firstUpdated() { - requestAnimationFrame(() => { - this.focusEnd(); - }); + override afterEnterEditingMode() { + this.focusEnd(); } - override onExitEditMode() { + override beforeExitEditingMode() { this._setValue(); } override render() { - const formatted = this.value ? this._getFormattedString(this.value) : ''; + if (this.isEditing$.value) { + const formatted = this.value ? this._getFormattedString(this.value) : ''; - return html``; + return html``; + } else { + return html`
+ ${this._getFormattedString()} +
`; + } } - - @query('input') - private accessor _inputEle!: HTMLInputElement; } export const numberPropertyConfig = @@ -191,6 +128,5 @@ export const numberPropertyConfig = icon: createIcon('NumberIcon'), cellRenderer: { view: createFromBaseCellRenderer(NumberCell), - edit: createFromBaseCellRenderer(NumberCellEditing), }, }); diff --git a/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.css.ts b/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.css.ts new file mode 100644 index 0000000000..4a1ca5f07c --- /dev/null +++ b/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.css.ts @@ -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, +}); diff --git a/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts index 157192f5fa..2987ed7974 100644 --- a/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts @@ -1,4 +1,4 @@ -import { css, html } from 'lit'; +import { html } from 'lit'; import { query, state } from 'lit/decorators.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 { startDrag } from '../../core/utils/drag.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'; -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 = { empty: 'var(--affine-black-10)', processing: 'var(--affine-processing-color)', @@ -76,38 +24,9 @@ const progressColors = { }; export class ProgressCell extends BaseCellRenderer { - 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`
-
-
-
-
-
-
${progress}
-
`; - } -} - -export class ProgressCellEditing extends BaseCellRenderer { - static override styles = styles; - startDrag = (event: MouseEvent) => { + if (!this.isEditing$.value) return; + const bgRect = this._progressBg.getBoundingClientRect(); const min = bgRect.left; const max = bgRect.right; @@ -135,7 +54,9 @@ export class ProgressCellEditing extends BaseCellRenderer { }; get _value() { - return this.tempValue ?? this.value ?? 0; + return this.isEditing$.value + ? (this.tempValue ?? this.value ?? 0) + : (this.value ?? 0); } _onChange(value?: number) { @@ -146,13 +67,17 @@ export class ProgressCellEditing extends BaseCellRenderer { const disposables = this._disposables; disposables.addFromEvent(this._progressBg, 'pointerdown', this.startDrag); + disposables.addFromEvent(window, 'keydown', evt => { - if (evt.key === 'ArrowDown') { + if (!this.isEditing$.value) { + return; + } + if (evt.key === 'ArrowDown' || evt.key === 'ArrowLeft') { evt.preventDefault(); this._onChange(Math.max(0, this._value - 1)); return; } - if (evt.key === 'ArrowUp') { + if (evt.key === 'ArrowUp' || evt.key === 'ArrowRight') { evt.preventDefault(); this._onChange(Math.min(100, this._value + 1)); return; @@ -160,20 +85,25 @@ export class ProgressCellEditing extends BaseCellRenderer { }); } + preventDefault(e: ClipboardEvent) { + e.stopPropagation(); + } + override onCopy(_e: ClipboardEvent) { - _e.preventDefault(); + this.preventDefault(_e); } override onCut(_e: ClipboardEvent) { - _e.preventDefault(); + this.preventDefault(_e); } - override onExitEditMode() { - this.onChange(this._value); + override beforeExitEditingMode() { + const value = this._value; + this.valueSetNextTick(value); } override onPaste(_e: ClipboardEvent) { - _e.preventDefault(); + this.preventDefault(_e); } protected override render() { @@ -190,25 +120,37 @@ export class ProgressCellEditing extends BaseCellRenderer { backgroundColor: progress === 0 ? progressColors.empty : 'var(--affine-hover-color)', }); - const handleStyles = styleMap({ - left: `calc(${progress}% - 3px)`, - }); - return html`
-
-
-
-
+ return html` +
+
+
+
+
+ ${this.isEditing$.value + ? html`
` + : ''} +
+
+ ${progress}
-
${progress}
-
`; + `; } - @query('.affine-database-progress-bg') + @query(`.${progressBgStyle}`) private accessor _progressBg!: HTMLElement; @state() @@ -220,6 +162,5 @@ export const progressPropertyConfig = icon: createIcon('ProgressIcon'), cellRenderer: { view: createFromBaseCellRenderer(ProgressCell), - edit: createFromBaseCellRenderer(ProgressCellEditing), }, }); diff --git a/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.css.ts b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.css.ts new file mode 100644 index 0000000000..ea8dfe25d5 --- /dev/null +++ b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.css.ts @@ -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', +}); diff --git a/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts index bfdf9ba89a..39e4d5c37a 100644 --- a/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts @@ -1,5 +1,5 @@ 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 { 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 { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createIcon } from '../../core/utils/uni-icon.js'; +import { selectStyle } from './cell-renderer.css.js'; import { type SelectPropertyData, selectPropertyModelConfig, } from './define.js'; -export class SelectCell extends BaseCellRenderer { - override render() { - const value = this.value ? [this.value] : []; - return html` - - `; - } -} - -export class SelectCellEditing extends BaseCellRenderer< - string, - SelectPropertyData -> { +export class SelectCell extends BaseCellRenderer { + closePopup?: () => void; private readonly popTagSelect = () => { - const value = signal(this._value); - this._disposables.add({ - dispose: popTagSelect( - popupTargetFromElement( - this.querySelector('affine-multi-tag-view') ?? this - ), - { - name: this.cell.property.name$.value, - mode: 'single', - options: this.options$, - onOptionsChange: this._onOptionsChange, - value: signal(this._value), - onChange: v => { - this._onChange(v); - value.value = v; - }, - onComplete: this._editComplete, - minWidth: 400, - } - ), + this.closePopup = popTagSelect(popupTargetFromElement(this), { + name: this.cell.property.name$.value, + mode: 'single', + options: this.options$, + onOptionsChange: this._onOptionsChange, + value: this._value$, + onChange: v => { + this.valueSetImmediate(v[0]); + }, + onComplete: this._editComplete, + minWidth: 400, }); }; @@ -56,10 +34,6 @@ export class SelectCellEditing extends BaseCellRenderer< this.selectCurrentCell(false); }; - _onChange = ([id]: string[]) => { - this.onChange(id); - }; - _onOptionsChange = (options: SelectTag[]) => { this.property.dataUpdate(data => { return { @@ -73,21 +47,30 @@ export class SelectCellEditing extends BaseCellRenderer< return this.property.data$.value.options; }); - get _value() { + _value$ = computed(() => { const value = this.value; return value ? [value] : []; + }); + + override afterEnterEditingMode() { + if (!this.closePopup) { + this.popTagSelect(); + } } - override firstUpdated() { - this.popTagSelect(); + override beforeExitEditingMode() { + this.closePopup?.(); + this.closePopup = undefined; } override render() { return html` - +
+ +
`; } } @@ -97,6 +80,5 @@ export const selectPropertyConfig = icon: createIcon('SingleSelectIcon'), cellRenderer: { view: createFromBaseCellRenderer(SelectCell), - edit: createFromBaseCellRenderer(SelectCellEditing), }, }); diff --git a/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.css.ts b/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.css.ts new file mode 100644 index 0000000000..60b828f12c --- /dev/null +++ b/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.css.ts @@ -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', + }, +}); diff --git a/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts index 40149a999f..d7753a5047 100644 --- a/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts @@ -1,74 +1,15 @@ -import { baseTheme } from '@toeverything/theme'; -import { css, html, unsafeCSS } from 'lit'; +import { html } from 'lit'; import { query } from 'lit/decorators.js'; import { BaseCellRenderer } from '../../core/property/index.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createIcon } from '../../core/utils/uni-icon.js'; +import { textInputStyle, textStyle } from './cell-renderer.css.js'; import { textPropertyModelConfig } from './define.js'; export class TextCell extends BaseCellRenderer { - static override styles = css` - affine-database-text-cell { - 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`
${this.value ?? ''}
`; - } -} -export class TextCellEditing extends BaseCellRenderer { - 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; - } - `; + @query('input') + private accessor _inputEle!: HTMLInputElement; private readonly _keydown = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.isComposing) { @@ -79,35 +20,40 @@ export class TextCellEditing extends BaseCellRenderer { } }; - private readonly _setValue = (str: string = this._inputEle.value) => { - this._inputEle.value = `${this.value ?? ''}`; - this.onChange(str); + private readonly _setValue = (str: string = this._inputEle?.value) => { + if (this._inputEle) { + this._inputEle.value = `${this.value ?? ''}`; + } + this.valueSetNextTick(str); }; focusEnd = () => { + if (!this._inputEle) return; + const end = this._inputEle.value.length; this._inputEle.focus(); this._inputEle.setSelectionRange(end, end); }; - override firstUpdated() { + override afterEnterEditingMode() { this.focusEnd(); } - override onExitEditMode() { + override beforeExitEditingMode() { this._setValue(); } override render() { - return html``; + if (this.isEditing$.value) { + return html``; + } else { + return html`
${this.value ?? ''}
`; + } } - - @query('input') - private accessor _inputEle!: HTMLInputElement; } export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({ @@ -115,6 +61,5 @@ export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({ cellRenderer: { view: createFromBaseCellRenderer(TextCell), - edit: createFromBaseCellRenderer(TextCellEditing), }, }); diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts index ef4772c313..97f943679c 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts @@ -5,7 +5,7 @@ import { ShadowlessElement } from '@blocksuite/block-std'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { computed, effect, signal } from '@preact/signals-core'; 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 type { @@ -52,7 +52,7 @@ export class MobileKanbanCell extends SignalWatcher( private readonly _cell = signal(); - isEditing$ = computed(() => { + isSelectionEditing$ = computed(() => { const selection = this.kanban?.props.selection$.value; if (selection?.selectionType !== 'cell') { return false; @@ -108,19 +108,21 @@ export class MobileKanbanCell extends SignalWatcher( if (this.column.readonly$.value) return; this.disposables.add( effect(() => { - const isEditing = this.isEditing$.value; + const isEditing = this.isSelectionEditing$.value; if (isEditing) { - this.isEditing = true; - this._cell.value?.onEnterEditMode(); + this.isEditing$.value = true; + requestAnimationFrame(() => { + this._cell.value?.afterEnterEditingMode(); + }); } else { - this._cell.value?.onExitEditMode(); - this.isEditing = false; + this._cell.value?.beforeExitEditingMode(); + this.isEditing$.value = false; } }) ); this._disposables.addFromEvent(this, 'click', e => { e.stopPropagation(); - if (!this.isEditing) { + if (!this.isEditing$.value) { this.selectCurrentCell(!this.column.readonly$.value); } }); @@ -129,16 +131,16 @@ export class MobileKanbanCell extends SignalWatcher( override render() { const props: CellRenderProps = { cell: this.column.cellGet(this.cardId), - isEditing: this.isEditing, + isEditing$: this.isEditing$, selectCurrentCell: this.selectCurrentCell, }; const renderer = this.column.renderer$.value; if (!renderer) return; - const { view, edit } = renderer; - this.view.lockRows(this.isEditing); - this.dataset['editing'] = `${this.isEditing}`; + const { view } = renderer; + this.view.lockRows(this.isEditing$.value); + this.dataset['editing'] = `${this.isEditing$.value}`; return html` ${this.renderIcon()} - ${renderUniLit(this.isEditing && edit ? edit : view, props, { + ${renderUniLit(view, props, { ref: this._cell, class: 'mobile-kanban-cell', style: { display: 'block', flex: '1', overflow: 'hidden' }, @@ -167,8 +169,7 @@ export class MobileKanbanCell extends SignalWatcher( @property({ attribute: false }) accessor groupKey!: string; - @state() - accessor isEditing = false; + isEditing$ = signal(false); @property({ attribute: false }) accessor view!: KanbanSingleView; diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts index 652610250e..0b29c328d1 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts @@ -109,7 +109,7 @@ export class KanbanCell extends SignalWatcher( if (!selectionElement) return; if (e.shiftKey) return; - if (!this.editing) { + if (!this.isEditing$.value) { this.selectCurrentCell(!this.column.readonly$.value); } }); @@ -130,22 +130,22 @@ export class KanbanCell extends SignalWatcher( override render() { const props: CellRenderProps = { cell: this.column.cellGet(this.cardId), - isEditing: this.editing, + isEditing$: this.isEditing$, selectCurrentCell: this.selectCurrentCell, }; const renderer = this.column.renderer$.value; if (!renderer) return; - const { view, edit } = renderer; - this.view.lockRows(this.editing); - this.dataset['editing'] = `${this.editing}`; + const { view } = renderer; + this.view.lockRows(this.isEditing$.value); + this.dataset['editing'] = `${this.isEditing$.value}`; this.style.border = this.isFocus ? '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)' : ''; return html` ${this.renderIcon()} - ${renderUniLit(this.editing && edit ? edit : view, props, { + ${renderUniLit(view, props, { ref: this._cell, class: 'kanban-cell', style: { display: 'block', flex: '1', overflow: 'hidden' }, @@ -168,8 +168,7 @@ export class KanbanCell extends SignalWatcher( @property({ attribute: false }) accessor contentOnly = false; - @state() - accessor editing = false; + isEditing$ = signal(false); @property({ attribute: false }) accessor groupKey!: string; diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/drag.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/drag.ts index 543cc6ab9d..1b90e2f10f 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/drag.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/drag.ts @@ -153,7 +153,7 @@ export class KanbanDragController implements ReactiveController { const target = event.target; if (target instanceof Element) { const cell = target.closest('affine-data-view-kanban-cell'); - if (cell?.editing) { + if (cell?.isEditing$.value) { return; } cell?.selectCurrentCell(false); diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts index 8c1ae4b4ae..af1f096eb3 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts @@ -96,13 +96,11 @@ export class KanbanSelectionController implements ReactiveController { const cell = container?.cell; if (selection.isEditing) { - requestAnimationFrame(() => { - cell?.onExitEditMode(); - }); + cell?.beforeExitEditingMode(); if (cell?.blurCell()) { container.blur(); } - container.editing = false; + container.isEditing$.value = false; } else { container.blur(); } @@ -142,11 +140,13 @@ export class KanbanSelectionController implements ReactiveController { container.isFocus = true; const cell = container?.cell; if (selection.isEditing) { - cell?.onEnterEditMode(); if (cell?.focusCell()) { container.focus(); } - container.editing = true; + container.isEditing$.value = true; + requestAnimationFrame(() => { + cell?.afterEnterEditingMode(); + }); } else { container.focus(); } diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts index 9314fb0fdd..9359c9cb80 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts @@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { computed, effect, signal } from '@preact/signals-core'; import { css } from 'lit'; -import { property, state } from 'lit/decorators.js'; +import { property } from 'lit/decorators.js'; import { type CellRenderProps, @@ -47,7 +47,7 @@ export class MobileTableCell extends SignalWatcher( return this.column.cellGet(this.rowId); }); - isEditing$ = computed(() => { + isSelectionEditing$ = computed(() => { const selection = this.table?.props.selection$.value; if (selection?.selectionType !== 'area') { return false; @@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher( return this.closest('mobile-table-group')?.group?.key; } - private get readonly() { - return this.column.readonly$.value; - } - private get table() { return this.closest('mobile-data-view-table'); } @@ -110,18 +106,21 @@ export class MobileTableCell extends SignalWatcher( if (this.column.readonly$.value) return; this.disposables.add( effect(() => { - const isEditing = this.isEditing$.value; + const isEditing = this.isSelectionEditing$.value; if (isEditing) { - this.isEditing = true; - this._cell.value?.onEnterEditMode(); + this.isEditing$.value = true; + const cell = this._cell.value; + requestAnimationFrame(() => { + cell?.afterEnterEditingMode(); + }); } else { - this._cell.value?.onExitEditMode(); - this.isEditing = false; + this._cell.value?.beforeExitEditingMode(); + this.isEditing$.value = false; } }) ); this.disposables.addFromEvent(this, 'click', () => { - if (!this.isEditing) { + if (!this.isEditing$.value) { this.selectCurrentCell(!this.column.readonly$.value); } }); @@ -132,17 +131,16 @@ export class MobileTableCell extends SignalWatcher( if (!renderer) { return; } - const { edit, view } = renderer; - const uni = !this.readonly && this.isEditing && edit != null ? edit : view; - this.view.lockRows(this.isEditing); - this.dataset['editing'] = `${this.isEditing}`; + const { view } = renderer; + this.view.lockRows(this.isEditing$.value); + this.dataset['editing'] = `${this.isEditing$.value}`; const props: CellRenderProps = { cell: this.cell$.value, - isEditing: this.isEditing, + isEditing$: this.isEditing$, selectCurrentCell: this.selectCurrentCell, }; - return renderUniLit(uni, props, { + return renderUniLit(view, props, { ref: this._cell, style: { display: 'contents', @@ -156,8 +154,7 @@ export class MobileTableCell extends SignalWatcher( @property({ attribute: false }) accessor columnIndex!: number; - @state() - accessor isEditing = false; + isEditing$ = signal(false); @property({ attribute: false }) accessor rowIndex!: number; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts index 82c92fb338..86e6c8d9c9 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts @@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { computed, signal } from '@preact/signals-core'; import { css } from 'lit'; -import { property, state } from 'lit/decorators.js'; +import { property } from 'lit/decorators.js'; import { renderUniLit } from '../../../core/index.js'; import type { @@ -88,10 +88,6 @@ export class DatabaseCellContainer extends SignalWatcher( return this.closest('affine-data-view-table-group')?.group?.key; } - private get readonly() { - return this.column.readonly$.value; - } - private get selectionView() { return this.closest('affine-database-table')?.selectionController; } @@ -104,7 +100,7 @@ export class DatabaseCellContainer extends SignalWatcher( override connectedCallback() { super.connectedCallback(); this._disposables.addFromEvent(this, 'click', () => { - if (!this.isEditing) { + if (!this.isEditing$.value) { this.selectCurrentCell(!this.column.readonly$.value); } }); @@ -128,17 +124,16 @@ export class DatabaseCellContainer extends SignalWatcher( if (!renderer) { return; } - const { edit, view } = renderer; - const uni = !this.readonly && this.isEditing && edit != null ? edit : view; - this.view.lockRows(this.isEditing); - this.dataset['editing'] = `${this.isEditing}`; + const { view } = renderer; + this.view.lockRows(this.isEditing$.value); + this.dataset['editing'] = `${this.isEditing$.value}`; const props: CellRenderProps = { cell: this.cell$.value, - isEditing: this.isEditing, + isEditing$: this.isEditing$, selectCurrentCell: this.selectCurrentCell, }; - return renderUniLit(uni, props, { + return renderUniLit(view, props, { ref: this._cell, style: { display: 'contents', @@ -152,8 +147,7 @@ export class DatabaseCellContainer extends SignalWatcher( @property({ attribute: false }) accessor columnIndex!: number; - @state() - accessor isEditing = false; + isEditing$ = signal(false); @property({ attribute: false }) accessor rowIndex!: number; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts index 496de0ba7f..354cb2d244 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts @@ -192,11 +192,9 @@ export class TableSelectionController implements ReactiveController { if (container) { const cell = container.cell; if (old.isEditing) { - requestAnimationFrame(() => { - cell?.onExitEditMode(); - }); + cell?.beforeExitEditingMode(); cell?.blurCell(); - container.isEditing = false; + container.isEditing$.value = false; } } } @@ -211,9 +209,11 @@ export class TableSelectionController implements ReactiveController { if (container) { const cell = container.cell; if (newSelection.isEditing) { - cell?.onEnterEditMode(); - container.isEditing = true; - cell?.focusCell(); + container.isEditing$.value = true; + requestAnimationFrame(() => { + cell?.afterEnterEditingMode(); + cell?.focusCell(); + }); } } } diff --git a/blocksuite/affine/rich-text/src/rich-text.ts b/blocksuite/affine/rich-text/src/rich-text.ts index bcb75fca04..b5465d518c 100644 --- a/blocksuite/affine/rich-text/src/rich-text.ts +++ b/blocksuite/affine/rich-text/src/rich-text.ts @@ -10,7 +10,7 @@ import { type VLine, } from '@blocksuite/inline'; 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 { property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -57,7 +57,7 @@ export class RichText extends WithDisposable(ShadowlessElement) { #verticalScrollContainer: HTMLElement | null = null; - private _inlineEditor: AffineInlineEditor | null = null; + private readonly _inlineEditor$ = signal(null); private readonly _onCopy = (e: ClipboardEvent) => { 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 get inlineEditor() { - return this._inlineEditor; + return this._inlineEditor$.value; } get inlineEditorContainer() { @@ -152,7 +152,7 @@ export class RichText extends WithDisposable(ShadowlessElement) { } private _init() { - if (this._inlineEditor) { + if (this.inlineEditor) { console.error('Inline editor already exists.'); return; } @@ -162,22 +162,25 @@ export class RichText extends WithDisposable(ShadowlessElement) { } // init inline editor - this._inlineEditor = new InlineEditor(this._yText, { - isEmbed: delta => this.embedChecker(delta), - hooks: { - beforeinput: onVBeforeinput, - compositionEnd: onVCompositionEnd, - }, - inlineRangeProvider: this.inlineRangeProvider, - vLineRenderer: this.vLineRenderer, - }); + this._inlineEditor$.value = new InlineEditor( + this._yText, + { + isEmbed: delta => this.embedChecker(delta), + hooks: { + beforeinput: onVBeforeinput, + compositionEnd: onVCompositionEnd, + }, + inlineRangeProvider: this.inlineRangeProvider, + vLineRenderer: this.vLineRenderer, + } + ); + const inlineEditor = this._inlineEditor$.value; if (this.attributesSchema) { - this._inlineEditor.setAttributeSchema(this.attributesSchema); + inlineEditor.setAttributeSchema(this.attributesSchema); } if (this.attributeRenderer) { - this._inlineEditor.setAttributeRenderer(this.attributeRenderer); + inlineEditor.setAttributeRenderer(this.attributeRenderer); } - const inlineEditor = this._inlineEditor; const markdownMatches = this.markdownMatches; if (markdownMatches) { @@ -291,7 +294,7 @@ export class RichText extends WithDisposable(ShadowlessElement) { if (this.inlineEditor?.mounted) { this.inlineEditor.unmount(); } - this._inlineEditor = null; + this._inlineEditor$.value = null; } override connectedCallback() { @@ -384,8 +387,8 @@ export class RichText extends WithDisposable(ShadowlessElement) { this._init(); return; } - if (this._inlineEditor && changedProperties.has('readonly')) { - this._inlineEditor.setReadonly(this.readonly); + if (this.inlineEditor && changedProperties.has('readonly')) { + this.inlineEditor.setReadonly(this.readonly); } } diff --git a/blocksuite/tests-legacy/e2e/database/actions.ts b/blocksuite/tests-legacy/e2e/database/actions.ts index a2eba54359..1e26ac65d3 100644 --- a/blocksuite/tests-legacy/e2e/database/actions.ts +++ b/blocksuite/tests-legacy/e2e/database/actions.ts @@ -1,7 +1,4 @@ -import type { - RichTextCell, - RichTextCellEditing, -} from '@blocksuite/affine/blocks/database'; +import type { RichTextCell } from '@blocksuite/affine/blocks/database'; import { ZERO_WIDTH_SPACE } from '@blocksuite/affine/inline'; 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) { 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) { @@ -85,28 +82,17 @@ export function getDatabaseBodyRow(page: Page, rowIndex = 0) { return rows.nth(rowIndex); } -export function getDatabaseTableContainer(page: Page) { - const container = page.locator('.affine-database-table-container'); - return container; -} - export async function assertDatabaseTitleColumnText( page: Page, title: string, - index = 0 + rowIndex = 0, + columnIndex = 0 ) { - const text = await page.evaluate(index => { - const rowContainer = document.querySelector('.affine-database-block-rows'); - const row = rowContainer?.querySelector( - `.database-row:nth-child(${index + 1})` - ); - 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); + const selectCell1 = getDatabaseCell(page, { + rowIndex: rowIndex, + columnIndex: columnIndex, + }); + const text = await selectCell1.innerText(); if (title === '') { expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_SPACE}])$`)); @@ -115,49 +101,34 @@ export async function assertDatabaseTitleColumnText( } } -export function getDatabaseBodyCell( +export function getDatabaseCell( page: Page, { rowIndex, + columnType, columnIndex, }: { - rowIndex: number; - columnIndex: number; + rowIndex?: number; + columnType?: string; + columnIndex?: number; } ) { const row = getDatabaseBodyRow(page, rowIndex); - const cell = row.locator('.database-cell').nth(columnIndex); - return cell; + const index = columnIndex ?? 0; + const columns = columnType + ? row.getByTestId(columnType) + : row.locator('affine-database-cell-container'); + return columns.nth(index); } -export function getDatabaseBodyCellContent( - page: Page, - { - 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 const getDatabaseColumnCells = (page: Page, columnIndex: number) => { + return page.locator( + `affine-database-cell-container[data-column-index="${columnIndex}"]` + ); +}; 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( @@ -207,14 +178,10 @@ export async function assertDatabaseCellRichTexts( `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 richText = (await cellEditing.count()) === 0 ? cell : cellEditing; - const actualTexts = await richText.evaluate(ele => { - return (ele as RichTextCellEditing).inlineEditor?.yTextString; + const actualTexts = await cell.evaluate(ele => { + return (ele as RichTextCell).inlineEditor$.value?.yTextString; }); expect(actualTexts).toEqual(text); } @@ -263,13 +230,11 @@ export async function assertDatabaseCellLink( const cell = row?.querySelector( `.database-cell:nth-child(${columnIndex + 1})` ); - const richText = - cell?.querySelector('affine-database-link-cell') ?? - cell?.querySelector( - 'affine-database-link-cell-editing' - ); + const richText = cell?.querySelector( + 'affine-database-link-cell' + ); if (!richText) throw new Error('Missing database rich text cell'); - return richText.inlineEditor!.yText.toString(); + return richText.inlineEditor$.value!.yText.toString(); }, { rowIndex, columnIndex } ); @@ -381,7 +346,7 @@ export async function assertCellsSelection( const focusBox = await getBoundingBox(focus); const [rowIndex, columnIndex] = start; - const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex }); + const cell = getDatabaseCell(page, { rowIndex, columnIndex: columnIndex }); const cellBox = await getBoundingBox(cell); expect(focusBox).toEqual({ x: cellBox.x, @@ -407,7 +372,7 @@ export async function assertCellsSelection( let x = 0; let y = 0; for (let i = rowIndexStart; i <= rowIndexEnd; i++) { - const cell = getDatabaseBodyCell(page, { + const cell = getDatabaseCell(page, { rowIndex: i, columnIndex: columnIndexStart, }); @@ -419,7 +384,7 @@ export async function assertCellsSelection( } for (let j = columnIndexStart; j <= columnIndexEnd; j++) { - const cell = getDatabaseBodyCell(page, { + const cell = getDatabaseCell(page, { rowIndex: rowIndexStart, columnIndex: j, }); @@ -468,7 +433,7 @@ export async function focusKanbanCardHeader(page: Page, index = 0) { export async function clickKanbanCardHeader(page: Page, index = 0) { const cardHeader = page.locator('data-view-header-area-text').nth(index); await cardHeader.click(); - await cardHeader.click(); + await pressEnter(page); } export async function assertKanbanCardHeaderText( @@ -563,6 +528,7 @@ export function getKanbanCard( const card = group.locator('affine-data-view-kanban-card').nth(cardIndex); return card; } + export const moveToCenterOf = async (page: Page, locator: Locator) => { const box = (await locator.boundingBox())!; expect(box).toBeDefined(); diff --git a/blocksuite/tests-legacy/e2e/database/clipboard.spec.ts b/blocksuite/tests-legacy/e2e/database/clipboard.spec.ts index d8dd549060..4f0032aa9d 100644 --- a/blocksuite/tests-legacy/e2e/database/clipboard.spec.ts +++ b/blocksuite/tests-legacy/e2e/database/clipboard.spec.ts @@ -24,7 +24,7 @@ import { assertRichTexts } from '../utils/asserts.js'; import { test } from '../utils/playwright.js'; import { assertDatabaseTitleColumnText, - getDatabaseBodyCell, + getDatabaseCell, getElementStyle, initDatabaseColumn, switchColumnType, @@ -92,12 +92,12 @@ test.describe('copy&paste when selecting', () => { await initDatabaseRowWithData(page, ''); await initDatabaseRowWithData(page, ''); - const startCell = getDatabaseBodyCell(page, { + const startCell = getDatabaseCell(page, { rowIndex: 0, columnIndex: 0, }); 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 startX = startCellBox.x + startCellBox.width / 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, 'text2', 3); - const selectCell21 = getDatabaseBodyCell(page, { + const selectCell21 = getDatabaseCell(page, { rowIndex: 2, columnIndex: 1, }); - const selectCell31 = getDatabaseBodyCell(page, { + const selectCell31 = getDatabaseCell(page, { rowIndex: 3, columnIndex: 1, }); @@ -145,7 +145,7 @@ test.describe('copy&paste when selecting', () => { await waitNextFrame(page, 100); await initDatabaseColumn(page); await switchColumnType(page, 'Number', 2); - const numberCell = getDatabaseBodyCell(page, { + const numberCell = getDatabaseCell(page, { rowIndex: 0, columnIndex: 2, }); @@ -162,12 +162,12 @@ test.describe('copy&paste when selecting', () => { await waitNextFrame(page); await assertDatabaseTitleColumnText(page, 'text1', 1); - const selectCell = getDatabaseBodyCell(page, { + const selectCell = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1, }); expect(await selectCell.innerText()).toBe('abc'); - const selectNumberCell = getDatabaseBodyCell(page, { + const selectNumberCell = getDatabaseCell(page, { rowIndex: 1, columnIndex: 2, }); diff --git a/blocksuite/tests-legacy/e2e/database/column.spec.ts b/blocksuite/tests-legacy/e2e/database/column.spec.ts index 199a5d7f31..6ab6b333b8 100644 --- a/blocksuite/tests-legacy/e2e/database/column.spec.ts +++ b/blocksuite/tests-legacy/e2e/database/column.spec.ts @@ -27,8 +27,8 @@ import { changeColumnType, clickDatabaseOutside, clickSelectOption, + getDatabaseCell, getDatabaseHeaderColumn, - getFirstColumnCell, initDatabaseColumn, performColumnAction, performSelectColumnTagAction, @@ -67,7 +67,10 @@ test.describe('column operations', () => { const { text: title1 } = await getDatabaseHeaderColumn(page, 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'); await initDatabaseColumn(page, 'abc'); @@ -198,7 +201,10 @@ test.describe('switch column type', () => { await pressEscape(page); await changeColumnType(page, 1, 'Number'); - const cell = getFirstColumnCell(page, 'number'); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'number', + }); await assertDatabaseCellNumber(page, { text: '', }); @@ -217,10 +223,11 @@ test.describe('switch column type', () => { await pressEscape(page); await switchColumnType(page, 'Text'); - // For now, rich-text will only be initialized on click - // Therefore, for the time being, here is to detect whether there is '.affine-database-rich-text' - const cell = getFirstColumnCell(page, 'affine-database-rich-text'); - expect(await cell.count()).toBe(1); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'rich-text', + }); + await cell.isVisible(); await pressEnter(page); await type(page, '123'); await pressEscape(page); @@ -239,35 +246,39 @@ test.describe('switch column type', () => { await type(page, 'abc'); await pressEnter(page); await pressEscape(page); - const cell = getFirstColumnCell(page, 'select-selected'); - expect(await cell.count()).toBe(2); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnIndex: 1, + }); + const tags = cell.getByTestId('tag-selected'); + expect(await tags.count()).toBe(2); await switchColumnType(page, 'Select', 1); - expect(await cell.count()).toBe(1); - expect(await cell.innerText()).toBe('123'); + expect(await tags.count()).toBe(1); + expect(await tags.innerText()).toBe('123'); await pressEnter(page); await type(page, 'def'); await pressEnter(page); - expect(await cell.innerText()).toBe('def'); + expect(await tags.innerText()).toBe('def'); await switchColumnType(page, 'Multi-select'); await pressEnter(page); await type(page, '666'); await pressEnter(page); await pressEscape(page); - expect(await cell.count()).toBe(2); - expect(await cell.nth(0).innerText()).toBe('def'); - expect(await cell.nth(1).innerText()).toBe('666'); + expect(await tags.count()).toBe(2); + expect(await tags.nth(0).innerText()).toBe('def'); + expect(await tags.nth(1).innerText()).toBe('666'); await switchColumnType(page, 'Select'); - expect(await cell.count()).toBe(1); - expect(await cell.innerText()).toBe('def'); + expect(await tags.count()).toBe(1); + expect(await tags.innerText()).toBe('def'); await pressEnter(page); await type(page, '888'); await pressEnter(page); - expect(await cell.innerText()).toBe('888'); + expect(await tags.innerText()).toBe('888'); }); test('switch between number and rich-text', async ({ page }) => { @@ -302,12 +313,18 @@ test.describe('switch column type', () => { await switchColumnType(page, 'Number'); 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'); await switchColumnType(page, 'Select'); await initDatabaseDynamicRowWithData(page, 'abc'); - const selectCell = getFirstColumnCell(page, 'select-selected'); + const selectCell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'select', + }); expect(await selectCell.innerText()).toBe('abc'); await switchColumnType(page, 'Number'); @@ -325,7 +342,10 @@ test.describe('switch column type', () => { await pressEscape(page); 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 waitNextFrame(page, 500); @@ -345,7 +365,10 @@ test.describe('switch column type', () => { await pressEscape(page); await changeColumnType(page, 1, 'Checkbox'); - let checkbox = getFirstColumnCell(page, 'checkbox'); + let checkbox = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'checkbox', + }); await expect(checkbox).not.toHaveClass('checked'); // checked @@ -357,7 +380,10 @@ test.describe('switch column type', () => { await clickDatabaseOutside(page); await waitNextFrame(page, 100); await changeColumnType(page, 1, 'Checkbox'); - checkbox = getFirstColumnCell(page, 'checkbox'); + checkbox = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'checkbox', + }).locator('.checkbox'); await expect(checkbox).toHaveClass(/checked/); // not checked @@ -369,7 +395,10 @@ test.describe('switch column type', () => { await clickDatabaseOutside(page); await waitNextFrame(page, 100); await changeColumnType(page, 1, 'Checkbox'); - checkbox = getFirstColumnCell(page, 'checkbox'); + checkbox = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'checkbox', + }); await expect(checkbox).not.toHaveClass('checked'); }); @@ -382,10 +411,14 @@ test.describe('switch column type', () => { await pressEscape(page); await switchColumnType(page, 'Progress'); - const progress = getFirstColumnCell(page, 'progress'); - expect(await progress.textContent()).toBe('0'); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'progress', + }); + const progress = cell.getByTestId('progress'); + expect((await progress.textContent())?.trim()).toBe('0'); await waitNextFrame(page, 500); - const progressBg = page.locator('.affine-database-progress-bg'); + const progressBg = cell.getByTestId('progress-background'); const { x: progressBgX, y: progressBgY, @@ -393,7 +426,7 @@ test.describe('switch column type', () => { } = await getBoundingBox(progressBg); await page.mouse.move(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 { x: dragX, y: dragY, @@ -427,19 +460,25 @@ test.describe('switch column type', () => { await switchColumnType(page, 'Link'); const linkText = 'http://example.com'; - const cell = getFirstColumnCell(page, 'affine-database-link'); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'link', + }); await pressEnter(page); await type(page, linkText); await pressEscape(page); - const link = cell.locator('affine-database-link-node > a'); - const linkContent = link.locator('.link-node-text'); + const link = cell.getByTestId('property-link-a'); await expect(link).toHaveAttribute('href', linkText); - expect(await linkContent.textContent()).toBe(linkText); + expect(await link.textContent()).toBe(linkText); // not link text 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 waitNextFrame(page); await selectAllByKeyboard(page); await type(page, 'abc'); await pressEnter(page); @@ -461,7 +500,7 @@ test.describe('select column tag action', () => { await pressArrowRight(page); await type(page, '4567abc00'); 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(1).innerText()).toBe('123'); }); @@ -479,7 +518,7 @@ test.describe('select column tag action', () => { // esc 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); expect(await option1.innerText()).toBe('123456'); }); diff --git a/blocksuite/tests-legacy/e2e/database/database.spec.ts b/blocksuite/tests-legacy/e2e/database/database.spec.ts index 3404738cae..e132fa1c0d 100644 --- a/blocksuite/tests-legacy/e2e/database/database.spec.ts +++ b/blocksuite/tests-legacy/e2e/database/database.spec.ts @@ -40,9 +40,8 @@ import { clickDatabaseOutside, focusDatabaseHeader, focusDatabaseSearch, - getDatabaseBodyCell, + getDatabaseCell, getDatabaseHeaderColumn, - getFirstColumnCell, initDatabaseColumn, switchColumnType, } from './actions.js'; @@ -66,9 +65,9 @@ test('edit database block title and create new rows', async ({ page }) => { await assertBlockProps(page, '2', { title: 'hello', }); - await undoByClick(page); + await undoByKeyboard(page); await assertBlockProps(page, '2', { - title: 'Database 1', + title: '', }); 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 clickDatabaseOutside(page); - const cell = getFirstColumnCell(page, 'number'); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'number', + }); const text = await cell.textContent(); expect(text?.trim()).toBe('1'); }); @@ -116,13 +118,17 @@ test('should rich-text column support soft enter', async ({ page }) => { await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, '123', true); - const cell = getFirstColumnCell(page, 'affine-database-rich-text'); - await cell.click(); + // const cell = getDatabaseCell(page, { + // rowIndex: 0, + // columnType: 'rich-text', + // }); + // await cell.click(); + await pressEnter(page); await pressArrowLeft(page); await pressEnter(page); await assertDatabaseCellRichTexts(page, { text: '123' }); - await cell.click(); + await pressEnter(page); await pressArrowRight(page); await pressArrowLeft(page); await pressShiftEnter(page); @@ -139,10 +145,13 @@ test('should the multi-select mode work correctly', async ({ page }) => { await pressEscape(page); await initDatabaseDynamicRowWithData(page, '2'); await pressEscape(page); - const cell = getFirstColumnCell(page, 'select-selected'); - expect(await cell.count()).toBe(2); - expect(await cell.nth(0).innerText()).toBe('1'); - expect(await cell.nth(1).innerText()).toBe('2'); + const tags = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'multi-select', + }).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 }) => { @@ -375,7 +384,10 @@ test('should title column support quick changing of column type', async ({ await typeIcon.click(); await waitNextFrame(page); await clickColumnType(page, 'Select'); - const cell = getFirstColumnCell(page, 'select-selected'); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'select', + }); expect(await cell.count()).toBe(1); }); @@ -513,12 +525,19 @@ test.describe('readonly mode', () => { await switchColumnType(page, 'Text'); 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 pressEnter(page); await type(page, '123'); + await pressEnter(page); await assertDatabaseCellRichTexts(page, { text: '123' }); await switchReadonly(page); + await waitNextFrame(page); + await pressEnter(page); await pressBackspace(page); await type(page, '789'); await assertDatabaseCellRichTexts(page, { text: '123' }); @@ -610,7 +629,10 @@ test.describe('readonly mode', () => { const database = page.locator('affine-database'); await expect(database).toBeVisible(); - const cell = getFirstColumnCell(page, 'affine-database-rich-text'); + const cell = getDatabaseCell(page, { + rowIndex: 0, + columnType: 'rich-text', + }); await cell.click(); const focusBorder = database.locator( @@ -635,11 +657,11 @@ test.describe('readonly mode', () => { const database = page.locator('affine-database'); await expect(database).toBeVisible(); - const startCell = getDatabaseBodyCell(page, { + const startCell = getDatabaseCell(page, { rowIndex: 0, columnIndex: 0, }); - const endCell = getDatabaseBodyCell(page, { + const endCell = getDatabaseCell(page, { rowIndex: 0, columnIndex: 1, }); diff --git a/blocksuite/tests-legacy/e2e/database/selection.spec.ts b/blocksuite/tests-legacy/e2e/database/selection.spec.ts index e0911a1b7c..57d8cae9c2 100644 --- a/blocksuite/tests-legacy/e2e/database/selection.spec.ts +++ b/blocksuite/tests-legacy/e2e/database/selection.spec.ts @@ -33,7 +33,7 @@ import { assertRowsSelection, clickKanbanCardHeader, focusKanbanCardHeader, - getDatabaseBodyCell, + getDatabaseCell, getKanbanCard, initDatabaseColumn, switchColumnType, @@ -62,7 +62,7 @@ test.describe('focus', () => { await switchColumnType(page, 'Number'); await initDatabaseDynamicRowWithData(page, '123', true); - const selectColumn = getDatabaseBodyCell(page, { + const selectColumn = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1, }); @@ -105,7 +105,6 @@ test.describe('row-level selection', () => { await initDatabaseColumn(page); await initDatabaseRowWithData(page, 'title'); - await pressEscape(page); await waitNextFrame(page, 100); await assertCellsSelection(page, { start: [0, 0], @@ -123,7 +122,6 @@ test.describe('row-level selection', () => { await initDatabaseColumn(page); await initDatabaseDynamicRowWithData(page, '123', true); - await pressEscape(page); await waitNextFrame(page, 100); await pressEscape(page); await assertRowsSelection(page, [0, 0]); @@ -134,12 +132,11 @@ test.describe('row-level selection', () => { await initEmptyDatabaseState(page); await initDatabaseColumn(page); - await initDatabaseDynamicRowWithData(page, '', true); - await pressEscape(page); await switchColumnType(page, 'Number'); + await initDatabaseDynamicRowWithData(page, 'asd', true); await initDatabaseDynamicRowWithData(page, '123', true); - const selectColumn = getDatabaseBodyCell(page, { + const selectColumn = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1, }); @@ -250,11 +247,11 @@ test.describe('cell-level selection', () => { await switchColumnType(page, 'Number'); await initDatabaseDynamicRowWithData(page, '123', true); - const startCell = getDatabaseBodyCell(page, { + const startCell = getDatabaseCell(page, { rowIndex: 0, columnIndex: 0, }); - const endCell = getDatabaseBodyCell(page, { + const endCell = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1, }); @@ -293,11 +290,11 @@ test.describe('cell-level selection', () => { await initDatabaseDynamicRowWithData(page, '123', false); await pressEscape(page); - const startCell = getDatabaseBodyCell(page, { + const startCell = getDatabaseCell(page, { rowIndex: 0, columnIndex: 0, }); - const endCell = getDatabaseBodyCell(page, { + const endCell = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1, }); @@ -319,12 +316,12 @@ test.describe('cell-level selection', () => { await pressBackspace(page); await assertDatabaseTitleColumnText(page, '', 0); await assertDatabaseTitleColumnText(page, '', 1); - const selectCell1 = getDatabaseBodyCell(page, { + const selectCell1 = getDatabaseCell(page, { rowIndex: 0, columnIndex: 1, }); expect(await selectCell1.innerText()).toBe(''); - const selectCell2 = getDatabaseBodyCell(page, { + const selectCell2 = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1, }); diff --git a/blocksuite/tests-legacy/e2e/utils/actions/misc.ts b/blocksuite/tests-legacy/e2e/utils/actions/misc.ts index 31ee0db9ab..ab926d31b2 100644 --- a/blocksuite/tests-legacy/e2e/utils/actions/misc.ts +++ b/blocksuite/tests-legacy/e2e/utils/actions/misc.ts @@ -450,6 +450,7 @@ export async function initDatabaseRowWithData(page: Page, data: string) { await initDatabaseRow(page); await waitNextFrame(page, 50); await type(page, data); + await pressEscape(page); } export const getAddRow = (page: Page): Locator => { return page.locator('.data-view-table-group-add-row'); @@ -473,6 +474,7 @@ export async function initDatabaseDynamicRowWithData( await pressEnter(page); await waitNextFrame(page); await type(page, data); + await waitNextFrame(page); await pressEnter(page); } diff --git a/tests/affine-local/e2e/page-properties.spec.ts b/tests/affine-local/e2e/page-properties.spec.ts index 08ffeec1f1..65d6052e8d 100644 --- a/tests/affine-local/e2e/page-properties.spec.ts +++ b/tests/affine-local/e2e/page-properties.spec.ts @@ -1,3 +1,4 @@ +import { waitNextFrame } from '@affine-test/kit/bs/misc'; import { test } from '@affine-test/kit/playwright'; import { openHomePage, @@ -319,10 +320,11 @@ test('can show database backlink info', async ({ page }) => { await page.keyboard.press('Escape'); await page.keyboard.press('ArrowRight'); await page.keyboard.press('Enter'); + await waitNextFrame(page); + await waitNextFrame(page); await page.keyboard.type('Done'); - await page - .locator('affine-multi-tag-select .select-option:has-text("Done")') - .click(); + await waitNextFrame(page); + await page.keyboard.press('Enter'); // go back to title cell await page.keyboard.press('ArrowLeft'); diff --git a/yarn.lock b/yarn.lock index 2e248af9e8..d92343fa0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2358,6 +2358,7 @@ __metadata: "@preact/signals-core": "npm:^1.8.0" "@toeverything/theme": "npm:^1.1.12" "@types/mdast": "npm:^4.0.4" + "@vanilla-extract/css": "npm:^1.17.0" date-fns: "npm:^4.0.0" lit: "npm:^3.2.0" minimatch: "npm:^10.0.1" @@ -3203,6 +3204,7 @@ __metadata: "@preact/signals-core": "npm:^1.8.0" "@toeverything/theme": "npm:^1.1.12" "@types/lodash-es": "npm:^4.17.12" + "@vanilla-extract/css": "npm:^1.17.0" date-fns: "npm:^4.0.0" lit: "npm:^3.2.0" lodash-es: "npm:^4.17.21"