refactor(editor): add a layer of ui-logic to enhance type safety (#12511)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced modular UI logic layers for Kanban and Table views, enhancing maintainability and scalability.
  - Added new CSS-in-JS style modules for database blocks and table views, improving visual consistency.
  - Expanded telemetry event tracking for database views, properties, filters, and groups.
  - Added utility functions for lazy initialization and cached computed values.

- **Refactor**
  - Unified logic and state management across Kanban and Table views by replacing direct component dependencies with logic-centric architecture.
  - Updated components and widgets to use the new logic-based approach for state, selection, and event handling.
  - Replaced inline styles with CSS classes; updated class names to align with new component structure.
  - Centralized state access through UI logic instances, eliminating direct DOM queries and simplifying dependencies.
  - Consolidated Kanban and Table view presets effects for streamlined initialization.
  - Replaced Lit reactive state with Preact signals in multiple components for improved reactivity.
  - Split monolithic components into separate logic and UI classes for clearer separation of concerns.
  - Removed obsolete components and consolidated exports for cleaner API surface.

- **Bug Fixes**
  - Enhanced selection and interaction reliability in database cells and views.
  - Fixed scrolling issues on mobile table views for improved compatibility.

- **Chores**
  - Updated end-to-end test selectors to reflect new component names and structure.
  - Removed deprecated utilities and cleaned up unused imports.

- **Documentation**
  - Improved type definitions and public API exports for better developer experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
zzj3720
2025-05-27 09:36:44 +00:00
parent 9bf86e3f61
commit 0f1a3c212d
109 changed files with 2625 additions and 2436 deletions
@@ -23,9 +23,9 @@ import {
createRecordDetail, createRecordDetail,
createUniComponentFromWebComponent, createUniComponentFromWebComponent,
type DataSource, type DataSource,
DataView,
dataViewCommonStyle, dataViewCommonStyle,
type DataViewProps, type DataViewProps,
DataViewRootUILogic,
type DataViewSelection, type DataViewSelection,
type DataViewWidget, type DataViewWidget,
type DataViewWidgetProps, type DataViewWidgetProps,
@@ -133,8 +133,6 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
private _dataSource?: DataSource; private _dataSource?: DataSource;
private readonly dataView = new DataView();
_bindHotkey: DataViewProps['bindHotkey'] = hotkeys => { _bindHotkey: DataViewProps['bindHotkey'] = hotkeys => {
return { return {
dispose: this.host.event.bindHotkey(hotkeys, { dispose: this.host.event.bindHotkey(hotkeys, {
@@ -232,10 +230,6 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
return this.rootComponent; return this.rootComponent;
} }
get view() {
return this.dataView.expose;
}
private renderDatabaseOps() { private renderDatabaseOps() {
if (this.store.readonly) { if (this.store.readonly) {
return nothing; return nothing;
@@ -250,68 +244,68 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true'); this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
} }
private readonly dataViewRootLogic = new DataViewRootUILogic({
virtualPadding$: signal(0),
bindHotkey: this._bindHotkey,
handleEvent: this._handleEvent,
selection$: this.selection$,
setSelection: this.setSelection,
dataSource: this.dataSource,
headerWidget: this.headerWidget,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
},
eventTrace: (key, params) => {
const telemetryService = this.std.getOptional(TelemetryProvider);
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
blockId: this.blockId,
});
},
detailPanelConfig: {
openDetailPanel: (target, data) => {
const peekViewService = this.std.getOptional(PeekViewProvider);
if (peekViewService) {
const template = createRecordDetail({
...data,
openDoc: () => {},
detail: {
header: uniMap(
createUniComponentFromWebComponent(BlockRenderer),
props => ({
...props,
host: this.host,
})
),
note: uniMap(
createUniComponentFromWebComponent(NoteRenderer),
props => ({
...props,
model: this.model,
host: this.host,
})
),
},
});
return peekViewService.peek({ target, template });
} else {
return Promise.resolve();
}
},
},
});
override renderBlock() { override renderBlock() {
const peekViewService = this.std.getOptional(PeekViewProvider);
const telemetryService = this.std.getOptional(TelemetryProvider);
return html` return html`
<div contenteditable="false" style="position: relative"> <div contenteditable="false" style="position: relative">
${this.dataView.render({ ${this.dataViewRootLogic.render()}
virtualPadding$: signal(0),
bindHotkey: this._bindHotkey,
handleEvent: this._handleEvent,
selection$: this.selection$,
setSelection: this.setSelection,
dataSource: this.dataSource,
headerWidget: this.headerWidget,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
},
eventTrace: (key, params) => {
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
blockId: this.blockId,
});
},
detailPanelConfig: {
openDetailPanel: (target, data) => {
if (peekViewService) {
const template = createRecordDetail({
...data,
openDoc: () => {},
detail: {
header: uniMap(
createUniComponentFromWebComponent(BlockRenderer),
props => ({
...props,
host: this.host,
})
),
note: uniMap(
createUniComponentFromWebComponent(NoteRenderer),
props => ({
...props,
model: this.model,
host: this.host,
})
),
},
});
return peekViewService.peek({ target, template });
} else {
return Promise.resolve();
}
},
},
})}
</div> </div>
`; `;
} }
@@ -1,15 +1,19 @@
import { stopPropagation } from '@blocksuite/affine-shared/utils'; import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit'; import type { DataViewUILogicBase } from '@blocksuite/data-view';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import type { Text } from '@blocksuite/store'; import type { Text } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import { css, html } from 'lit'; import { css, html } from 'lit';
import { property, query, state } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import type { DatabaseBlockComponent } from '../../database-block.js'; import type { DatabaseBlockComponent } from '../../database-block.js';
export class DatabaseTitle extends WithDisposable(ShadowlessElement) { export class DatabaseTitle extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css` static override styles = css`
.affine-database-title { .affine-database-title {
position: relative; position: relative;
@@ -71,22 +75,23 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
`; `;
private readonly compositionEnd = () => { private readonly compositionEnd = () => {
this.isComposing$.value = false;
this.titleText.replace(0, this.titleText.length, this.input.value); this.titleText.replace(0, this.titleText.length, this.input.value);
}; };
private readonly onBlur = () => { private readonly onBlur = () => {
this.isFocus = false; this.isFocus$.value = false;
}; };
private readonly onFocus = () => { private readonly onFocus = () => {
this.isFocus = true; this.isFocus$.value = true;
if (this.database?.viewSelection$?.value) { if (this.dataViewLogic.selection$.value) {
this.database?.setSelection(undefined); this.dataViewLogic.setSelection(undefined);
} }
}; };
private readonly onInput = (e: InputEvent) => { private readonly onInput = (e: InputEvent) => {
this.text = this.input.value; this.text$.value = this.input.value;
if (!e.isComposing) { if (!e.isComposing) {
this.titleText.replace(0, this.titleText.length, this.input.value); this.titleText.replace(0, this.titleText.length, this.input.value);
} }
@@ -102,9 +107,9 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
}; };
updateText = () => { updateText = () => {
if (!this.isFocus) { if (!this.isFocus$.value) {
this.input.value = this.titleText.toString(); this.input.value = this.titleText.toString();
this.text = this.input.value; this.text$.value = this.input.value;
} }
}; };
@@ -124,25 +129,25 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
} }
override render() { override render() {
const isEmpty = !this.text; const isEmpty = !this.text$.value;
const classList = classMap({ const classList = classMap({
'affine-database-title': true, 'affine-database-title': true,
ellipsis: !this.isFocus, ellipsis: !this.isFocus$.value,
}); });
const untitledStyle = styleMap({ const untitledStyle = styleMap({
height: isEmpty ? 'auto' : 0, height: isEmpty ? 'auto' : 0,
opacity: isEmpty && !this.isFocus ? 1 : 0, opacity: isEmpty && !this.isFocus$.value ? 1 : 0,
}); });
return html` <div return html` <div
class="${classList}" class="${classList}"
data-title-empty="${isEmpty}" data-title-empty="${isEmpty}"
data-title-focus="${this.isFocus}" data-title-focus="${this.isFocus$.value}"
> >
<div class="text" style="${untitledStyle}">Untitled</div> <div class="text" style="${untitledStyle}">Untitled</div>
<div class="text">${this.text}</div> <div class="text">${this.text$.value}</div>
<textarea <textarea
.disabled="${this.readonly}" .disabled="${this.readonly$.value}"
@input="${this.onInput}" @input="${this.onInput}"
@keydown="${this.onKeyDown}" @keydown="${this.onKeyDown}"
@copy="${stopPropagation}" @copy="${stopPropagation}"
@@ -159,23 +164,24 @@ export class DatabaseTitle extends WithDisposable(ShadowlessElement) {
@query('textarea') @query('textarea')
private accessor input!: HTMLTextAreaElement; private accessor input!: HTMLTextAreaElement;
@state() private readonly isComposing$ = signal(false);
accessor isComposing = false; private readonly isFocus$ = signal(false);
@state() private onPressEnterKey() {
private accessor isFocus = false; this.dataViewLogic.addRow?.('start');
}
@property({ attribute: false }) get readonly$() {
accessor onPressEnterKey: (() => void) | undefined = undefined; return this.dataViewLogic.view.readonly$;
}
@property({ attribute: false }) private readonly text$ = signal('');
accessor readonly!: boolean;
@state()
private accessor text = '';
@property({ attribute: false }) @property({ attribute: false })
accessor titleText!: Text; accessor titleText!: Text;
@property({ attribute: false })
accessor dataViewLogic!: DataViewUILogicBase;
} }
declare global { declare global {
@@ -0,0 +1,73 @@
import { css } from '@emotion/css';
import { cssVarV2 } from '@toeverything/theme/v2';
export const databaseBlockStyles = css({
display: 'block',
borderRadius: '8px',
backgroundColor: 'var(--affine-background-primary-color)',
padding: '8px',
margin: '8px -8px -8px',
});
export const databaseBlockSelectedStyles = css({
backgroundColor: 'var(--affine-hover-color)',
borderRadius: '4px',
});
export const databaseOpsStyles = css({
padding: '2px',
borderRadius: '4px',
display: 'flex',
cursor: 'pointer',
alignItems: 'center',
height: 'max-content',
fontSize: '16px',
color: cssVarV2.icon.primary,
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
'@media print': {
display: 'none',
},
});
export const databaseHeaderBarStyles = css({
'@media print': {
display: 'none !important',
},
});
export const databaseTitleStyles = css({
overflow: 'hidden',
});
export const databaseHeaderContainerStyles = css({
marginBottom: '16px',
display: 'flex',
flexDirection: 'column',
});
export const databaseTitleRowStyles = css({
display: 'flex',
gap: '12px',
marginBottom: '8px',
alignItems: 'center',
});
export const databaseToolbarRowStyles = css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '12px',
});
export const databaseViewBarContainerStyles = css({
flex: 1,
});
export const databaseContentStyles = css({
position: 'relative',
backgroundColor: 'var(--affine-background-primary-color)',
borderRadius: '4px',
});
@@ -19,15 +19,14 @@ import { getDropResult } from '@blocksuite/affine-widget-drag-handle';
import { import {
createRecordDetail, createRecordDetail,
createUniComponentFromWebComponent, createUniComponentFromWebComponent,
DataView, DataViewRootUILogic,
dataViewCommonStyle,
type DataViewInstance,
type DataViewProps,
type DataViewSelection, type DataViewSelection,
type DataViewUILogicBase,
type DataViewWidget, type DataViewWidget,
type DataViewWidgetProps, type DataViewWidgetProps,
defineUniComponent, defineUniComponent,
ExternalGroupByConfigProvider, ExternalGroupByConfigProvider,
lazy,
renderUniLit, renderUniLit,
type SingleView, type SingleView,
uniMap, uniMap,
@@ -44,12 +43,23 @@ import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import { Slice } from '@blocksuite/store'; import { Slice } from '@blocksuite/store';
import { autoUpdate } from '@floating-ui/dom'; import { autoUpdate } from '@floating-ui/dom';
import { computed, signal } from '@preact/signals-core'; import { computed, signal } from '@preact/signals-core';
import { css, html, nothing, unsafeCSS } from 'lit'; import { html, nothing } from 'lit';
import { popSideDetail } from './components/layout.js'; import { popSideDetail } from './components/layout.js';
import { DatabaseConfigExtension } from './config.js'; import { DatabaseConfigExtension } from './config.js';
import { EditorHostKey } from './context/host-context.js'; import { EditorHostKey } from './context/host-context.js';
import { DatabaseBlockDataSource } from './data-source.js'; import { DatabaseBlockDataSource } from './data-source.js';
import {
databaseBlockStyles,
databaseContentStyles,
databaseHeaderBarStyles,
databaseHeaderContainerStyles,
databaseOpsStyles,
databaseTitleRowStyles,
databaseTitleStyles,
databaseToolbarRowStyles,
databaseViewBarContainerStyles,
} from './database-block-styles.js';
import { BlockRenderer } from './detail-panel/block-renderer.js'; import { BlockRenderer } from './detail-panel/block-renderer.js';
import { NoteRenderer } from './detail-panel/note-renderer.js'; import { NoteRenderer } from './detail-panel/note-renderer.js';
import { DatabaseSelection } from './selection.js'; import { DatabaseSelection } from './selection.js';
@@ -58,52 +68,7 @@ import { getSingleDocIdFromText } from './utils/title-doc.js';
import type { DatabaseViewExtensionOptions } from './view'; import type { DatabaseViewExtensionOptions } from './view';
export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBlockModel> { export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBlockModel> {
static override styles = css` private readonly clickDatabaseOps = (e: MouseEvent) => {
${unsafeCSS(dataViewCommonStyle('affine-database'))}
affine-database {
display: block;
border-radius: 8px;
background-color: var(--affine-background-primary-color);
padding: 8px;
margin: 8px -8px -8px;
}
.database-block-selected {
background-color: var(--affine-hover-color);
border-radius: 4px;
}
.database-ops {
padding: 2px;
border-radius: 4px;
display: flex;
cursor: pointer;
align-items: center;
height: max-content;
}
.database-ops svg {
width: 16px;
height: 16px;
color: var(--affine-icon-color);
}
.database-ops:hover {
background-color: var(--affine-hover-color);
}
@media print {
.database-ops {
display: none;
}
.database-header-bar {
display: none !important;
}
}
`;
private readonly _clickDatabaseOps = (e: MouseEvent) => {
const options = this.optionsConfig.configure(this.model, { const options = this.optionsConfig.configure(this.model, {
items: [ items: [
menu.input({ menu.input({
@@ -155,36 +120,33 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
}); });
}; };
private _dataSource?: DatabaseBlockDataSource; private readonly dataSource = lazy(() => {
const dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
dataSource.serviceSet(EditorHostKey, this.host);
this.std.provider
.getAll(ExternalGroupByConfigProvider)
.forEach(config => {
dataSource.serviceSet(
ExternalGroupByConfigProvider(config.name),
config
);
});
});
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && dataSource.viewManager.viewGet(id)) {
dataSource.viewManager.setCurrentView(id);
}
return dataSource;
});
private readonly dataView = new DataView(); private readonly renderTitle = (dataViewLogic: DataViewUILogicBase) => {
private readonly renderTitle = (dataViewMethod: DataViewInstance) => {
const addRow = () => dataViewMethod.addRow?.('start');
return html` <affine-database-title return html` <affine-database-title
style="overflow: hidden" class="${databaseTitleStyles}"
.titleText="${this.model.props.title}" .titleText="${this.model.props.title}"
.readonly="${this.dataSource.readonly$.value}" .dataViewLogic="${dataViewLogic}"
.onPressEnterKey="${addRow}"
></affine-database-title>`; ></affine-database-title>`;
}; };
_bindHotkey: DataViewProps['bindHotkey'] = hotkeys => {
return {
dispose: this.host.event.bindHotkey(hotkeys, {
blockId: this.topContenteditableElement?.blockId ?? this.blockId,
}),
};
};
_handleEvent: DataViewProps['handleEvent'] = (name, handler) => {
return {
dispose: this.host.event.add(name, handler, {
blockId: this.blockId,
}),
};
};
createTemplate = ( createTemplate = (
data: { data: {
view: SingleView; view: SingleView;
@@ -218,18 +180,12 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
headerWidget: DataViewWidget = defineUniComponent( headerWidget: DataViewWidget = defineUniComponent(
(props: DataViewWidgetProps) => { (props: DataViewWidgetProps) => {
return html` return html`
<div style="margin-bottom: 16px;display:flex;flex-direction: column"> <div class="${databaseHeaderContainerStyles}">
<div <div class="${databaseTitleRowStyles}">
style="display:flex;gap:12px;margin-bottom: 8px;align-items: center" ${this.renderTitle(props.dataViewLogic)} ${this.renderDatabaseOps()}
>
${this.renderTitle(props.dataViewInstance)}
${this.renderDatabaseOps()}
</div> </div>
<div <div class="${databaseToolbarRowStyles} ${databaseHeaderBarStyles}">
style="display:flex;align-items:center;justify-content: space-between;gap: 12px" <div class="${databaseViewBarContainerStyles}">
class="database-header-bar"
>
<div style="flex:1">
${renderUniLit(widgetPresets.viewBar, { ${renderUniLit(widgetPresets.viewBar, {
...props, ...props,
onChangeView: id => { onChangeView: id => {
@@ -284,7 +240,9 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return () => {}; return () => {};
}; };
setSelection = (selection: DataViewSelection | undefined) => { private readonly setSelection = (
selection: DataViewSelection | undefined
) => {
if (selection) { if (selection) {
getSelection()?.removeAllRanges(); getSelection()?.removeAllRanges();
} }
@@ -301,7 +259,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
); );
}; };
toolsWidget: DataViewWidget = widgetPresets.createTools({ private readonly toolsWidget: DataViewWidget = widgetPresets.createTools({
table: [ table: [
widgetPresets.tools.filter, widgetPresets.tools.filter,
widgetPresets.tools.sort, widgetPresets.tools.sort,
@@ -318,7 +276,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
], ],
}); });
viewSelection$ = computed(() => { private readonly viewSelection$ = computed(() => {
const databaseSelection = this.selection.value.find( const databaseSelection = this.selection.value.find(
(selection): selection is DatabaseSelection => { (selection): selection is DatabaseSelection => {
if (selection.blockId !== this.blockId) { if (selection.blockId !== this.blockId) {
@@ -330,28 +288,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return databaseSelection?.viewSelection; return databaseSelection?.viewSelection;
}); });
virtualPadding$ = signal(0); private readonly virtualPadding$ = signal(0);
get dataSource(): DatabaseBlockDataSource {
if (!this._dataSource) {
this._dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
dataSource.serviceSet(EditorHostKey, this.host);
this.std.provider
.getAll(ExternalGroupByConfigProvider)
.forEach(config => {
dataSource.serviceSet(
ExternalGroupByConfigProvider(config.name),
config
);
});
});
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && this.dataSource.viewManager.viewGet(id)) {
this.dataSource.viewManager.setCurrentView(id);
}
}
return this._dataSource;
}
get optionsConfig(): DatabaseViewExtensionOptions { get optionsConfig(): DatabaseViewExtensionOptions {
return { return {
@@ -369,15 +306,15 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return this.rootComponent; return this.rootComponent;
} }
get view() {
return this.dataView.expose;
}
private renderDatabaseOps() { private renderDatabaseOps() {
if (this.dataSource.readonly$.value) { if (this.dataSource.value.readonly$.value) {
return nothing; return nothing;
} }
return html` <div class="database-ops" @click="${this._clickDatabaseOps}"> return html` <div
data-testid="database-ops"
class="${databaseOpsStyles}"
@click="${this.clickDatabaseOps}"
>
${MoreHorizontalIcon()} ${MoreHorizontalIcon()}
</div>`; </div>`;
} }
@@ -386,6 +323,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
super.connectedCallback(); super.connectedCallback();
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true'); this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
this.classList.add(databaseBlockStyles);
this.listenFullWidthChange(); this.listenFullWidthChange();
} }
@@ -402,85 +340,97 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
}) })
); );
} }
private readonly dataViewRootLogic = lazy(
override renderBlock() { () =>
const peekViewService = this.std.getOptional(PeekViewProvider); new DataViewRootUILogic({
const telemetryService = this.std.getOptional(TelemetryProvider); virtualPadding$: this.virtualPadding$,
return html` bindHotkey: hotkeys => {
<div return {
contenteditable="false" dispose: this.host.event.bindHotkey(hotkeys, {
style="position: relative;background-color: var(--affine-background-primary-color);border-radius: 4px" blockId: this.topContenteditableElement?.blockId ?? this.blockId,
> }),
${this.dataView.render({ };
virtualPadding$: this.virtualPadding$, },
bindHotkey: this._bindHotkey, handleEvent: (name, handler) => {
handleEvent: this._handleEvent, return {
selection$: this.viewSelection$, dispose: this.host.event.add(name, handler, {
setSelection: this.setSelection,
dataSource: this.dataSource,
headerWidget: this.headerWidget,
onDrag: this.onDrag,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
},
},
eventTrace: (key, params) => {
telemetryService?.track(key, {
...(params as TelemetryEventMap[typeof key]),
blockId: this.blockId, blockId: this.blockId,
}); }),
};
},
selection$: this.viewSelection$,
setSelection: this.setSelection,
dataSource: this.dataSource.value,
headerWidget: this.headerWidget,
onDrag: this.onDrag,
clipboard: this.std.clipboard,
notification: {
toast: message => {
const notification = this.std.getOptional(NotificationProvider);
if (notification) {
notification.toast(message);
} else {
toast(this.host, message);
}
}, },
detailPanelConfig: { },
openDetailPanel: (target, data) => { eventTrace: (key, params) => {
if (peekViewService) { const telemetryService = this.std.getOptional(TelemetryProvider);
const openDoc = (docId: string) => { telemetryService?.track(key, {
return peekViewService.peek({ ...(params as TelemetryEventMap[typeof key]),
docId, blockId: this.blockId,
databaseId: this.blockId, });
databaseDocId: this.model.store.id, },
databaseRowId: data.rowId, detailPanelConfig: {
target: this, openDetailPanel: (target, data) => {
}); const peekViewService = this.std.getOptional(PeekViewProvider);
}; if (peekViewService) {
const doc = getSingleDocIdFromText( const openDoc = (docId: string) => {
this.model.store.getBlock(data.rowId)?.model?.text return peekViewService.peek({
); docId,
if (doc) { databaseId: this.blockId,
return openDoc(doc); databaseDocId: this.model.store.id,
} databaseRowId: data.rowId,
const abort = new AbortController(); target: this,
return new Promise<void>(focusBack => {
peekViewService
.peek(
{
target,
template: this.createTemplate(data, docId => {
// abort.abort();
openDoc(docId).then(focusBack).catch(focusBack);
}),
},
{ abortSignal: abort.signal }
)
.then(focusBack)
.catch(focusBack);
}); });
} else { };
return popSideDetail( const doc = getSingleDocIdFromText(
this.createTemplate(data, () => { this.model.store.getBlock(data.rowId)?.model?.text
// );
}) if (doc) {
); return openDoc(doc);
} }
}, const abort = new AbortController();
return new Promise<void>(focusBack => {
peekViewService
.peek(
{
target,
template: this.createTemplate(data, docId => {
// abort.abort();
openDoc(docId).then(focusBack).catch(focusBack);
}),
},
{ abortSignal: abort.signal }
)
.then(focusBack)
.catch(focusBack);
});
} else {
return popSideDetail(
this.createTemplate(data, () => {
//
})
);
}
}, },
})} },
})
);
override renderBlock() {
return html`
<div contenteditable="false" class="${databaseContentStyles}">
${this.dataViewRootLogic.value.render()}
</div> </div>
`; `;
} }
@@ -70,3 +70,19 @@ export const dividerV = css({
backgroundColor: 'var(--affine-divider-color)', backgroundColor: 'var(--affine-divider-color)',
margin: '0 8px', margin: '0 8px',
}); });
export const dv = {
p2,
p4,
p8,
hover,
icon16,
icon20,
border,
round4,
round8,
color2,
shadow2,
dividerH,
dividerV,
};
+137 -160
View File
@@ -2,42 +2,43 @@ import type {
DatabaseAllEvents, DatabaseAllEvents,
EventTraceFn, EventTraceFn,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import type { DisposableMember } from '@blocksuite/global/disposable';
import { IS_MOBILE } from '@blocksuite/global/env'; import { IS_MOBILE } from '@blocksuite/global/env';
import { BlockSuiteError } from '@blocksuite/global/exceptions';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std'; import {
type Clipboard,
type EventName,
ShadowlessElement,
type UIEventHandler,
} from '@blocksuite/std';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { css, unsafeCSS } from 'lit'; import { css, unsafeCSS } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js'; import { ref } from 'lit/directives/ref.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import { dataViewCommonStyle } from './common/css-variable.js'; import { dataViewCommonStyle } from './common/css-variable.js';
import type { DataViewSelection, DataViewSelectionState } from './types.js'; import type { DataSource } from './data-source/index.js';
import type { DataViewSelection } from './types.js';
import { cacheComputed } from './utils/cache.js';
import { renderUniLit } from './utils/uni-component/index.js'; import { renderUniLit } from './utils/uni-component/index.js';
import type { DataViewInstance, DataViewProps } from './view/types.js'; import type { DataViewUILogicBase } from './view/data-view-base.js';
import type { SingleView } from './view-manager/single-view.js'; import type { SingleView } from './view-manager/single-view.js';
import type { DataViewWidget } from './widget/index.js';
type ViewProps = { export type DataViewRendererConfig = {
view: SingleView; clipboard: Clipboard;
selection$: ReadonlySignal<DataViewSelectionState>; onDrag?: (evt: MouseEvent, id: string) => () => void;
setSelection: (selection?: DataViewSelectionState) => void; notification: {
bindHotkey: DataViewProps['bindHotkey']; toast: (message: string) => void;
handleEvent: DataViewProps['handleEvent']; };
}; virtualPadding$: ReadonlySignal<number>;
headerWidget: DataViewWidget | undefined;
export type DataViewRendererConfig = Pick< handleEvent: (name: EventName, handler: UIEventHandler) => DisposableMember;
DataViewProps, bindHotkey: (hotkeys: Record<string, UIEventHandler>) => DisposableMember;
| 'bindHotkey' dataSource: DataSource;
| 'handleEvent'
| 'virtualPadding$'
| 'clipboard'
| 'dataSource'
| 'headerWidget'
| 'onDrag'
| 'notification'
> & {
selection$: ReadonlySignal<DataViewSelection | undefined>; selection$: ReadonlySignal<DataViewSelection | undefined>;
setSelection: (selection: DataViewSelection | undefined) => void; setSelection: (selection: DataViewSelection | undefined) => void;
eventTrace: EventTraceFn<DatabaseAllEvents>; eventTrace: EventTraceFn<DatabaseAllEvents>;
@@ -52,7 +53,104 @@ export type DataViewRendererConfig = Pick<
}; };
}; };
export class DataViewRenderer extends SignalWatcher( export class DataViewRootUILogic {
private get dataSource() {
return this.config.dataSource;
}
private get viewManager() {
return this.dataSource.viewManager;
}
private createDataViewUILogic(viewId: string): DataViewUILogicBase {
const view = this.viewManager.viewGet(viewId);
if (!view) {
throw new BlockSuiteError(
BlockSuiteError.ErrorCode.DatabaseBlockError,
`View ${viewId} not found`
);
}
const pcLogic = view.meta.renderer.pcLogic;
const mobileLogic = view.meta.renderer.mobileLogic;
const logic = (IS_MOBILE ? mobileLogic : pcLogic) ?? pcLogic;
return new (logic(view))(this, view);
}
private readonly views$ = cacheComputed(this.viewManager.views$, viewId =>
this.createDataViewUILogic(viewId)
);
private readonly viewsMap$ = computed(() => {
return Object.fromEntries(
this.views$.list.value.map(logic => [logic.view.id, logic])
);
});
private readonly _uiRef = signal<DataViewRootUI>();
get selection$() {
return this.config.selection$;
}
setSelection(selection?: DataViewSelection) {
this.config.setSelection(selection);
}
constructor(public readonly config: DataViewRendererConfig) {}
get dataViewRenderer() {
return this._uiRef.value;
}
readonly currentViewId$ = computed(() => {
return this.dataSource.viewManager.currentViewId$.value;
});
readonly currentView$ = computed(() => {
const currentViewId = this.currentViewId$.value;
if (!currentViewId) {
return;
}
return this.viewsMap$.value[currentViewId];
});
focusFirstCell = () => {
this.currentView$.value?.focusFirstCell();
};
openDetailPanel = (ops: {
view: SingleView;
rowId: string;
onClose?: () => void;
}) => {
const openDetailPanel = this.config.detailPanelConfig.openDetailPanel;
const target = this.dataViewRenderer;
if (openDetailPanel && target) {
openDetailPanel(target, {
view: ops.view,
rowId: ops.rowId,
})
.catch(console.error)
.finally(ops.onClose);
}
};
setupViewChangeListener() {
let preId: string | undefined = undefined;
return this.currentViewId$.subscribe(current => {
if (current !== preId) {
this.config.setSelection(undefined);
}
preId = current;
});
}
render() {
return html` <affine-data-view-renderer
${ref(this._uiRef)}
.logic="${this}"
></affine-data-view-renderer>`;
}
}
export class DataViewRootUI extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
static override styles = css` static override styles = css`
@@ -63,63 +161,14 @@ export class DataViewRenderer extends SignalWatcher(
} }
`; `;
private readonly _view = signal<DataViewInstance>();
@property({ attribute: false }) @property({ attribute: false })
accessor config!: DataViewRendererConfig; accessor logic!: DataViewRootUILogic;
private readonly currentViewId$ = computed(() => { @state()
return this.config.dataSource.viewManager.currentViewId$.value; accessor currentView: string | undefined = undefined;
});
viewMap$ = computed(() => {
const manager = this.config.dataSource.viewManager;
return Object.fromEntries(
manager.views$.value.map(view => [view, manager.viewGet(view)])
);
});
currentViewConfig$ = computed<ViewProps | undefined>(() => {
const currentViewId = this.currentViewId$.value;
if (!currentViewId) {
return;
}
const view = this.viewMap$.value[currentViewId];
if (!view) {
return;
}
return {
view: view,
selection$: computed(() => {
const selection$ = this.config.selection$;
if (selection$.value?.viewId === currentViewId) {
return selection$.value;
}
return;
}),
setSelection: selection => {
this.config.setSelection(selection);
},
handleEvent: (name, handler) =>
this.config.handleEvent(name, context => {
return handler(context);
}),
bindHotkey: hotkeys =>
this.config.bindHotkey(
Object.fromEntries(
Object.entries(hotkeys).map(([key, fn]) => [
key,
ctx => {
return fn(ctx);
},
])
)
),
};
});
focusFirstCell = () => { focusFirstCell = () => {
this.view?.focusFirstCell(); this.logic.focusFirstCell();
}; };
openDetailPanel = (ops: { openDetailPanel = (ops: {
@@ -127,72 +176,12 @@ export class DataViewRenderer extends SignalWatcher(
rowId: string; rowId: string;
onClose?: () => void; onClose?: () => void;
}) => { }) => {
const openDetailPanel = this.config.detailPanelConfig.openDetailPanel; this.logic.openDetailPanel(ops);
if (openDetailPanel) {
openDetailPanel(this, {
view: ops.view,
rowId: ops.rowId,
})
.catch(console.error)
.finally(ops.onClose);
}
}; };
get view() {
return this._view.value;
}
private renderView(viewData?: ViewProps) {
if (!viewData) {
return;
}
const props: DataViewProps = {
dataViewEle: this,
headerWidget: this.config.headerWidget,
onDrag: this.config.onDrag,
dataSource: this.config.dataSource,
virtualPadding$: this.config.virtualPadding$,
clipboard: this.config.clipboard,
notification: this.config.notification,
view: viewData.view,
selection$: viewData.selection$,
setSelection: viewData.setSelection,
bindHotkey: viewData.bindHotkey,
handleEvent: viewData.handleEvent,
eventTrace: (key, params) => {
this.config.eventTrace(key, {
...(params as DatabaseAllEvents[typeof key]),
viewId: viewData.view.id,
viewType: viewData.view.type,
});
},
};
const renderer = viewData.view.meta.renderer;
const view =
(IS_MOBILE ? renderer.mobileView : renderer.view) ?? renderer.view;
return keyed(
viewData.view.id,
renderUniLit(
view,
{ props },
{
ref: this._view,
}
)
);
}
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
let preId: string | undefined = undefined; this.disposables.add(this.logic.setupViewChangeListener());
this.disposables.add(
this.currentViewId$.subscribe(current => {
if (current !== preId) {
this.config.setSelection(undefined);
}
preId = current;
})
);
} }
override render() { override render() {
@@ -201,34 +190,22 @@ export class DataViewRenderer extends SignalWatcher(
'data-view-root': true, 'data-view-root': true,
'prevent-reference-popup': true, 'prevent-reference-popup': true,
}); });
const currentView = this.logic.currentView$.value;
if (!currentView) {
return;
}
return html` return html`
<div style="display: contents" class="${containerClass}"> <div style="display: contents" class="${containerClass}">
${this.renderView(this.currentViewConfig$.value)} ${renderUniLit(currentView.renderer, {
logic: currentView,
})}
</div> </div>
`; `;
} }
@state()
accessor currentView: string | undefined = undefined;
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'affine-data-view-renderer': DataViewRenderer; 'affine-data-view-renderer': DataViewRootUI;
}
}
export class DataView {
private readonly _ref = createRef<DataViewRenderer>();
get expose() {
return this._ref.value?.view;
}
render(props: DataViewRendererConfig) {
return html` <affine-data-view-renderer
${ref(this._ref)}
.config="${props}"
></affine-data-view-renderer>`;
} }
} }
@@ -2,7 +2,7 @@ import { DataViewPropertiesSettingView } from './common/properties.js';
import { Button } from './component/button/button.js'; import { Button } from './component/button/button.js';
import { Overflow } from './component/overflow/overflow.js'; import { Overflow } from './component/overflow/overflow.js';
import { MultiTagSelect, MultiTagView } from './component/tags/index.js'; import { MultiTagSelect, MultiTagView } from './component/tags/index.js';
import { DataViewRenderer } from './data-view.js'; import { DataViewRootUI } from './data-view.js';
import { RecordDetail } from './detail/detail.js'; import { RecordDetail } from './detail/detail.js';
import { RecordField } from './detail/field.js'; import { RecordField } from './detail/field.js';
import { VariableRefView } from './expression/ref/ref-view.js'; import { VariableRefView } from './expression/ref/ref-view.js';
@@ -15,7 +15,7 @@ import { AffineLitIcon, UniAnyRender, UniLit } from './index.js';
import { AnyRender } from './utils/uni-component/render-template.js'; import { AnyRender } from './utils/uni-component/render-template.js';
export function coreEffects() { export function coreEffects() {
customElements.define('affine-data-view-renderer', DataViewRenderer); customElements.define('affine-data-view-renderer', DataViewRootUI);
customElements.define('any-render', AnyRender); customElements.define('any-render', AnyRender);
customElements.define( customElements.define(
'data-view-properties-setting', 'data-view-properties-setting',
@@ -1,7 +1,7 @@
export * from './common/index.js'; export * from './common/index.js';
export * from './component/index.js'; export * from './component/index.js';
export { DataSourceBase } from './data-source/base.js'; export { DataSourceBase } from './data-source/base.js';
export { DataView } from './data-view.js'; export { DataViewRootUILogic } from './data-view.js';
export * from './filter/index.js'; export * from './filter/index.js';
export * from './group-by'; export * from './group-by';
export * from './logical/index.js'; export * from './logical/index.js';
@@ -183,7 +183,6 @@ export class TypeSystem {
// eslint-disable-next-line sonarjs/no-collapsible-if // eslint-disable-next-line sonarjs/no-collapsible-if
if (realArg != null) { if (realArg != null) {
if (!this._unify(newCtx, realArg, arg)) { if (!this._unify(newCtx, realArg, arg)) {
console.log('arg', realArg, arg);
return; return;
} }
} }
@@ -0,0 +1,32 @@
import { computed, type ReadonlySignal } from '@preact/signals-core';
export const cacheComputed = <T>(
ids: ReadonlySignal<string[]>,
create: (id: string) => T
) => {
const cache = new Map<string, T>();
const getOrCreate = (id: string): T => {
if (cache.has(id)) {
return cache.get(id)!;
}
const value = create(id);
if (value) {
cache.set(id, value);
}
return value;
};
return {
getOrCreate,
list: computed<T[]>(() => {
const list = ids.value;
const keys = new Set(cache.keys());
for (const [cachedId] of cache) {
keys.delete(cachedId);
}
for (const id of keys) {
cache.delete(id);
}
return list.map(id => getOrCreate(id));
}),
};
};
@@ -1,2 +1,3 @@
export * from './lazy.js';
export * from './uni-component/index.js'; export * from './uni-component/index.js';
export * from './uni-icon.js'; export * from './uni-icon.js';
@@ -0,0 +1,11 @@
export const lazy = <T>(fn: () => T): { value: T } => {
let data: { value: T } | undefined;
return {
get value() {
if (!data) {
data = { value: fn() };
}
return data.value;
},
};
};
@@ -1,17 +1,106 @@
import type {
DatabaseAllEvents,
DatabaseAllViewEvents,
EventTraceFn,
} from '@blocksuite/affine-shared/services';
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { DisposableMember } from '@blocksuite/global/disposable';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std'; import {
type EventName,
ShadowlessElement,
type UIEventHandler,
} from '@blocksuite/std';
import { computed } from '@preact/signals-core';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import type { DataViewRootUILogic } from '../data-view.js';
import type { DataViewSelection } from '../types.js'; import type { DataViewSelection } from '../types.js';
import type { SingleView } from '../view-manager/single-view.js'; import type { SingleView } from '../view-manager/single-view.js';
import type { DataViewWidget } from '../widget/index.js';
import type { DataViewInstance, DataViewProps } from './types.js'; import type { DataViewInstance, DataViewProps } from './types.js';
export abstract class DataViewBase< export abstract class DataViewBase<
T extends SingleView = SingleView,
Selection extends DataViewSelection = DataViewSelection, Selection extends DataViewSelection = DataViewSelection,
> extends SignalWatcher(WithDisposable(ShadowlessElement)) { > extends SignalWatcher(WithDisposable(ShadowlessElement)) {
abstract expose: DataViewInstance; abstract expose: DataViewInstance;
@property({ attribute: false }) @property({ attribute: false })
accessor props!: DataViewProps<T, Selection>; accessor props!: DataViewProps<Selection>;
} }
export abstract class DataViewUIBase<
Logic extends DataViewUILogicBase = DataViewUILogicBase,
> extends SignalWatcher(WithDisposable(ShadowlessElement)) {
@property({ attribute: false })
accessor logic!: Logic;
}
export abstract class DataViewUILogicBase<
T extends SingleView = SingleView,
Selection extends DataViewSelection = DataViewSelection,
> {
constructor(
public readonly root: DataViewRootUILogic,
public readonly view: T
) {}
get headerWidget(): DataViewWidget | undefined {
return this.root.config.headerWidget;
}
bindHotkey(hotkeys: Record<string, UIEventHandler>): DisposableMember {
return this.root.config.bindHotkey(
Object.fromEntries(
Object.entries(hotkeys).map(([key, fn]) => [
key,
ctx => {
return fn(ctx);
},
])
)
);
}
handleEvent(name: EventName, handler: UIEventHandler): DisposableMember {
return this.root.config.handleEvent(name, context => {
return handler(context);
});
}
setSelection(selection?: Selection): void {
this.root.setSelection(selection);
}
selection$ = computed<Selection | undefined>(() => {
const selection$ = this.root.selection$;
if (selection$.value?.viewId === this.view.id) {
return selection$.value as Selection | undefined;
}
return;
});
eventTrace: EventTraceFn<DatabaseAllViewEvents> = (key, params) => {
this.root.config.eventTrace(key, {
...(params as DatabaseAllEvents[typeof key]),
viewId: this.view.id,
viewType: this.view.type,
});
};
abstract clearSelection: () => void;
abstract addRow: (position: InsertToPosition) => string | undefined;
abstract focusFirstCell: () => void;
abstract showIndicator: (evt: MouseEvent) => boolean;
abstract hideIndicator: () => void;
abstract moveTo: (id: string, evt: MouseEvent) => void;
abstract renderer: UniComponent<{
logic: DataViewUILogicBase<T, Selection>;
}>;
}
type Constructor<T extends abstract new (...args: any) => any> = new (
...args: ConstructorParameters<T>
) => InstanceType<T>;
export type DataViewUILogicBaseConstructor = Constructor<
typeof DataViewUILogicBase
>;
@@ -2,6 +2,7 @@ import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { SingleView } from '../view-manager/single-view.js'; import type { SingleView } from '../view-manager/single-view.js';
import type { ViewManager } from '../view-manager/view-manager.js'; import type { ViewManager } from '../view-manager/view-manager.js';
import type { DataViewUILogicBaseConstructor } from './data-view-base.js';
import type { DataViewInstance, DataViewProps } from './types.js'; import type { DataViewInstance, DataViewProps } from './types.js';
export type BasicViewDataType< export type BasicViewDataType<
@@ -48,9 +49,10 @@ type DataViewComponent = UniComponent<
>; >;
export interface DataViewRendererConfig { export interface DataViewRendererConfig {
view: DataViewComponent;
mobileView?: DataViewComponent; mobileView?: DataViewComponent;
icon: UniComponent; icon: UniComponent;
pcLogic: (view: SingleView) => DataViewUILogicBaseConstructor;
mobileLogic?: (view: SingleView) => DataViewUILogicBaseConstructor;
} }
export type ViewMeta< export type ViewMeta<
@@ -1,3 +1,4 @@
export * from './convert.js'; export * from './convert.js';
export * from './data-view.js'; export * from './data-view.js';
export * from './data-view-base.js';
export * from './types.js'; export * from './types.js';
@@ -4,44 +4,21 @@ import type {
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { Disposable } from '@blocksuite/global/disposable'; import type { Disposable } from '@blocksuite/global/disposable';
import type { Clipboard, EventName, UIEventHandler } from '@blocksuite/std'; import type { EventName, UIEventHandler } from '@blocksuite/std';
import type { ReadonlySignal } from '@preact/signals-core'; import type { ReadonlySignal } from '@preact/signals-core';
import type { DataSource } from '../common/index.js';
import type { DataViewRenderer } from '../data-view.js';
import type { DataViewSelection } from '../types.js'; import type { DataViewSelection } from '../types.js';
import type { SingleView } from '../view-manager/index.js'; import type { SingleView } from '../view-manager/index.js';
import type { DataViewWidget } from '../widget/index.js'; import type { DataViewWidget } from '../widget/index.js';
export interface DataViewProps< export interface DataViewProps<
T extends SingleView = SingleView,
Selection extends DataViewSelection = DataViewSelection, Selection extends DataViewSelection = DataViewSelection,
> { > {
dataViewEle: DataViewRenderer;
headerWidget?: DataViewWidget; headerWidget?: DataViewWidget;
view: T;
dataSource: DataSource;
bindHotkey: (hotkeys: Record<string, UIEventHandler>) => Disposable; bindHotkey: (hotkeys: Record<string, UIEventHandler>) => Disposable;
handleEvent: (name: EventName, handler: UIEventHandler) => Disposable; handleEvent: (name: EventName, handler: UIEventHandler) => Disposable;
setSelection: (selection?: Selection) => void; setSelection: (selection?: Selection) => void;
selection$: ReadonlySignal<Selection | undefined>; selection$: ReadonlySignal<Selection | undefined>;
virtualPadding$: ReadonlySignal<number>;
onDrag?: (evt: MouseEvent, id: string) => () => void;
clipboard: Clipboard;
notification: {
toast: (message: string) => void;
};
eventTrace: EventTraceFn<DatabaseAllViewEvents>; eventTrace: EventTraceFn<DatabaseAllViewEvents>;
} }
@@ -1,8 +1,10 @@
import type { UniComponent } from '@blocksuite/affine-shared/types'; import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { DataViewInstance } from '../view/types.js'; import type { DataViewUILogicBase } from '../view/data-view-base.js';
export type DataViewWidgetProps = { export type DataViewWidgetProps<
dataViewInstance: DataViewInstance; ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
> = {
dataViewLogic: ViewLogic;
}; };
export type DataViewWidget = UniComponent<DataViewWidgetProps>; export type DataViewWidget = UniComponent<DataViewWidgetProps>;
@@ -2,30 +2,27 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import type { DataViewInstance } from '../view/types.js'; import type { DataViewUILogicBase } from '../view/data-view-base.js';
import type { SingleView } from '../view-manager/index.js';
import type { DataViewWidgetProps } from './types.js'; import type { DataViewWidgetProps } from './types.js';
export class WidgetBase<View extends SingleView = SingleView> export class WidgetBase<
ViewLogic extends DataViewUILogicBase = DataViewUILogicBase,
>
extends SignalWatcher(WithDisposable(ShadowlessElement)) extends SignalWatcher(WithDisposable(ShadowlessElement))
implements DataViewWidgetProps implements DataViewWidgetProps<ViewLogic>
{ {
get dataSource() { get dataSource() {
return this.view.manager.dataSource; return this.viewManager.dataSource;
} }
get view() { get view() {
return this.dataViewInstance.view; return this.dataViewLogic.view;
} }
get viewManager() { get viewManager() {
return this.view.manager; return this.view.manager;
} }
get viewMethods() {
return this.dataViewInstance;
}
@property({ attribute: false }) @property({ attribute: false })
accessor dataViewInstance!: DataViewInstance<View>; accessor dataViewLogic!: ViewLogic;
} }
@@ -1,48 +1,7 @@
import { DataViewKanban, TableViewSelector } from './index.js'; import { kanbanEffects } from './kanban/effect.js';
import { MobileKanbanCard } from './kanban/mobile/card.js'; import { tableEffects } from './table/effect.js';
import { MobileKanbanCell } from './kanban/mobile/cell.js';
import { MobileKanbanGroup } from './kanban/mobile/group.js';
import { MobileDataViewKanban } from './kanban/mobile/kanban-view.js';
import { KanbanCard } from './kanban/pc/card.js';
import { KanbanCell } from './kanban/pc/cell.js';
import { KanbanGroup } from './kanban/pc/group.js';
import { KanbanHeader } from './kanban/pc/header.js';
import { MobileTableCell } from './table/mobile/cell.js';
import { MobileTableColumnHeader } from './table/mobile/column-header.js';
import { MobileTableGroup } from './table/mobile/group.js';
import { MobileTableHeader } from './table/mobile/header.js';
import { MobileTableRow } from './table/mobile/row.js';
import { MobileDataViewTable } from './table/mobile/table-view.js';
import { pcEffects } from './table/pc/effect.js';
import { pcVirtualEffects } from './table/pc-virtual/effect.js';
import { DataBaseColumnStats } from './table/stats/column-stats-bar.js';
import { DatabaseColumnStatsCell } from './table/stats/column-stats-column.js';
export function viewPresetsEffects() { export function viewPresetsEffects() {
customElements.define('affine-data-view-kanban-card', KanbanCard); kanbanEffects();
customElements.define('mobile-kanban-card', MobileKanbanCard); tableEffects();
customElements.define('affine-data-view-kanban-cell', KanbanCell);
customElements.define('mobile-kanban-cell', MobileKanbanCell);
customElements.define('affine-data-view-kanban-group', KanbanGroup);
customElements.define('mobile-kanban-group', MobileKanbanGroup);
customElements.define('affine-data-view-kanban', DataViewKanban);
customElements.define('mobile-data-view-kanban', MobileDataViewKanban);
customElements.define('affine-data-view-kanban-header', KanbanHeader);
customElements.define('mobile-table-cell', MobileTableCell);
customElements.define('mobile-table-group', MobileTableGroup);
customElements.define('mobile-data-view-table', MobileDataViewTable);
customElements.define('mobile-table-header', MobileTableHeader);
customElements.define('mobile-table-column-header', MobileTableColumnHeader);
customElements.define('mobile-table-row', MobileTableRow);
customElements.define('affine-database-column-stats', DataBaseColumnStats);
customElements.define(
'affine-database-column-stats-cell',
DatabaseColumnStatsCell
);
customElements.define('affine-database-table-selector', TableViewSelector);
pcEffects();
pcVirtualEffects();
} }
@@ -0,0 +1,7 @@
import { mobileEffects } from './mobile/effect.js';
import { pcEffects } from './pc/effect.js';
export function kanbanEffects() {
pcEffects();
mobileEffects();
}
@@ -1,5 +1,4 @@
export * from './define.js'; export * from './define.js';
export * from './kanban-view-manager.js'; export * from './kanban-view-manager.js';
export * from './pc/kanban-view.js';
export * from './renderer.js'; export * from './renderer.js';
export * from './selection.js'; export * from './selection.js';
@@ -10,8 +10,8 @@ import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js'; import type { KanbanColumn } from '../kanban-view-manager.js';
import type { KanbanColumn, KanbanSingleView } from '../kanban-view-manager.js'; import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
import { popCardMenu } from './menu.js'; import { popCardMenu } from './menu.js';
const styles = css` const styles = css`
@@ -94,7 +94,7 @@ export class MobileKanbanCard extends SignalWatcher(
private readonly clickCenterPeek = (e: MouseEvent) => { private readonly clickCenterPeek = (e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
this.dataViewEle.openDetailPanel({ this.kanbanViewLogic.root.openDetailPanel({
view: this.view, view: this.view,
rowId: this.cardId, rowId: this.cardId,
}); });
@@ -104,10 +104,9 @@ export class MobileKanbanCard extends SignalWatcher(
e.stopPropagation(); e.stopPropagation();
popCardMenu( popCardMenu(
popupTargetFromElement(e.currentTarget as HTMLElement), popupTargetFromElement(e.currentTarget as HTMLElement),
this.view,
this.groupKey, this.groupKey,
this.cardId, this.cardId,
this.dataViewEle this.kanbanViewLogic
); );
}; };
@@ -126,10 +125,10 @@ export class MobileKanbanCard extends SignalWatcher(
return html` <mobile-kanban-cell return html` <mobile-kanban-cell
.contentOnly="${false}" .contentOnly="${false}"
data-column-id="${column.id}" data-column-id="${column.id}"
.view="${this.view}"
.groupKey="${this.groupKey}" .groupKey="${this.groupKey}"
.column="${column}" .column="${column}"
.cardId="${this.cardId}" .cardId="${this.cardId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></mobile-kanban-cell>`; ></mobile-kanban-cell>`;
} }
)} )}
@@ -184,10 +183,10 @@ export class MobileKanbanCard extends SignalWatcher(
<mobile-kanban-cell <mobile-kanban-cell
.contentOnly="${true}" .contentOnly="${true}"
data-column-id="${title.id}" data-column-id="${title.id}"
.view="${this.view}"
.groupKey="${this.groupKey}" .groupKey="${this.groupKey}"
.column="${title}" .column="${title}"
.cardId="${this.cardId}" .cardId="${this.cardId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></mobile-kanban-cell> ></mobile-kanban-cell>
</div>`; </div>`;
} }
@@ -205,9 +204,6 @@ export class MobileKanbanCard extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor cardId!: string; accessor cardId!: string;
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false }) @property({ attribute: false })
accessor groupKey!: string; accessor groupKey!: string;
@@ -215,7 +211,11 @@ export class MobileKanbanCard extends SignalWatcher(
accessor isFocus = false; accessor isFocus = false;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: KanbanSingleView; accessor kanbanViewLogic!: MobileKanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
} }
declare global { declare global {
@@ -14,7 +14,7 @@ import type {
} from '../../../core/property/index.js'; } from '../../../core/property/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js'; import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import type { Property } from '../../../core/view-manager/property.js'; import type { Property } from '../../../core/view-manager/property.js';
import type { KanbanSingleView } from '../kanban-view-manager.js'; import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css` const styles = css`
mobile-kanban-cell { mobile-kanban-cell {
@@ -53,7 +53,7 @@ export class MobileKanbanCell extends SignalWatcher(
private readonly _cell = signal<DataViewCellLifeCycle>(); private readonly _cell = signal<DataViewCellLifeCycle>();
isSelectionEditing$ = computed(() => { isSelectionEditing$ = computed(() => {
const selection = this.kanban?.props.selection$.value; const selection = this.kanbanViewLogic.selection$.value;
if (selection?.selectionType !== 'cell') { if (selection?.selectionType !== 'cell') {
return false; return false;
} }
@@ -73,8 +73,8 @@ export class MobileKanbanCell extends SignalWatcher(
if (this.view.readonly$.value) { if (this.view.readonly$.value) {
return; return;
} }
const setSelection = this.kanban?.props.setSelection; const setSelection = this.kanbanViewLogic.setSelection;
const viewId = this.kanban?.props.view.id; const viewId = this.kanbanViewLogic.view.id;
if (setSelection && viewId) { if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) { if (editing && this.cell?.beforeEnterEditMode() === false) {
return; return;
@@ -95,14 +95,6 @@ export class MobileKanbanCell extends SignalWatcher(
return this._cell.value; return this._cell.value;
} }
get kanban() {
return this.closest('mobile-data-view-kanban');
}
get selection() {
return this.closest('mobile-data-view-kanban')?.props.selection$.value;
}
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this.column.readonly$.value) return; if (this.column.readonly$.value) return;
@@ -172,7 +164,11 @@ export class MobileKanbanCell extends SignalWatcher(
isEditing$ = signal(false); isEditing$ = signal(false);
@property({ attribute: false }) @property({ attribute: false })
accessor view!: KanbanSingleView; accessor kanbanViewLogic!: MobileKanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
} }
declare global { declare global {
@@ -0,0 +1,11 @@
import { MobileKanbanCard } from './card.js';
import { MobileKanbanCell } from './cell.js';
import { MobileKanbanGroup } from './group.js';
import { MobileKanbanViewUI } from './kanban-view-ui-logic.js';
export function mobileEffects() {
customElements.define('mobile-kanban-card', MobileKanbanCard);
customElements.define('mobile-kanban-cell', MobileKanbanCell);
customElements.define('mobile-kanban-group', MobileKanbanGroup);
customElements.define('mobile-data-view-kanban-ui', MobileKanbanViewUI);
}
@@ -11,11 +11,10 @@ import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js'; import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js'; import type { Group } from '../../../core/group-by/trait.js';
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js'; import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
import type { KanbanSingleView } from '../kanban-view-manager.js'; import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css` const styles = css`
mobile-kanban-group { mobile-kanban-group {
@@ -112,9 +111,8 @@ export class MobileKanbanGroup extends SignalWatcher(
<mobile-kanban-card <mobile-kanban-card
data-card-id="${row.rowId}" data-card-id="${row.rowId}"
.groupKey="${this.group.key}" .groupKey="${this.group.key}"
.dataViewEle="${this.dataViewEle}"
.view="${this.view}"
.cardId="${row.rowId}" .cardId="${row.rowId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></mobile-kanban-card> ></mobile-kanban-card>
`; `;
} }
@@ -133,14 +131,15 @@ export class MobileKanbanGroup extends SignalWatcher(
`; `;
} }
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false }) @property({ attribute: false })
accessor group!: Group; accessor group!: Group;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: KanbanSingleView; accessor kanbanViewLogic!: MobileKanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
} }
declare global { declare global {
@@ -0,0 +1,168 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { css } from '@emotion/css';
import { signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import { sortable } from '../../../core/utils/wc-dnd/sort/sort-context.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection';
const mobileKanbanViewWrapper = css({
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
});
const mobileKanbanGroups = css({
position: 'relative',
zIndex: 1,
display: 'flex',
gap: '20px',
paddingBottom: '4px',
overflowX: 'scroll',
overflowY: 'hidden',
});
const mobileAddGroup = css({
height: '32px',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
padding: '4px',
borderRadius: '4px',
fontSize: '16px',
color: `var(${unsafeCSSVarV2('icon/primary')})`,
});
export class MobileKanbanViewUILogic extends DataViewUILogicBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
ui$ = signal<MobileKanbanViewUI | undefined>(undefined);
private get readonly() {
return this.view.readonly$.value;
}
clearSelection = () => {};
addRow = (position: InsertToPosition) => {
if (this.readonly) return;
return this.view.rowAdd(position);
};
focusFirstCell = () => {};
showIndicator = (_evt: MouseEvent) => {
return false;
};
hideIndicator = () => {};
moveTo = () => {};
get groupManager() {
return this.view.groupTrait;
}
renderAddGroup = () => {
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = this.groupManager.property$.value;
if (column) {
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
}),
],
},
});
};
return html` <div class="${mobileAddGroup}" @click="${add}">
${AddCursorIcon()}
</div>`;
};
renderer = createUniComponentFromWebComponent(MobileKanbanViewUI);
}
export class MobileKanbanViewUI extends DataViewUIBase<MobileKanbanViewUILogic> {
override connectedCallback(): void {
super.connectedCallback();
this.logic.ui$.value = this;
this.classList.add(mobileKanbanViewWrapper);
}
override render(): TemplateResult {
const groups = this.logic.groupManager.groupsDataList$.value;
if (!groups) {
return html``;
}
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.logic.headerWidget, {
dataViewLogic: this.logic,
})}
<div class="${mobileKanbanGroups}" style="${wrapperStyle}">
${repeat(
groups,
group => group.key,
group => {
return html` <mobile-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.kanbanViewLogic="${this.logic}"
.group="${group}"
></mobile-kanban-group>`;
}
)}
${this.logic.renderAddGroup()}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-kanban-ui': MobileKanbanViewUI;
}
}
@@ -1,149 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { css } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
import { sortable } from '../../../core/utils/wc-dnd/sort/sort-context.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection';
const styles = css`
mobile-data-view-kanban {
user-select: none;
display: flex;
flex-direction: column;
}
.mobile-kanban-groups {
position: relative;
z-index: 1;
display: flex;
gap: 20px;
padding-bottom: 4px;
overflow-x: scroll;
overflow-y: hidden;
}
.mobile-add-group {
height: 32px;
flex-shrink: 0;
display: flex;
align-items: center;
padding: 4px;
border-radius: 4px;
font-size: 16px;
color: ${unsafeCSSVarV2('icon/primary')};
}
`;
export class MobileDataViewKanban extends DataViewBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
static override styles = styles;
renderAddGroup = () => {
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = this.groupManager.property$.value;
if (column) {
column.dataUpdate(
() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div class="mobile-add-group" @click="${add}">
${AddCursorIcon()}
</div>`;
};
get expose(): DataViewInstance {
return {
clearSelection: () => {},
focusFirstCell: () => {},
getSelection: () => {
return this.props.selection$.value;
},
hideIndicator: () => {},
moveTo: () => {},
showIndicator: () => {
return false;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
get groupManager() {
return this.props.view.groupTrait;
}
override render() {
const groups = this.groupManager.groupsDataList$.value;
if (!groups) {
return html``;
}
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div class="mobile-kanban-groups" style="${wrapperStyle}">
${repeat(
groups,
group => group.key,
group => {
return html` <mobile-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.group="${group}"
></mobile-kanban-group>`;
}
)}
${this.renderAddGroup()}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-kanban': MobileDataViewKanban;
}
}
@@ -12,18 +12,16 @@ import {
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { html } from 'lit'; import { html } from 'lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { groupTraitKey } from '../../../core/group-by/trait.js'; import { groupTraitKey } from '../../../core/group-by/trait.js';
import type { KanbanSingleView } from '../kanban-view-manager.js'; import type { MobileKanbanViewUILogic } from './kanban-view-ui-logic.js';
export const popCardMenu = ( export const popCardMenu = (
ele: PopupTarget, ele: PopupTarget,
view: KanbanSingleView,
groupKey: string, groupKey: string,
cardId: string, cardId: string,
dataViewEle: DataViewRenderer kanbanViewLogic: MobileKanbanViewUILogic
) => { ) => {
const groupTrait = view.traitGet(groupTraitKey); const groupTrait = kanbanViewLogic.view.traitGet(groupTraitKey);
if (!groupTrait) { if (!groupTrait) {
return; return;
} }
@@ -34,8 +32,8 @@ export const popCardMenu = (
name: 'Expand Card', name: 'Expand Card',
prefix: ExpandFullIcon(), prefix: ExpandFullIcon(),
select: () => { select: () => {
dataViewEle.openDetailPanel({ kanbanViewLogic.root.openDetailPanel({
view: view, view: kanbanViewLogic.view,
rowId: cardId, rowId: cardId,
}); });
}, },
@@ -81,7 +79,10 @@ export const popCardMenu = (
${MoveLeftIcon()} ${MoveLeftIcon()}
</div>`, </div>`,
select: () => { select: () => {
view.addCard({ before: true, id: cardId }, groupKey); kanbanViewLogic.view.addCard(
{ before: true, id: cardId },
groupKey
);
}, },
}), }),
menu.action({ menu.action({
@@ -92,7 +93,10 @@ export const popCardMenu = (
${MoveRightIcon()} ${MoveRightIcon()}
</div>`, </div>`,
select: () => { select: () => {
view.addCard({ before: false, id: cardId }, groupKey); kanbanViewLogic.view.addCard(
{ before: false, id: cardId },
groupKey
);
}, },
}), }),
], ],
@@ -106,7 +110,7 @@ export const popCardMenu = (
}, },
prefix: DeleteIcon(), prefix: DeleteIcon(),
select: () => { select: () => {
view.rowsDelete([cardId]); kanbanViewLogic.view.rowsDelete([cardId]);
}, },
}), }),
], ],
@@ -2,15 +2,16 @@ import { popupTargetFromElement } from '@blocksuite/affine-components/context-me
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { CenterPeekIcon, MoreHorizontalIcon } from '@blocksuite/icons/lit'; import { CenterPeekIcon, MoreHorizontalIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import { signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { css, unsafeCSS } from 'lit'; import { css, unsafeCSS } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js'; import type { KanbanColumn } from '../kanban-view-manager.js';
import type { KanbanColumn, KanbanSingleView } from '../kanban-view-manager.js'; import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
import { openDetail, popCardMenu } from './menu.js'; import { openDetail, popCardMenu } from './menu.js';
const styles = css` const styles = css`
@@ -130,7 +131,7 @@ export class KanbanCard extends SignalWatcher(
e.stopPropagation(); e.stopPropagation();
const selection = this.getSelection(); const selection = this.getSelection();
if (selection) { if (selection) {
openDetail(this.dataViewEle, this.cardId, selection); openDetail(this.kanbanViewLogic, this.cardId, selection);
} }
}; };
@@ -149,7 +150,7 @@ export class KanbanCard extends SignalWatcher(
], ],
}; };
popCardMenu( popCardMenu(
this.dataViewEle, this.kanbanViewLogic,
popupTargetFromElement(ele), popupTargetFromElement(ele),
this.cardId, this.cardId,
selection selection
@@ -174,7 +175,7 @@ export class KanbanCard extends SignalWatcher(
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
const ref = target.closest('affine-data-view-kanban-cell') ?? this; const ref = target.closest('affine-data-view-kanban-cell') ?? this;
popCardMenu( popCardMenu(
this.dataViewEle, this.kanbanViewLogic,
popupTargetFromElement(ref), popupTargetFromElement(ref),
this.cardId, this.cardId,
selection selection
@@ -183,7 +184,7 @@ export class KanbanCard extends SignalWatcher(
}; };
private getSelection() { private getSelection() {
return this.closest('affine-data-view-kanban')?.selectionController; return this.kanbanViewLogic.selectionController;
} }
private renderBody(columns: KanbanColumn[]) { private renderBody(columns: KanbanColumn[]) {
@@ -201,10 +202,10 @@ export class KanbanCard extends SignalWatcher(
return html` <affine-data-view-kanban-cell return html` <affine-data-view-kanban-cell
.contentOnly="${false}" .contentOnly="${false}"
data-column-id="${column.id}" data-column-id="${column.id}"
.view="${this.view}"
.groupKey="${this.groupKey}" .groupKey="${this.groupKey}"
.column="${column}" .column="${column}"
.cardId="${this.cardId}" .cardId="${this.cardId}"
.kanbanViewLogic="${this.kanbanViewLogic}"
></affine-data-view-kanban-cell>`; ></affine-data-view-kanban-cell>`;
} }
)} )}
@@ -259,7 +260,7 @@ export class KanbanCard extends SignalWatcher(
<affine-data-view-kanban-cell <affine-data-view-kanban-cell
.contentOnly="${true}" .contentOnly="${true}"
data-column-id="${title.id}" data-column-id="${title.id}"
.view="${this.view}" .kanbanViewLogic="${this.kanbanViewLogic}"
.groupKey="${this.groupKey}" .groupKey="${this.groupKey}"
.column="${title}" .column="${title}"
.cardId="${this.cardId}" .cardId="${this.cardId}"
@@ -288,7 +289,7 @@ export class KanbanCard extends SignalWatcher(
if (selection) { if (selection) {
selection.selection = undefined; selection.selection = undefined;
} }
this.dataViewEle.openDetailPanel({ this.kanbanViewLogic.root.openDetailPanel({
view: this.view, view: this.view,
rowId: this.cardId, rowId: this.cardId,
onClose: () => { onClose: () => {
@@ -304,7 +305,7 @@ export class KanbanCard extends SignalWatcher(
const columns = this.view.properties$.value.filter( const columns = this.view.properties$.value.filter(
v => !this.view.isInHeader(v.id) v => !this.view.isInHeader(v.id)
); );
this.style.border = this.isFocus this.style.border = this.isFocus$.value
? '1px solid var(--affine-primary-color)' ? '1px solid var(--affine-primary-color)'
: ''; : '';
return html` return html`
@@ -316,17 +317,17 @@ export class KanbanCard extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor cardId!: string; accessor cardId!: string;
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false }) @property({ attribute: false })
accessor groupKey!: string; accessor groupKey!: string;
@state() isFocus$ = signal(false);
accessor isFocus = false;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: KanbanSingleView; accessor kanbanViewLogic!: KanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
} }
declare global { declare global {
@@ -4,7 +4,7 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import { signal } from '@preact/signals-core'; import { signal } from '@preact/signals-core';
import { css } from 'lit'; import { css } from 'lit';
import { property, state } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { import type {
@@ -13,8 +13,8 @@ import type {
} from '../../../core/property/index.js'; } from '../../../core/property/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js'; import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import type { Property } from '../../../core/view-manager/property.js'; import type { Property } from '../../../core/view-manager/property.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelection } from '../selection'; import type { KanbanViewSelection } from '../selection';
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css` const styles = css`
affine-data-view-kanban-cell { affine-data-view-kanban-cell {
@@ -62,10 +62,7 @@ export class KanbanCell extends SignalWatcher(
private readonly _cell = signal<DataViewCellLifeCycle>(); private readonly _cell = signal<DataViewCellLifeCycle>();
selectCurrentCell = (editing: boolean) => { selectCurrentCell = (editing: boolean) => {
const selectionView = this.closest( const selectionView = this.kanbanViewLogic.selectionController;
'affine-data-view-kanban'
)?.selectionController;
if (!selectionView) return;
if (selectionView) { if (selectionView) {
const selection = selectionView.selection; const selection = selectionView.selection;
if (selection && this.isSelected(selection) && editing) { if (selection && this.isSelected(selection) && editing) {
@@ -93,7 +90,7 @@ export class KanbanCell extends SignalWatcher(
} }
get selection() { get selection() {
return this.closest('affine-data-view-kanban')?.selectionController; return this.kanbanViewLogic.selectionController;
} }
override connectedCallback() { override connectedCallback() {
@@ -103,9 +100,7 @@ export class KanbanCell extends SignalWatcher(
return; return;
} }
e.stopPropagation(); e.stopPropagation();
const selectionElement = this.closest( const selectionElement = this.kanbanViewLogic.selectionController;
'affine-data-view-kanban'
)?.selectionController;
if (!selectionElement) return; if (!selectionElement) return;
if (e.shiftKey) return; if (e.shiftKey) return;
@@ -138,7 +133,7 @@ export class KanbanCell extends SignalWatcher(
const { view } = renderer; const { view } = renderer;
this.view.lockRows(this.isEditing$.value); this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.isEditing$.value}`; this.dataset['editing'] = `${this.isEditing$.value}`;
this.style.border = this.isFocus this.style.border = this.isFocus$.value
? '1px solid var(--affine-primary-color)' ? '1px solid var(--affine-primary-color)'
: ''; : '';
this.style.boxShadow = this.isEditing$.value this.style.boxShadow = this.isEditing$.value
@@ -173,11 +168,14 @@ export class KanbanCell extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor groupKey!: string; accessor groupKey!: string;
@state() isFocus$ = signal(false);
accessor isFocus = false;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: KanbanSingleView; accessor kanbanViewLogic!: KanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
} }
declare global { declare global {
@@ -2,7 +2,7 @@ import type { UIEventStateContext } from '@blocksuite/std';
import type { ReactiveController } from 'lit'; import type { ReactiveController } from 'lit';
import type { KanbanViewSelectionWithType } from '../../selection'; import type { KanbanViewSelectionWithType } from '../../selection';
import type { DataViewKanban } from '../kanban-view.js'; import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanClipboardController implements ReactiveController { export class KanbanClipboardController implements ReactiveController {
private readonly _onCopy = ( private readonly _onCopy = (
@@ -19,31 +19,35 @@ export class KanbanClipboardController implements ReactiveController {
}; };
private get readonly() { private get readonly() {
return this.host.props.view.readonly$.value; return this.logic.view.readonly$.value;
} }
constructor(public host: DataViewKanban) { get host() {
host.addController(this); return this.logic.ui$.value;
} }
constructor(public logic: KanbanViewUILogic) {}
hostConnected() { hostConnected() {
this.host.disposables.add( if (this.host) {
this.host.props.handleEvent('copy', ctx => { this.host.disposables.add(
const kanbanSelection = this.host.selectionController.selection; this.logic.handleEvent('copy', ctx => {
if (!kanbanSelection) return false; const kanbanSelection = this.logic.selectionController.selection;
if (!kanbanSelection) return false;
this._onCopy(ctx, kanbanSelection); this._onCopy(ctx, kanbanSelection);
return true; return true;
}) })
); );
this.host.disposables.add( this.host.disposables.add(
this.host.props.handleEvent('paste', ctx => { this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false; if (this.readonly) return false;
this._onPaste(ctx); this._onPaste(ctx);
return true; return true;
}) })
); );
}
} }
} }
@@ -7,10 +7,14 @@ import { autoScrollOnBoundary } from '../../../../core/utils/auto-scroll.js';
import { startDrag } from '../../../../core/utils/drag.js'; import { startDrag } from '../../../../core/utils/drag.js';
import { KanbanCard } from '../card.js'; import { KanbanCard } from '../card.js';
import { KanbanGroup } from '../group.js'; import { KanbanGroup } from '../group.js';
import type { DataViewKanban } from '../kanban-view.js'; import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanDragController implements ReactiveController { export class KanbanDragController implements ReactiveController {
dragStart = (ele: KanbanCard, evt: PointerEvent) => { dragStart = (ele: KanbanCard, evt: PointerEvent) => {
const host = this.host;
if (!host) {
return;
}
const eleRect = ele.getBoundingClientRect(); const eleRect = ele.getBoundingClientRect();
const offsetLeft = evt.x - eleRect.left; const offsetLeft = evt.x - eleRect.left;
const offsetTop = evt.y - eleRect.top; const offsetTop = evt.y - eleRect.top;
@@ -36,8 +40,8 @@ export class KanbanDragController implements ReactiveController {
return; return;
} }
preview.display(evt.x - offsetLeft, evt.y - offsetTop); preview.display(evt.x - offsetLeft, evt.y - offsetTop);
if (!Rect.fromDOM(this.host).isPointIn(Point.from(evt))) { if (!Rect.fromDOM(host).isPointIn(Point.from(evt))) {
const callback = this.host.props.onDrag; const callback = this.logic.root.config.onDrag;
if (callback) { if (callback) {
this.dropPreview.remove(); this.dropPreview.remove();
return { return {
@@ -47,7 +51,7 @@ export class KanbanDragController implements ReactiveController {
} }
return; return;
} }
const result = this.shooIndicator(evt, ele); const result = this.showIndicator(evt, ele);
if (result) { if (result) {
return { return {
type: 'self', type: 'self',
@@ -80,19 +84,26 @@ export class KanbanDragController implements ReactiveController {
} }
}, },
}); });
const cancelScroll = autoScrollOnBoundary( const cancelScroll =
this.scrollContainer, this.scrollContainer != null
computed(() => { ? autoScrollOnBoundary(
return { this.scrollContainer,
left: drag.mousePosition.value.x, computed(() => {
right: drag.mousePosition.value.x, return {
top: drag.mousePosition.value.y, left: drag.mousePosition.value.x,
bottom: drag.mousePosition.value.y, right: drag.mousePosition.value.x,
}; top: drag.mousePosition.value.y,
}) bottom: drag.mousePosition.value.y,
); };
})
)
: () => {};
}; };
get host() {
return this.logic.ui$.value;
}
dropPreview = createDropPreview(); dropPreview = createDropPreview();
getInsertPosition = ( getInsertPosition = (
@@ -119,7 +130,7 @@ export class KanbanDragController implements ReactiveController {
} }
}; };
shooIndicator = ( showIndicator = (
evt: MouseEvent, evt: MouseEvent,
self: KanbanCard | undefined self: KanbanCard | undefined
): { group: KanbanGroup; position: InsertToPosition } | undefined => { ): { group: KanbanGroup; position: InsertToPosition } | undefined => {
@@ -133,38 +144,36 @@ export class KanbanDragController implements ReactiveController {
}; };
get scrollContainer() { get scrollContainer() {
const scrollContainer = this.host.querySelector( const scrollContainer = this.logic.scrollContainer$.value;
'.affine-data-view-kanban-groups'
) as HTMLElement;
return scrollContainer; return scrollContainer;
} }
constructor(private readonly host: DataViewKanban) { constructor(private readonly logic: KanbanViewUILogic) {}
this.host.addController(this);
}
hostConnected() { hostConnected() {
if (this.host.props.view.readonly$.value) { if (this.logic.view.readonly$.value) {
return; return;
} }
this.host.disposables.add( if (this.host) {
this.host.props.handleEvent('dragStart', context => { this.host.disposables.add(
const event = context.get('pointerState').raw; this.logic.handleEvent('dragStart', context => {
const target = event.target; const event = context.get('pointerState').raw;
if (target instanceof Element) { const target = event.target;
const cell = target.closest('affine-data-view-kanban-cell'); if (target instanceof Element) {
if (cell?.isEditing$.value) { const cell = target.closest('affine-data-view-kanban-cell');
return; if (cell?.isEditing$.value) {
return;
}
cell?.selectCurrentCell(false);
const card = target.closest('affine-data-view-kanban-card');
if (card) {
this.dragStart(card, event);
}
} }
cell?.selectCurrentCell(false); return true;
const card = target.closest('affine-data-view-kanban-card'); })
if (card) { );
this.dragStart(card, event); }
}
}
return true;
})
);
} }
} }
@@ -174,8 +183,8 @@ const createDragPreview = (card: KanbanCard, x: number, y: number) => {
const div = document.createElement('div'); const div = document.createElement('div');
const kanbanCard = new KanbanCard(); const kanbanCard = new KanbanCard();
kanbanCard.cardId = card.cardId; kanbanCard.cardId = card.cardId;
kanbanCard.view = card.view; kanbanCard.kanbanViewLogic = card.kanbanViewLogic;
kanbanCard.isFocus = true; kanbanCard.isFocus$.value = true;
kanbanCard.style.backgroundColor = 'var(--affine-background-primary-color)'; kanbanCard.style.backgroundColor = 'var(--affine-background-primary-color)';
div.append(kanbanCard); div.append(kanbanCard);
div.className = 'with-data-view-css-variable'; div.className = 'with-data-view-css-variable';
@@ -1,63 +1,67 @@
import type { ReactiveController } from 'lit'; import type { ReactiveController } from 'lit';
import type { DataViewKanban } from '../kanban-view.js'; import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanHotkeysController implements ReactiveController { export class KanbanHotkeysController implements ReactiveController {
private get hasSelection() { private get hasSelection() {
return !!this.host.selectionController.selection; return !!this.logic.selectionController.selection;
} }
constructor(private readonly host: DataViewKanban) { constructor(public logic: KanbanViewUILogic) {}
this.host.addController(this);
get host() {
return this.logic.ui$.value;
} }
hostConnected() { hostConnected() {
this.host.disposables.add( if (this.host) {
this.host.props.bindHotkey({ this.host.disposables.add(
Escape: () => { this.logic.bindHotkey({
this.host.selectionController.focusOut(); Escape: () => {
return true; this.logic.selectionController.focusOut();
}, return true;
Enter: () => { },
this.host.selectionController.focusIn(); Enter: () => {
}, this.logic.selectionController.focusIn();
ArrowUp: context => { },
if (!this.hasSelection) return false; ArrowUp: context => {
if (!this.hasSelection) return false;
this.host.selectionController.focusNext('up'); this.logic.selectionController.focusNext('up');
context.get('keyboardState').raw.preventDefault(); context.get('keyboardState').raw.preventDefault();
return true; return true;
}, },
ArrowDown: context => { ArrowDown: context => {
if (!this.hasSelection) return false; if (!this.hasSelection) return false;
this.host.selectionController.focusNext('down'); this.logic.selectionController.focusNext('down');
context.get('keyboardState').raw.preventDefault(); context.get('keyboardState').raw.preventDefault();
return true; return true;
}, },
Tab: context => { Tab: context => {
if (!this.hasSelection) return false; if (!this.hasSelection) return false;
this.host.selectionController.focusNext('down'); this.logic.selectionController.focusNext('down');
context.get('keyboardState').raw.preventDefault(); context.get('keyboardState').raw.preventDefault();
return true; return true;
}, },
ArrowLeft: () => { ArrowLeft: () => {
if (!this.hasSelection) return false; if (!this.hasSelection) return false;
this.host.selectionController.focusNext('left'); this.logic.selectionController.focusNext('left');
return true; return true;
}, },
ArrowRight: () => { ArrowRight: () => {
if (!this.hasSelection) return false; if (!this.hasSelection) return false;
this.host.selectionController.focusNext('right'); this.logic.selectionController.focusNext('right');
return true; return true;
}, },
Backspace: () => { Backspace: () => {
this.host.selectionController.deleteCard(); this.logic.selectionController.deleteCard();
}, },
}) })
); );
}
} }
} }
@@ -12,7 +12,7 @@ import type {
import { KanbanCard } from '../card.js'; import { KanbanCard } from '../card.js';
import { KanbanCell } from '../cell.js'; import { KanbanCell } from '../cell.js';
import type { KanbanGroup } from '../group.js'; import type { KanbanGroup } from '../group.js';
import type { DataViewKanban } from '../kanban-view.js'; import type { KanbanViewUILogic } from '../kanban-view-ui-logic.js';
export class KanbanSelectionController implements ReactiveController { export class KanbanSelectionController implements ReactiveController {
private _selection?: KanbanViewSelectionWithType; private _selection?: KanbanViewSelectionWithType;
@@ -47,52 +47,62 @@ export class KanbanSelectionController implements ReactiveController {
} }
set selection(data: KanbanViewSelection | undefined) { set selection(data: KanbanViewSelection | undefined) {
const host = this.host;
if (!host) {
return;
}
if (!data) { if (!data) {
this.host.props.setSelection(); this.logic.setSelection();
return; return;
} }
const selection: KanbanViewSelectionWithType = { const selection: KanbanViewSelectionWithType = {
...data, ...data,
viewId: this.host.props.view.id, viewId: this.logic.view.id,
type: 'kanban', type: 'kanban',
}; };
if (selection.selectionType === 'cell' && selection.isEditing) { if (selection.selectionType === 'cell' && selection.isEditing) {
const container = getFocusCell(this.host, selection); const container = getFocusCell(host, selection);
const cell = container?.cell; const cell = container?.cell;
const isEditing = cell const isEditing = cell
? cell.beforeEnterEditMode() ? cell.beforeEnterEditMode()
? selection.isEditing ? selection.isEditing
: false : false
: false; : false;
this.host.props.setSelection({ this.logic.setSelection({
...selection, ...selection,
isEditing, isEditing,
}); });
} else { } else {
this.host.props.setSelection(selection); this.logic.setSelection(selection);
} }
} }
get view() { get view() {
return this.host.props.view; return this.logic.view;
} }
constructor(private readonly host: DataViewKanban) { get host() {
this.host.addController(this); return this.logic.ui$.value;
} }
constructor(public logic: KanbanViewUILogic) {}
blur(selection: KanbanViewSelection) { blur(selection: KanbanViewSelection) {
const host = this.host;
if (!host) {
return;
}
if (selection.selectionType !== 'cell') { if (selection.selectionType !== 'cell') {
const selectCards = getSelectedCards(this.host, selection); const selectCards = getSelectedCards(this.host, selection);
selectCards.forEach(card => (card.isFocus = false)); selectCards.forEach(card => (card.isFocus$.value = false));
return; return;
} }
const container = getFocusCell(this.host, selection); const container = getFocusCell(this.host, selection);
if (!container) { if (!container) {
return; return;
} }
container.isFocus = false; container.isFocus$.value = false;
const cell = container?.cell; const cell = container?.cell;
if (selection.isEditing) { if (selection.isEditing) {
@@ -116,19 +126,23 @@ export class KanbanSelectionController implements ReactiveController {
return; return;
} }
if (selection.selectionType === 'card') { if (selection.selectionType === 'card') {
this.host.props.view.rowsDelete(selection.cards.map(v => v.cardId)); this.view.rowsDelete(selection.cards.map(v => v.cardId));
this.selection = undefined; this.selection = undefined;
} }
} }
focus(selection: KanbanViewSelection) { focus(selection: KanbanViewSelection) {
const host = this.host;
if (!host) {
return;
}
if (selection.selectionType !== 'cell') { if (selection.selectionType !== 'cell') {
const selectCards = getSelectedCards(this.host, selection); const selectCards = getSelectedCards(this.host, selection);
selectCards.forEach((card, index) => { selectCards.forEach((card, index) => {
if (index === 0) { if (index === 0) {
card.scrollIntoView({ block: 'nearest', inline: 'nearest' }); card.scrollIntoView({ block: 'nearest', inline: 'nearest' });
} }
card.isFocus = true; card.isFocus$.value = true;
}); });
return; return;
} }
@@ -137,7 +151,7 @@ export class KanbanSelectionController implements ReactiveController {
return; return;
} }
container.scrollIntoView({ block: 'nearest', inline: 'nearest' }); container.scrollIntoView({ block: 'nearest', inline: 'nearest' });
container.isFocus = true; container.isFocus$.value = true;
const cell = container?.cell; const cell = container?.cell;
if (selection.isEditing) { if (selection.isEditing) {
if (cell?.focusCell()) { if (cell?.focusCell()) {
@@ -153,10 +167,9 @@ export class KanbanSelectionController implements ReactiveController {
} }
focusFirstCell() { focusFirstCell() {
const group = this.host.groupManager?.groupsDataList$.value?.[0]; const group = this.logic.groups$.value?.[0];
const card = group?.rows[0]; const card = group?.rows[0];
const columnId = const columnId = card && this.view.getHeaderTitle(card.rowId)?.id;
card && this.host.props.view.getHeaderTitle(card.rowId)?.id;
if (group && card && columnId) { if (group && card && columnId) {
this.selection = { this.selection = {
selectionType: 'cell', selectionType: 'cell',
@@ -169,6 +182,10 @@ export class KanbanSelectionController implements ReactiveController {
} }
focusIn() { focusIn() {
const host = this.host;
if (!host) {
return;
}
const selection = this.selection; const selection = this.selection;
if (!selection) return; if (!selection) return;
if (selection.selectionType === 'cell' && selection.isEditing) return; if (selection.selectionType === 'cell' && selection.isEditing) return;
@@ -198,6 +215,10 @@ export class KanbanSelectionController implements ReactiveController {
} }
focusNext(position: 'up' | 'down' | 'left' | 'right') { focusNext(position: 'up' | 'down' | 'left' | 'right') {
const host = this.host;
if (!host) {
return;
}
const selection = this.selection; const selection = this.selection;
if (!selection) { if (!selection) {
return; return;
@@ -222,7 +243,7 @@ export class KanbanSelectionController implements ReactiveController {
} }
} else if (selection.selectionType === 'card') { } else if (selection.selectionType === 'card') {
// card focus // card focus
const group = this.host.querySelector( const group = this.host?.querySelector(
`affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]` `affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]`
); );
const cardElements = Array.from( const cardElements = Array.from(
@@ -292,7 +313,11 @@ export class KanbanSelectionController implements ReactiveController {
cards: KanbanCardSelectionCard[]; cards: KanbanCardSelectionCard[];
} }
| undefined { | undefined {
const group = this.host.querySelector( const host = this.host;
if (!host) {
return;
}
const group = host.querySelector(
`affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]` `affine-data-view-kanban-group[data-key="${selection.cards[0].groupKey}"]`
); );
const kanbanCards = Array.from( const kanbanCards = Array.from(
@@ -332,7 +357,7 @@ export class KanbanSelectionController implements ReactiveController {
} }
const groups = Array.from( const groups = Array.from(
this.host.querySelectorAll('affine-data-view-kanban-group') this.host?.querySelectorAll('affine-data-view-kanban-group') ?? []
); );
if (nextPosition === 'right') { if (nextPosition === 'right') {
@@ -369,6 +394,10 @@ export class KanbanSelectionController implements ReactiveController {
groupKey?: string; groupKey?: string;
} }
| undefined { | undefined {
const host = this.host;
if (!host) {
return;
}
const kanbanCells = getCardCellsBySelection(this.host, selection); const kanbanCells = getCardCellsBySelection(this.host, selection);
const group = this.host.querySelector( const group = this.host.querySelector(
`affine-data-view-kanban-group[data-key="${selection.groupKey}"]` `affine-data-view-kanban-group[data-key="${selection.groupKey}"]`
@@ -426,7 +455,7 @@ export class KanbanSelectionController implements ReactiveController {
} }
const groups = Array.from( const groups = Array.from(
this.host.querySelectorAll('affine-data-view-kanban-group') this.host?.querySelectorAll('affine-data-view-kanban-group') ?? []
); );
if (nextPosition === 'right') { if (nextPosition === 'right') {
@@ -453,8 +482,8 @@ export class KanbanSelectionController implements ReactiveController {
} }
hostConnected() { hostConnected() {
this.host.disposables.add( this.host?.disposables.add(
this.host.props.selection$.subscribe(selection => { this.logic.selection$.subscribe(selection => {
const old = this._selection; const old = this._selection;
if (old) { if (old) {
this.blur(old); this.blur(old);
@@ -0,0 +1,11 @@
import { KanbanCard } from './card.js';
import { KanbanCell } from './cell.js';
import { KanbanGroup } from './group.js';
import { KanbanHeader } from './header.js';
export function pcEffects() {
customElements.define('affine-data-view-kanban-card', KanbanCard);
customElements.define('affine-data-view-kanban-cell', KanbanCell);
customElements.define('affine-data-view-kanban-group', KanbanGroup);
customElements.define('affine-data-view-kanban-header', KanbanHeader);
}
@@ -11,11 +11,10 @@ import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js'; import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js'; import type { Group } from '../../../core/group-by/trait.js';
import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js'; import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js';
import type { KanbanSingleView } from '../kanban-view-manager.js'; import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
const styles = css` const styles = css`
affine-data-view-kanban-group { affine-data-view-kanban-group {
@@ -99,40 +98,34 @@ export class KanbanGroup extends SignalWatcher(
private readonly clickAddCard = () => { private readonly clickAddCard = () => {
const id = this.view.addCard('end', this.group.key); const id = this.view.addCard('end', this.group.key);
requestAnimationFrame(() => { requestAnimationFrame(() => {
const kanban = this.closest('affine-data-view-kanban'); const columnId =
if (kanban) { this.view.mainProperties$.value.titleColumn ||
const columnId = this.view.propertyIds$.value[0];
this.view.mainProperties$.value.titleColumn || if (!columnId) return;
this.view.propertyIds$.value[0]; this.kanbanViewLogic.selectionController.selection = {
if (!columnId) return; selectionType: 'cell',
kanban.selectionController.selection = { groupKey: this.group.key,
selectionType: 'cell', cardId: id,
groupKey: this.group.key, columnId,
cardId: id, isEditing: true,
columnId, };
isEditing: true,
};
}
}); });
}; };
private readonly clickAddCardInStart = () => { private readonly clickAddCardInStart = () => {
const id = this.view.addCard('start', this.group.key); const id = this.view.addCard('start', this.group.key);
requestAnimationFrame(() => { requestAnimationFrame(() => {
const kanban = this.closest('affine-data-view-kanban'); const columnId =
if (kanban) { this.view.mainProperties$.value.titleColumn ||
const columnId = this.view.propertyIds$.value[0];
this.view.mainProperties$.value.titleColumn || if (!columnId) return;
this.view.propertyIds$.value[0]; this.kanbanViewLogic.selectionController.selection = {
if (!columnId) return; selectionType: 'cell',
kanban.selectionController.selection = { groupKey: this.group.key,
selectionType: 'cell', cardId: id,
groupKey: this.group.key, columnId,
cardId: id, isEditing: true,
columnId, };
isEditing: true,
};
}
}); });
}; };
@@ -176,8 +169,7 @@ export class KanbanGroup extends SignalWatcher(
<affine-data-view-kanban-card <affine-data-view-kanban-card
data-card-id="${row.rowId}" data-card-id="${row.rowId}"
.groupKey="${this.group.key}" .groupKey="${this.group.key}"
.dataViewEle="${this.dataViewEle}" .kanbanViewLogic="${this.kanbanViewLogic}"
.view="${this.view}"
.cardId="${row.rowId}" .cardId="${row.rowId}"
></affine-data-view-kanban-card> ></affine-data-view-kanban-card>
`; `;
@@ -197,14 +189,15 @@ export class KanbanGroup extends SignalWatcher(
`; `;
} }
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false }) @property({ attribute: false })
accessor group!: Group; accessor group!: Group;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: KanbanSingleView; accessor kanbanViewLogic!: KanbanViewUILogic;
get view() {
return this.kanbanViewLogic.view;
}
} }
declare global { declare global {
@@ -0,0 +1,330 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { css } from '@emotion/css';
import { computed, signal } from '@preact/signals-core';
import { type TemplateResult } from 'lit';
import { ref } from 'lit/directives/ref.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import {
type GroupTrait,
groupTraitKey,
} from '../../../core/group-by/trait.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
import {
createSortContext,
sortable,
} from '../../../core/utils/wc-dnd/sort/sort-context.js';
import { horizontalListSortingStrategy } from '../../../core/utils/wc-dnd/sort/strategies/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection.js';
import { KanbanClipboardController } from './controller/clipboard.js';
import { KanbanDragController } from './controller/drag.js';
import { KanbanHotkeysController } from './controller/hotkeys.js';
import { KanbanSelectionController } from './controller/selection.js';
export class KanbanViewUILogic extends DataViewUILogicBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
ui$ = signal<KanbanViewUI | undefined>();
clipboardController = new KanbanClipboardController(this);
dragController = new KanbanDragController(this);
hotkeysController = new KanbanHotkeysController(this);
selectionController = new KanbanSelectionController(this);
groupTrait$ = computed(() => {
return this.view.traitGet(groupTraitKey);
});
groups$ = computed(() => {
const groupTrait = this.groupTrait$.value;
return groupTrait?.groupsDataList$.value || [];
});
private get readonly() {
return this.view.readonly$.value;
}
clearSelection = () => {
this.selectionController.clear();
};
addRow = (position: InsertToPosition) => {
if (this.readonly) return;
const rowId = this.view.rowAdd(position);
if (rowId) {
this.root.openDetailPanel({
view: this.view,
rowId,
});
}
return rowId;
};
focusFirstCell = () => {
this.selectionController.focusFirstCell();
};
showIndicator = (evt: MouseEvent) => {
return this.dragController.showIndicator(evt, undefined) != null;
};
hideIndicator = () => {
this.dragController.dropPreview.remove();
};
moveTo = (id: string, evt: MouseEvent) => {
const position = this.dragController.getInsertPosition(evt);
if (position) {
position.group.group.manager.moveCardTo(
id,
'',
position.group.group.key,
position.position
);
}
};
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
return;
}
const ele = event.currentTarget;
if (ele instanceof HTMLElement) {
if (ele.scrollWidth === ele.clientWidth) {
return;
}
event.stopPropagation();
}
};
renderAddGroup = (groupHelper: GroupTrait) => {
const addGroup = groupHelper.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = groupHelper.property$.value;
if (column) {
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
}),
],
},
});
};
return html` <div
style="height: 32px;flex-shrink:0;display:flex;align-items:center;"
@click="${add}"
>
<div class="${addGroupIconStyle}">${AddCursorIcon()}</div>
</div>`;
};
scrollContainer$ = signal<HTMLElement | undefined>(undefined);
renderer = createUniComponentFromWebComponent(KanbanViewUI);
}
export class KanbanViewUI extends DataViewUIBase<KanbanViewUILogic> {
readonly sortContext = createSortContext({
activators: defaultActivators,
container: this,
onDragEnd: evt => {
const over = evt.over;
const activeId = evt.active.id;
const groupTrait = this.logic.groupTrait$.value;
const groups = groupTrait?.groupsDataList$.value;
if (over && over.id !== activeId && groups) {
const activeIndex = groups.findIndex(data => data?.key === activeId);
const overIndex = groups.findIndex(data => data?.key === over.id);
groupTrait?.moveGroupTo(
activeId,
activeIndex > overIndex
? {
before: true,
id: over.id,
}
: {
before: false,
id: over.id,
}
);
}
},
modifiers: [
({ transform }) => {
return {
...transform,
y: 0,
};
},
],
items: computed(() => {
return this.logic.groups$.value?.map(v => v?.key ?? 'default key') ?? [];
}),
strategy: horizontalListSortingStrategy,
});
private renderGroups() {
const groups = this.logic.groups$.value;
if (!groups) {
return html``;
}
return html`${groups.map(group => {
return html` <affine-data-view-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.kanbanViewLogic="${this.logic}"
.group="${group}"
></affine-data-view-kanban-group>`;
})}`;
}
override connectedCallback(): void {
super.connectedCallback();
this.logic.ui$.value = this;
this.logic.clipboardController.hostConnected();
this.logic.dragController.hostConnected();
this.logic.hotkeysController.hostConnected();
this.logic.selectionController.hostConnected();
this.classList.add('kanban-view', kanbanViewStyle);
this.style.userSelect = 'none';
this.style.display = 'flex';
this.style.flexDirection = 'column';
}
override render(): TemplateResult {
const groups = this.logic.groups$.value;
if (!groups) {
return html``;
}
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
const groupTrait = this.logic.groupTrait$.value;
return html`
${renderUniLit(this.logic.root.config.headerWidget, {
dataViewLogic: this.logic,
})}
<div
${ref(this.logic.scrollContainer$)}
class="${kanbanGroupsStyle}"
style="${wrapperStyle}"
@wheel="${this.logic.onWheel}"
>
${this.renderGroups()}
${groupTrait ? this.logic.renderAddGroup(groupTrait) : ''}
</div>
`;
}
}
const kanbanViewStyle = css({
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
});
const kanbanGroupsStyle = css({
position: 'relative',
zIndex: 1,
display: 'flex',
gap: '20px',
paddingBottom: '4px',
overflowX: 'scroll',
overflowY: 'hidden',
'&:hover': {
paddingBottom: '0px',
},
'&::-webkit-scrollbar': {
WebkitAppearance: 'none',
display: 'block',
},
'&::-webkit-scrollbar:horizontal': {
height: '4px',
},
'&::-webkit-scrollbar-thumb': {
borderRadius: '2px',
backgroundColor: 'transparent',
},
'&:hover::-webkit-scrollbar:horizontal': {
height: '8px',
},
'&:hover::-webkit-scrollbar-thumb': {
borderRadius: '16px',
backgroundColor: 'var(--affine-black-30)',
},
'&:hover::-webkit-scrollbar-track': {
backgroundColor: 'var(--affine-hover-color)',
},
});
const addGroupIconStyle = css({
padding: '4px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'var(--affine-hover-color)',
},
'& svg': {
width: '16px',
height: '16px',
fill: 'var(--affine-icon-color)',
color: 'var(--affine-icon-color)',
},
});
declare global {
interface HTMLElementTagNameMap {
'dv-kanban-view-ui': KanbanViewUI;
}
}
@@ -1,300 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { computed } from '@preact/signals-core';
import { css } from 'lit';
import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
import {
createSortContext,
sortable,
} from '../../../core/utils/wc-dnd/sort/sort-context.js';
import { horizontalListSortingStrategy } from '../../../core/utils/wc-dnd/sort/strategies/index.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import type { KanbanSingleView } from '../kanban-view-manager.js';
import type { KanbanViewSelectionWithType } from '../selection';
import { KanbanClipboardController } from './controller/clipboard.js';
import { KanbanDragController } from './controller/drag.js';
import { KanbanHotkeysController } from './controller/hotkeys.js';
import { KanbanSelectionController } from './controller/selection.js';
const styles = css`
affine-data-view-kanban {
user-select: none;
display: flex;
flex-direction: column;
}
.affine-data-view-kanban-groups {
position: relative;
z-index: 1;
display: flex;
gap: 20px;
padding-bottom: 4px;
overflow-x: scroll;
overflow-y: hidden;
}
.affine-data-view-kanban-groups:hover {
padding-bottom: 0px;
}
.affine-data-view-kanban-groups::-webkit-scrollbar {
-webkit-appearance: none;
display: block;
}
.affine-data-view-kanban-groups::-webkit-scrollbar:horizontal {
height: 4px;
}
.affine-data-view-kanban-groups::-webkit-scrollbar-thumb {
border-radius: 2px;
background-color: transparent;
}
.affine-data-view-kanban-groups:hover::-webkit-scrollbar:horizontal {
height: 8px;
}
.affine-data-view-kanban-groups:hover::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: var(--affine-black-30);
}
.affine-data-view-kanban-groups:hover::-webkit-scrollbar-track {
background-color: var(--affine-hover-color);
}
.add-group-icon {
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
cursor: pointer;
}
.add-group-icon:hover {
background-color: var(--affine-hover-color);
}
.add-group-icon svg {
width: 16px;
height: 16px;
fill: var(--affine-icon-color);
color: var(--affine-icon-color);
}
`;
export class DataViewKanban extends DataViewBase<
KanbanSingleView,
KanbanViewSelectionWithType
> {
static override styles = styles;
private readonly dragController = new KanbanDragController(this);
clipboardController = new KanbanClipboardController(this);
hotkeysController = new KanbanHotkeysController(this);
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
return;
}
const ele = event.currentTarget;
if (ele instanceof HTMLElement) {
if (ele.scrollWidth === ele.clientWidth) {
return;
}
event.stopPropagation();
}
};
renderAddGroup = () => {
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = this.groupManager.property$.value;
if (column) {
column.dataUpdate(
() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div
style="height: 32px;flex-shrink:0;display:flex;align-items:center;"
@click="${add}"
>
<div class="add-group-icon">${AddCursorIcon()}</div>
</div>`;
};
selectionController = new KanbanSelectionController(this);
sortContext = createSortContext({
activators: defaultActivators,
container: this,
onDragEnd: evt => {
const over = evt.over;
const activeId = evt.active.id;
const groups = this.groupManager.groupsDataList$.value;
if (over && over.id !== activeId && groups) {
const activeIndex = groups.findIndex(data => data?.key === activeId);
const overIndex = groups.findIndex(data => data?.key === over.id);
this.groupManager.moveGroupTo(
activeId,
activeIndex > overIndex
? {
before: true,
id: over.id,
}
: {
before: false,
id: over.id,
}
);
}
},
modifiers: [
({ transform }) => {
return {
...transform,
y: 0,
};
},
],
items: computed(() => {
return (
this.groupManager.groupsDataList$.value?.map(
v => v?.key ?? 'default key'
) ?? []
);
}),
strategy: horizontalListSortingStrategy,
});
get expose(): DataViewInstance {
return {
clearSelection: () => {
this.selectionController.clear();
},
addRow: position => {
if (this.props.view.readonly$.value) return;
const rowId = this.props.view.rowAdd(position);
if (rowId) {
this.props.dataViewEle.openDetailPanel({
view: this.props.view,
rowId,
});
}
return rowId;
},
focusFirstCell: () => {
this.selectionController.focusFirstCell();
},
getSelection: () => {
return this.selectionController.selection;
},
hideIndicator: () => {
this.dragController.dropPreview.remove();
},
moveTo: (id, evt) => {
const position = this.dragController.getInsertPosition(evt);
if (position) {
position.group.group.manager.moveCardTo(
id,
'',
position.group.group.key,
position.position
);
}
},
showIndicator: evt => {
return this.dragController.shooIndicator(evt, undefined) != null;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
get groupManager() {
return this.props.view.groupTrait;
}
override render() {
const groups = this.groupManager.groupsDataList$.value;
if (!groups) {
return html``;
}
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div
class="affine-data-view-kanban-groups"
style="${wrapperStyle}"
@wheel="${this.onWheel}"
>
${repeat(
groups,
group => group?.key ?? 'default key',
group => {
if (!group) return;
return html` <affine-data-view-kanban-group
${sortable(group.key)}
data-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.group="${group}"
></affine-data-view-kanban-group>`;
}
)}
${this.renderAddGroup()}
</div>
`;
}
@query('.affine-data-view-kanban-groups')
accessor groups!: HTMLElement;
}
declare global {
interface HTMLElementTagNameMap {
'affine-data-view-kanban': DataViewKanban;
}
}
@@ -12,17 +12,17 @@ import {
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { html } from 'lit'; import { html } from 'lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { KanbanSelectionController } from './controller/selection.js'; import type { KanbanSelectionController } from './controller/selection.js';
import type { KanbanViewUILogic } from './kanban-view-ui-logic.js';
export const openDetail = ( export const openDetail = (
dataViewEle: DataViewRenderer, kanbanViewLogic: KanbanViewUILogic,
rowId: string, rowId: string,
selection: KanbanSelectionController selection: KanbanSelectionController
) => { ) => {
const old = selection.selection; const old = selection.selection;
selection.selection = undefined; selection.selection = undefined;
dataViewEle.openDetailPanel({ kanbanViewLogic.root.openDetailPanel({
view: selection.view, view: selection.view,
rowId: rowId, rowId: rowId,
onClose: () => { onClose: () => {
@@ -32,7 +32,7 @@ export const openDetail = (
}; };
export const popCardMenu = ( export const popCardMenu = (
dataViewEle: DataViewRenderer, kanbanViewLogic: KanbanViewUILogic,
ele: PopupTarget, ele: PopupTarget,
rowId: string, rowId: string,
selection: KanbanSelectionController selection: KanbanSelectionController
@@ -42,7 +42,7 @@ export const popCardMenu = (
name: 'Expand Card', name: 'Expand Card',
prefix: ExpandFullIcon(), prefix: ExpandFullIcon(),
select: () => { select: () => {
openDetail(dataViewEle, rowId, selection); openDetail(kanbanViewLogic, rowId, selection);
}, },
}), }),
menu.subMenu({ menu.subMenu({
@@ -1,11 +1,12 @@
import { createUniComponentFromWebComponent } from '../../core/index.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import { kanbanViewModel } from './define.js'; import { kanbanViewModel } from './define.js';
import { MobileDataViewKanban } from './mobile/kanban-view.js'; import { MobileKanbanViewUILogic } from './mobile/kanban-view-ui-logic.js';
import { DataViewKanban } from './pc/kanban-view.js'; import { KanbanViewUILogic } from './pc/kanban-view-ui-logic.js';
export const kanbanViewMeta = kanbanViewModel.createMeta({ export const kanbanViewMeta = kanbanViewModel.createMeta({
icon: createIcon('DatabaseKanbanViewIcon'), icon: createIcon('DatabaseKanbanViewIcon'),
view: createUniComponentFromWebComponent(DataViewKanban), // @ts-expect-error fixme: typesafe
mobileView: createUniComponentFromWebComponent(MobileDataViewKanban), pcLogic: () => KanbanViewUILogic,
// @ts-expect-error fixme: typesafe
mobileLogic: () => MobileKanbanViewUILogic,
}); });
@@ -0,0 +1,11 @@
import { mobileEffects } from './mobile/effect.js';
import { pcEffects } from './pc/effect.js';
import { pcVirtualEffects } from './pc-virtual/effect.js';
import { statsEffects } from './stats/effect.js';
export function tableEffects() {
mobileEffects();
statsEffects();
pcEffects();
pcVirtualEffects();
}
@@ -1,8 +1,6 @@
export * from './define.js'; export * from './define.js';
export * from './pc/effect.js'; export * from './pc/effect.js';
export * from './pc/table-view.js';
export * from './pc-virtual/effect.js'; export * from './pc-virtual/effect.js';
export * from './renderer.js'; export * from './renderer.js';
export * from './selection.js'; export * from './selection.js';
export * from './table-view-manager.js'; export * from './table-view-manager.js';
export * from './table-view-selector.js';
@@ -8,10 +8,10 @@ import {
type CellRenderProps, type CellRenderProps,
type DataViewCellLifeCycle, type DataViewCellLifeCycle,
renderUniLit, renderUniLit,
type SingleView,
} from '../../../core/index.js'; } from '../../../core/index.js';
import { TableViewAreaSelection } from '../selection'; import { TableViewAreaSelection } from '../selection';
import type { TableProperty } from '../table-view-manager.js'; import type { TableProperty } from '../table-view-manager.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export class MobileTableCell extends SignalWatcher( export class MobileTableCell extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
@@ -48,7 +48,7 @@ export class MobileTableCell extends SignalWatcher(
}); });
isSelectionEditing$ = computed(() => { isSelectionEditing$ = computed(() => {
const selection = this.table?.props.selection$.value; const selection = this.tableViewLogic.selection$.value;
if (selection?.selectionType !== 'area') { if (selection?.selectionType !== 'area') {
return false; return false;
} }
@@ -68,8 +68,8 @@ export class MobileTableCell extends SignalWatcher(
if (this.view.readonly$.value) { if (this.view.readonly$.value) {
return; return;
} }
const setSelection = this.table?.props.setSelection; const setSelection = this.tableViewLogic.setSelection;
const viewId = this.table?.props.view.id; const viewId = this.tableViewLogic.view.id;
if (setSelection && viewId) { if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) { if (editing && this.cell?.beforeEnterEditMode() === false) {
return; return;
@@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher(
return this.closest('mobile-table-group')?.group?.key; return this.closest('mobile-table-group')?.group?.key;
} }
private get table() {
return this.closest('mobile-data-view-table');
}
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this.column.readonly$.value) return; if (this.column.readonly$.value) return;
@@ -160,7 +156,11 @@ export class MobileTableCell extends SignalWatcher(
accessor rowIndex!: number; accessor rowIndex!: number;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: SingleView; accessor tableViewLogic!: MobileTableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -0,0 +1,15 @@
import { MobileTableCell } from './cell.js';
import { MobileTableColumnHeader } from './column-header.js';
import { MobileTableGroup } from './group.js';
import { MobileTableHeader } from './header.js';
import { MobileTableRow } from './row.js';
import { MobileTableViewUI } from './table-view-ui-logic.js';
export function mobileEffects() {
customElements.define('mobile-table-cell', MobileTableCell);
customElements.define('mobile-table-group', MobileTableGroup);
customElements.define('mobile-data-view-table-ui', MobileTableViewUI);
customElements.define('mobile-table-header', MobileTableHeader);
customElements.define('mobile-table-column-header', MobileTableColumnHeader);
customElements.define('mobile-table-row', MobileTableRow);
}
@@ -8,17 +8,14 @@ import { PlusIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { css, html, unsafeCSS } from 'lit'; import { css, html, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js'; import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js'; import type { Group } from '../../../core/group-by/trait.js';
import type { Row } from '../../../core/index.js'; import type { Row } from '../../../core/index.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js'; import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { DataViewTable } from '../pc/table-view.js'; import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
import { TableViewAreaSelection } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
const styles = css` const styles = css`
.data-view-table-group-add-row { .data-view-table-group-add-row {
@@ -54,40 +51,10 @@ export class MobileTableGroup extends SignalWatcher(
private readonly clickAddRow = () => { private readonly clickAddRow = () => {
this.view.rowAdd('end', this.group?.key); this.view.rowAdd('end', this.group?.key);
const selectionController = this.viewEle.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
selectionController.selection = TableViewAreaSelection.create({
groupKey: this.group?.key,
focus: {
rowIndex: this.rows.length - 1,
columnIndex: index,
},
isEditing: true,
});
});
}; };
private readonly clickAddRowInStart = () => { private readonly clickAddRowInStart = () => {
this.view.rowAdd('start', this.group?.key); this.view.rowAdd('start', this.group?.key);
const selectionController = this.viewEle.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
selectionController.selection = TableViewAreaSelection.create({
groupKey: this.group?.key,
focus: {
rowIndex: 0,
columnIndex: index,
},
isEditing: true,
});
});
}; };
private readonly clickGroupOptions = (e: MouseEvent) => { private readonly clickGroupOptions = (e: MouseEvent) => {
@@ -150,8 +117,7 @@ export class MobileTableGroup extends SignalWatcher(
return html` <mobile-table-row return html` <mobile-table-row
data-row-index="${idx}" data-row-index="${idx}"
data-row-id="${row.rowId}" data-row-id="${row.rowId}"
.dataViewEle="${this.dataViewEle}" .tableViewLogic="${this.tableViewLogic}"
.view="${this.view}"
.rowId="${row.rowId}" .rowId="${row.rowId}"
.rowIndex="${idx}" .rowIndex="${idx}"
></mobile-table-row>`; ></mobile-table-row>`;
@@ -172,8 +138,6 @@ export class MobileTableGroup extends SignalWatcher(
${PlusIcon()}<span style="font-size: 12px">New Record</span> ${PlusIcon()}<span style="font-size: 12px">New Record</span>
</div> </div>
</div>`} </div>`}
<affine-database-column-stats .view="${this.view}" .group="${this.group}">
</affine-database-column-stats>
`; `;
} }
@@ -181,20 +145,15 @@ export class MobileTableGroup extends SignalWatcher(
return this.renderRows(this.rows); return this.renderRows(this.rows);
} }
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false }) @property({ attribute: false })
accessor group: Group | undefined = undefined; accessor group: Group | undefined = undefined;
@query('.affine-database-block-rows')
accessor rowsContainer: HTMLElement | null = null;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: TableSingleView; accessor tableViewLogic!: MobileTableViewUILogic;
@property({ attribute: false }) get view() {
accessor viewEle!: DataViewTable; return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -8,6 +8,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import { cellDivider } from '../styles.js';
import type { TableSingleView } from '../table-view-manager.js'; import type { TableSingleView } from '../table-view-manager.js';
export class MobileTableHeader extends SignalWatcher( export class MobileTableHeader extends SignalWatcher(
@@ -60,7 +61,7 @@ export class MobileTableHeader extends SignalWatcher(
.column="${column}" .column="${column}"
.tableViewManager="${this.tableViewManager}" .tableViewManager="${this.tableViewManager}"
></mobile-table-column-header> ></mobile-table-column-header>
<div class="cell-divider" style="height: auto;"></div> <div class="${cellDivider}" style="height: auto;"></div>
`; `;
} }
)} )}
@@ -5,13 +5,13 @@ import {
} from '@blocksuite/affine-components/context-menu'; } from '@blocksuite/affine-components/context-menu';
import { DeleteIcon, ExpandFullIcon } from '@blocksuite/icons/lit'; import { DeleteIcon, ExpandFullIcon } from '@blocksuite/icons/lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { SingleView } from '../../../core/index.js'; import type { SingleView } from '../../../core/index.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export const popMobileRowMenu = ( export const popMobileRowMenu = (
target: PopupTarget, target: PopupTarget,
rowId: string, rowId: string,
dataViewEle: DataViewRenderer, tableViewLogic: MobileTableViewUILogic,
view: SingleView view: SingleView
) => { ) => {
popFilterableSimpleMenu(target, [ popFilterableSimpleMenu(target, [
@@ -21,7 +21,7 @@ export const popMobileRowMenu = (
name: 'Expand Row', name: 'Expand Row',
prefix: ExpandFullIcon(), prefix: ExpandFullIcon(),
select: () => { select: () => {
dataViewEle.openDetailPanel({ tableViewLogic.root.openDetailPanel({
view: view, view: view,
rowId: rowId, rowId: rowId,
}); });
@@ -10,9 +10,9 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js'; import { cellDivider } from '../styles.js';
import type { TableSingleView } from '../table-view-manager.js';
import { popMobileRowMenu } from './menu.js'; import { popMobileRowMenu } from './menu.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export class MobileTableRow extends SignalWatcher( export class MobileTableRow extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
@@ -73,7 +73,7 @@ export class MobileTableRow extends SignalWatcher(
v => v.id, v => v.id,
(column, i) => { (column, i) => {
const clickDetail = () => { const clickDetail = () => {
this.dataViewEle.openDetailPanel({ this.tableViewLogic.root.openDetailPanel({
view: this.view, view: this.view,
rowId: this.rowId, rowId: this.rowId,
}); });
@@ -83,7 +83,7 @@ export class MobileTableRow extends SignalWatcher(
popMobileRowMenu( popMobileRowMenu(
popupTargetFromElement(ele), popupTargetFromElement(ele),
this.rowId, this.rowId,
this.dataViewEle, this.tableViewLogic,
this.view this.view
); );
}; };
@@ -95,7 +95,7 @@ export class MobileTableRow extends SignalWatcher(
width: `${column.width$.value}px`, width: `${column.width$.value}px`,
border: i === 0 ? 'none' : undefined, border: i === 0 ? 'none' : undefined,
})} })}
.view="${view}" .tableViewLogic="${this.tableViewLogic}"
.column="${column}" .column="${column}"
.rowId="${this.rowId}" .rowId="${this.rowId}"
data-row-id="${this.rowId}" data-row-id="${this.rowId}"
@@ -107,7 +107,7 @@ export class MobileTableRow extends SignalWatcher(
data-column-index="${i}" data-column-index="${i}"
> >
</mobile-table-cell> </mobile-table-cell>
<div class="cell-divider"></div> <div class="${cellDivider}"></div>
</div> </div>
${!column.readonly$.value && ${!column.readonly$.value &&
column.view.mainProperties$.value.titleColumn === column.id column.view.mainProperties$.value.titleColumn === column.id
@@ -130,7 +130,7 @@ export class MobileTableRow extends SignalWatcher(
} }
@property({ attribute: false }) @property({ attribute: false })
accessor dataViewEle!: DataViewRenderer; accessor tableViewLogic!: MobileTableViewUILogic;
@property({ attribute: false }) @property({ attribute: false })
accessor rowId!: string; accessor rowId!: string;
@@ -138,8 +138,9 @@ export class MobileTableRow extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor rowIndex!: number; accessor rowIndex!: number;
@property({ attribute: false }) get view() {
accessor view!: TableSingleView; return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -0,0 +1,27 @@
import { css } from '@emotion/css';
import { cssVarV2 } from '@toeverything/theme/v2';
export const mobileTableViewWrapper = css({
position: 'relative',
width: '100%',
paddingBottom: '4px',
/**
* Disable horizontal scrolling to prevent crashes on iOS Safari
* See https://github.com/toeverything/AFFiNE/pull/12203
* and https://github.com/toeverything/blocksuite/pull/8784
*/
overflowX: 'hidden',
overflowY: 'hidden',
});
export const mobileTableViewContainer = css({
position: 'relative',
width: 'fit-content',
minWidth: '100%',
});
export const mobileCellDivider = css({
width: '1px',
height: '100%',
backgroundColor: cssVarV2.layer.insideBorder.border,
});
@@ -0,0 +1,160 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { GroupTrait } from '../../../core/group-by/trait.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import {
mobileTableViewContainer,
mobileTableViewWrapper,
} from './table-view-style.js';
export class MobileTableViewUILogic extends DataViewUILogicBase<
TableSingleView,
TableViewSelectionWithType
> {
ui$ = signal<MobileTableViewUI | undefined>(undefined);
private get readonly() {
return this.view.readonly$.value;
}
clearSelection = () => {};
addRow = (position: InsertToPosition) => {
if (this.readonly) return;
return this.view.rowAdd(position);
};
focusFirstCell = () => {};
showIndicator = (_evt: MouseEvent) => {
return false;
};
hideIndicator = () => {};
moveTo = () => {};
renderAddGroup = (groupHelper: GroupTrait) => {
const addGroup = groupHelper.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = groupHelper.property$.value;
if (column) {
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
}),
],
},
});
};
return html` <div style="display:flex;">
<div
class="dv-hover dv-round-8"
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
@click="${add}"
>
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
renderer = createUniComponentFromWebComponent(MobileTableViewUI);
}
export class MobileTableViewUI extends DataViewUIBase<MobileTableViewUILogic> {
override connectedCallback(): void {
super.connectedCallback();
this.logic.ui$.value = this;
this.classList.add(mobileTableViewWrapper);
}
private renderTable() {
const groups = this.logic.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div style="display:flex;flex-direction: column;gap: 16px;">
${repeat(
groups,
v => v.key,
group => {
return html` <mobile-table-group
data-group-key="${group.key}"
.tableViewLogic="${this.logic}"
.group="${group}"
></mobile-table-group>`;
}
)}
${this.logic.renderAddGroup(this.logic.view.groupTrait)}
</div>
`;
}
return html` <mobile-table-group
.tableViewLogic="${this.logic}"
></mobile-table-group>`;
}
override render(): TemplateResult {
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.logic.root.config.headerWidget, {
dataViewLogic: this.logic,
})}
<div class="${mobileTableViewWrapper}" style="${wrapperStyle}">
<div class="${mobileTableViewContainer}" style="${containerStyle}">
${this.renderTable()}
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-table-ui': MobileTableViewUI;
}
}
@@ -1,215 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { cssVarV2 } from '@toeverything/theme/v2';
import { css, unsafeCSS } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { GroupTrait } from '../../../core/group-by/trait.js';
import type { DataViewInstance } from '../../../core/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
export class MobileDataViewTable extends DataViewBase<
TableSingleView,
TableViewSelectionWithType
> {
static override styles = css`
.mobile-affine-database-table-wrapper {
position: relative;
width: 100%;
padding-bottom: 4px;
/**
* Disable horizontal scrolling to prevent crashes on iOS Safari
* See https://github.com/toeverything/AFFiNE/pull/12203
* and https://github.com/toeverything/blocksuite/pull/8784
*/
overflow-x: hidden;
overflow-y: hidden;
}
.mobile-affine-database-table-container {
position: relative;
width: fit-content;
min-width: 100%;
}
.cell-divider {
width: 1px;
height: 100%;
background-color: ${unsafeCSS(cssVarV2.layer.insideBorder.border)};
}
`;
private readonly _addRow = (
tableViewManager: TableSingleView,
position: InsertToPosition | number
) => {
if (this.readonly) return;
tableViewManager.rowAdd(position);
};
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
return;
}
const ele = event.currentTarget;
if (ele instanceof HTMLElement) {
if (ele.scrollWidth === ele.clientWidth) {
return;
}
event.stopPropagation();
}
};
renderAddGroup = (groupHelper: GroupTrait) => {
const addGroup = groupHelper.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = groupHelper.property$.value;
if (column) {
column.dataUpdate(
() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div style="display:flex;">
<div
class="dv-hover dv-round-8"
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
@click="${add}"
>
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
get expose(): DataViewInstance {
return {
clearSelection: () => {},
addRow: position => {
this._addRow(this.props.view, position);
},
focusFirstCell: () => {},
showIndicator: _evt => {
return false;
},
hideIndicator: () => {
// this.dragController.dropPreview.remove();
},
moveTo: (_id, _evt) => {
// const result = this.dragController.getInsertPosition(evt);
// if (result) {
// this.props.view.rowMove(
// id,
// result.position,
// undefined,
// result.groupKey,
// );
// }
},
getSelection: () => {
throw new BlockSuiteError(
ErrorCode.DatabaseBlockError,
'Not implemented'
);
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
private get readonly() {
return this.props.view.readonly$.value;
}
private renderTable() {
const groups = this.props.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div style="display:flex;flex-direction: column;gap: 16px;">
${repeat(
groups,
v => v.key,
group => {
return html` <mobile-table-group
data-group-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
.group="${group}"
></mobile-table-group>`;
}
)}
${this.renderAddGroup(this.props.view.groupTrait)}
</div>
`;
}
return html` <mobile-table-group
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
></mobile-table-group>`;
}
override render() {
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div class="mobile-affine-database-table-wrapper" style="${wrapperStyle}">
<div
class="mobile-affine-database-table-container"
style="${containerStyle}"
@wheel="${this.onWheel}"
>
${this.renderTable()}
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'mobile-data-view-table': MobileDataViewTable;
}
}
@@ -1,3 +1,4 @@
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { UIEventStateContext } from '@blocksuite/std'; import type { UIEventStateContext } from '@blocksuite/std';
import type { ReactiveController } from 'lit'; import type { ReactiveController } from 'lit';
@@ -9,20 +10,18 @@ import {
type TableViewSelection, type TableViewSelection,
type TableViewSelectionWithType, type TableViewSelectionWithType,
} from '../../selection'; } from '../../selection';
import type { VirtualTableView } from '../table-view.js'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table'; const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table';
type JsonAreaData = string[][]; type JsonAreaData = string[][];
const TEXT = 'text/plain'; const TEXT = 'text/plain';
export class TableClipboardController implements ReactiveController { export class TableClipboardController implements ReactiveController {
disposables = new DisposableGroup();
private readonly _onCopy = ( private readonly _onCopy = (
tableSelection: TableViewSelectionWithType, tableSelection: TableViewSelectionWithType,
isCut = false isCut = false
) => { ) => {
const table = this.host; const area = getSelectedArea(tableSelection, this.logic);
const area = getSelectedArea(tableSelection, table);
if (!area) { if (!area) {
return; return;
} }
@@ -44,7 +43,7 @@ export class TableClipboardController implements ReactiveController {
} }
} }
if (deleteRows.length) { if (deleteRows.length) {
this.props.view.rowsDelete(deleteRows); this.logic.view.rowsDelete(deleteRows);
} }
} }
this.clipboard this.clipboard
@@ -79,12 +78,11 @@ export class TableClipboardController implements ReactiveController {
private readonly _onPaste = async (_context: UIEventStateContext) => { private readonly _onPaste = async (_context: UIEventStateContext) => {
const event = _context.get('clipboardState').raw; const event = _context.get('clipboardState').raw;
event.stopPropagation(); event.stopPropagation();
const view = this.host;
const clipboardData = event.clipboardData; const clipboardData = event.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selection;
if (TableViewRowSelection.is(tableSelection)) { if (TableViewRowSelection.is(tableSelection)) {
return; return;
} }
@@ -97,7 +95,7 @@ export class TableClipboardController implements ReactiveController {
if (dataString) { if (dataString) {
// If internal format data exists, use it // If internal format data exists, use it
const jsonAreaData = JSON.parse(dataString) as JsonAreaData; const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
pasteToCells(view, jsonAreaData, tableSelection); pasteToCells(this.logic, jsonAreaData, tableSelection);
return true; return true;
} }
} catch { } catch {
@@ -115,7 +113,7 @@ export class TableClipboardController implements ReactiveController {
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows .filter(row => row.some(cell => cell !== '')); // Filter out empty rows
if (rows.length > 0) { if (rows.length > 0) {
pasteToCells(view, rows, tableSelection); pasteToCells(this.logic, rows, tableSelection);
} }
} }
} }
@@ -124,27 +122,28 @@ export class TableClipboardController implements ReactiveController {
}; };
private get clipboard() { private get clipboard() {
return this.props.clipboard; return this.logic.root.config.clipboard;
} }
private get notification() { private get notification() {
return this.props.notification; return this.logic.root.config.notification;
}
get props() {
return this.host.props;
} }
private get readonly() { private get readonly() {
return this.props.view.readonly$.value; return this.logic.view.readonly$.value;
} }
constructor(public host: VirtualTableView) { constructor(public logic: VirtualTableViewUILogic) {}
host.addController(this);
get host() {
return this.logic.ui$.value;
}
get selection() {
return this.logic.selectionController.selection;
} }
copy() { copy() {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selection;
if (!tableSelection) { if (!tableSelection) {
return; return;
} }
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
} }
cut() { cut() {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selection;
if (!tableSelection) { if (!tableSelection) {
return; return;
} }
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
} }
hostConnected() { hostConnected() {
this.host.disposables.add( this.disposables.add(
this.props.handleEvent('copy', _ctx => { this.logic.handleEvent('copy', _ctx => {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selection;
if (!tableSelection) return false; if (!tableSelection) return false;
this._onCopy(tableSelection); this._onCopy(tableSelection);
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
}) })
); );
this.host.disposables.add( this.disposables.add(
this.props.handleEvent('cut', _ctx => { this.logic.handleEvent('cut', _ctx => {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selection;
if (!tableSelection) return false; if (!tableSelection) return false;
this._onCut(tableSelection); this._onCut(tableSelection);
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
}) })
); );
this.host.disposables.add( this.disposables.add(
this.props.handleEvent('paste', ctx => { this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false; if (this.readonly) return false;
this._onPaste(ctx).catch(console.error); this._onPaste(ctx).catch(console.error);
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
function getSelectedArea( function getSelectedArea(
selection: TableViewSelection, selection: TableViewSelection,
table: VirtualTableView table: VirtualTableViewUILogic
): SelectedArea | undefined { ): SelectedArea | undefined {
const view = table.props.view; const view = table.view;
if (TableViewRowSelection.is(selection)) { if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rows(selection) const rows = TableViewRowSelection.rows(selection)
.map(row => { .map(row => {
@@ -282,7 +281,7 @@ function getTargetRangeFromSelection(
} }
function pasteToCells( function pasteToCells(
table: VirtualTableView, table: VirtualTableViewUILogic,
rows: JsonAreaData, rows: JsonAreaData,
selection: TableViewAreaSelection selection: TableViewAreaSelection
) { ) {
@@ -8,7 +8,7 @@ import * as Y from 'yjs';
import { t } from '../../../../core/index.js'; import { t } from '../../../../core/index.js';
import type { TableViewAreaSelection } from '../../selection'; import type { TableViewAreaSelection } from '../../selection';
import type { VirtualTableView } from '../table-view'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
export class DragToFillElement extends ShadowlessElement { export class DragToFillElement extends ShadowlessElement {
static override styles = css` static override styles = css`
@@ -55,12 +55,12 @@ declare global {
} }
export function fillSelectionWithFocusCellData( export function fillSelectionWithFocusCellData(
host: VirtualTableView, logic: VirtualTableViewUILogic,
selection: TableViewAreaSelection selection: TableViewAreaSelection
) { ) {
const { groupKey, rowsSelection, columnsSelection, focus } = selection; const { groupKey, rowsSelection, columnsSelection, focus } = selection;
const focusCell = host.selectionController.getCellContainer( const focusCell = logic.selectionController.getCellContainer(
groupKey, groupKey,
focus.rowIndex, focus.rowIndex,
focus.columnIndex focus.columnIndex
@@ -85,7 +85,7 @@ export function fillSelectionWithFocusCellData(
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i++) {
if (i === focus.rowIndex) continue; if (i === focus.rowIndex) continue;
const cellContainer = host.selectionController.getCellContainer( const cellContainer = logic.selectionController.getCellContainer(
groupKey, groupKey,
i, i,
draggingColIdx draggingColIdx
@@ -3,8 +3,7 @@
import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { ReactiveController } from 'lit'; import type { ReactiveController } from 'lit';
// import { startDrag } from '../../../../core/utils/drag.js'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
import type { VirtualTableView } from '../table-view';
export class TableDragController implements ReactiveController { export class TableDragController implements ReactiveController {
// dragStart = (row: TableRow, evt: PointerEvent) => { // dragStart = (row: TableRow, evt: PointerEvent) => {
@@ -91,9 +90,9 @@ export class TableDragController implements ReactiveController {
| undefined => { | undefined => {
const y = evt.y; const y = evt.y;
const tableRect = this.host const tableRect = this.host
.querySelector('affine-data-view-table-group') ?.querySelector('affine-data-view-table-group')
?.getBoundingClientRect(); ?.getBoundingClientRect();
const rows = this.host.querySelectorAll('data-view-table-row'); const rows = this.host?.querySelectorAll('data-view-table-row');
if (!rows || !tableRect || y < tableRect.top) { if (!rows || !tableRect || y < tableRect.top) {
return; return;
} }
@@ -127,12 +126,14 @@ export class TableDragController implements ReactiveController {
return position; return position;
}; };
constructor(private readonly host: VirtualTableView) { constructor(private readonly logic: VirtualTableViewUILogic) {}
this.host.addController(this);
get host() {
return this.logic.ui$.value;
} }
hostConnected() { hostConnected() {
if (this.host.props.view.readonly$.value) { if (this.logic.view.readonly$.value) {
return; return;
} }
// this.host.disposables.add( // this.host.disposables.add(
@@ -1,22 +1,26 @@
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu'; import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { ReactiveController } from 'lit'; import type { ReactiveController } from 'lit';
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection'; import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
import { popRowMenu } from '../row/menu'; import { popRowMenu } from '../row/menu';
import type { VirtualTableView } from '../table-view.js'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
export class TableHotkeysController implements ReactiveController { export class TableHotkeysController implements ReactiveController {
disposables = new DisposableGroup();
get selectionController() { get selectionController() {
return this.host.selectionController; return this.logic.selectionController;
} }
constructor(private readonly host: VirtualTableView) { constructor(private readonly logic: VirtualTableViewUILogic) {}
this.host.addController(this);
get host() {
return this.logic.ui$.value;
} }
hostConnected() { hostConnected() {
this.host.disposables.add( this.disposables.add(
this.host.props.bindHotkey({ this.logic.bindHotkey({
Backspace: () => { Backspace: () => {
const selection = this.selectionController.selection; const selection = this.selectionController.selection;
if (!selection) { if (!selection) {
@@ -25,7 +29,7 @@ export class TableHotkeysController implements ReactiveController {
if (TableViewRowSelection.is(selection)) { if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rowsIds(selection); const rows = TableViewRowSelection.rowsIds(selection);
this.selectionController.selection = undefined; this.selectionController.selection = undefined;
this.host.props.view.rowsDelete(rows); this.logic.view.rowsDelete(rows);
return; return;
} }
const { const {
@@ -334,14 +338,14 @@ export class TableHotkeysController implements ReactiveController {
context.get('keyboardState').raw.preventDefault(); context.get('keyboardState').raw.preventDefault();
this.selectionController.selection = TableViewRowSelection.create({ this.selectionController.selection = TableViewRowSelection.create({
rows: rows:
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap( this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
group => group =>
group?.rows.map(row => ({ group?.rows.map(row => ({
groupKey: group.key, groupKey: group.key,
id: row.rowId, id: row.rowId,
})) ?? [] })) ?? []
) ?? ) ??
this.host.props.view.rows$.value.map(row => ({ this.logic.view.rows$.value.map(row => ({
groupKey: undefined, groupKey: undefined,
id: row.rowId, id: row.rowId,
})), })),
@@ -377,7 +381,7 @@ export class TableHotkeysController implements ReactiveController {
rows: [row], rows: [row],
}); });
popRowMenu( popRowMenu(
this.host.props.dataViewEle, this.logic,
popupTargetFromElement(cell), popupTargetFromElement(cell),
this.selectionController this.selectionController
); );
@@ -1,4 +1,5 @@
import { getRangeByPositions } from '@blocksuite/affine-shared/utils'; import { getRangeByPositions } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import { computed, type ReadonlySignal } from '@preact/signals-core'; import { computed, type ReadonlySignal } from '@preact/signals-core';
@@ -18,7 +19,7 @@ import {
type TableViewSelectionWithType, type TableViewSelectionWithType,
} from '../../selection'; } from '../../selection';
import type { DatabaseCellContainer } from '../row/cell'; import type { DatabaseCellContainer } from '../row/cell';
import type { VirtualTableView } from '../table-view.js'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { TableGridCell } from '../types.js'; import type { TableGridCell } from '../types.js';
import { import {
DragToFillElement, DragToFillElement,
@@ -26,6 +27,7 @@ import {
} from './drag-to-fill.js'; } from './drag-to-fill.js';
export class TableSelectionController implements ReactiveController { export class TableSelectionController implements ReactiveController {
disposables = new DisposableGroup();
private _tableViewSelection?: TableViewSelectionWithType; private _tableViewSelection?: TableViewSelectionWithType;
private readonly getFocusCellContainer = () => { private readonly getFocusCellContainer = () => {
@@ -85,12 +87,12 @@ export class TableSelectionController implements ReactiveController {
); );
const cell = container?.cell; const cell = container?.cell;
const isEditing = cell ? cell.beforeEnterEditMode() : true; const isEditing = cell ? cell.beforeEnterEditMode() : true;
this.host.props.setSelection({ this.logic.setSelection({
...selection, ...selection,
isEditing, isEditing,
}); });
} else { } else {
this.host.props.setSelection(selection); this.logic.setSelection(selection);
} }
} }
@@ -99,27 +101,26 @@ export class TableSelectionController implements ReactiveController {
} }
get view() { get view() {
return this.host.props.view; return this.logic.view;
} }
get viewData() { constructor(public logic: VirtualTableViewUILogic) {
return this.view;
}
constructor(public host: VirtualTableView) {
host.addController(this);
this.__selectionElement = new SelectionElement(); this.__selectionElement = new SelectionElement();
this.__selectionElement.controller = this; this.__selectionElement.controller = this;
} }
get host() {
return this.logic.ui$.value;
}
private clearSelection() { private clearSelection() {
this.host.props.setSelection(); this.logic.setSelection();
} }
private handleDragEvent() { private handleDragEvent() {
this.host.disposables.add( this.disposables.add(
this.host.props.handleEvent('dragStart', context => { this.logic.handleEvent('dragStart', context => {
if (this.host.props.view.readonly$.value) { if (this.logic.view.readonly$.value) {
return; return;
} }
const event = context.get('pointerState').raw; const event = context.get('pointerState').raw;
@@ -150,8 +151,8 @@ export class TableSelectionController implements ReactiveController {
} }
private handleSelectionChange() { private handleSelectionChange() {
this.host.disposables.add( this.disposables.add(
this.host.props.selection$.subscribe(tableSelection => { this.logic.selection$.subscribe(tableSelection => {
if (!this.isValidSelection(tableSelection)) { if (!this.isValidSelection(tableSelection)) {
this.selection = undefined; this.selection = undefined;
return; return;
@@ -236,7 +237,7 @@ export class TableSelectionController implements ReactiveController {
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows ? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
: this.view.rows$.value; : this.view.rows$.value;
requestAnimationFrame(() => { requestAnimationFrame(() => {
const index = this.host.props.view.properties$.value.findIndex( const index = this.view.properties$.value.findIndex(
v => v.type$.value === 'title' v => v.type$.value === 'title'
); );
this.selection = TableViewAreaSelection.create({ this.selection = TableViewAreaSelection.create({
@@ -625,7 +626,7 @@ export class TableSelectionController implements ReactiveController {
} }
get virtualScroll() { get virtualScroll() {
return this.host.virtualScroll$.value; return this.logic.virtualScroll$.value;
} }
getGroup(groupKey: string | undefined) { getGroup(groupKey: string | undefined) {
@@ -913,7 +914,7 @@ export class TableSelectionController implements ReactiveController {
if (fillValues && this.selection) { if (fillValues && this.selection) {
this.__dragToFillElement.dragging = false; this.__dragToFillElement.dragging = false;
fillSelectionWithFocusCellData( fillSelectionWithFocusCellData(
this.host, this.logic,
TableViewAreaSelection.create({ TableViewAreaSelection.create({
groupKey: groupKey, groupKey: groupKey,
rowsSelection: selection.row, rowsSelection: selection.row,
@@ -1024,7 +1025,7 @@ export class SelectionElement extends SignalWatcher(
if (left == null || top == null || width == null || height == null) { if (left == null || top == null || width == null || height == null) {
return; return;
} }
const paddingLeft = this.controller.host.props.virtualPadding$.value; const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
return { return {
left: left + paddingLeft, left: left + paddingLeft,
top, top,
@@ -1054,7 +1055,7 @@ export class SelectionElement extends SignalWatcher(
if (!rect) { if (!rect) {
return; return;
} }
const paddingLeft = this.controller.host.props.virtualPadding$.value; const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
return { return {
left: rect.left + paddingLeft, left: rect.left + paddingLeft,
top: rect.top, top: rect.top,
@@ -1063,15 +1064,8 @@ export class SelectionElement extends SignalWatcher(
}; };
}); });
rowsPosition$ = computed(() => {
const selection = this.selection$.value;
if (selection?.selectionType !== 'area') {
return;
}
});
get selection$() { get selection$() {
return this.controller.host.props.selection$; return this.controller.logic.selection$;
} }
override render() { override render() {
@@ -1,3 +1,4 @@
import { KanbanViewUI } from '../../kanban/pc/kanban-view-ui-logic';
import { DragToFillElement } from './controller/drag-to-fill'; import { DragToFillElement } from './controller/drag-to-fill';
import { SelectionElement } from './controller/selection'; import { SelectionElement } from './controller/selection';
import { TableGroupFooter } from './group/bottom/group-footer'; import { TableGroupFooter } from './group/bottom/group-footer';
@@ -12,11 +13,12 @@ import { TableVerticalIndicator } from './group/top/header/vertical-indicator';
import { DatabaseCellContainer } from './row/cell'; import { DatabaseCellContainer } from './row/cell';
import { TableRowHeader } from './row/row-header'; import { TableRowHeader } from './row/row-header';
import { TableRowLast } from './row/row-last'; import { TableRowLast } from './row/row-last';
import { VirtualTableView } from './table-view'; import { TableViewUI } from './table-view-ui-logic';
import { VirtualElementWrapper } from './virtual/virtual-cell'; import { VirtualElementWrapper } from './virtual/virtual-cell';
export function pcVirtualEffects() { export function pcVirtualEffects() {
customElements.define('affine-virtual-table', VirtualTableView); customElements.define('dv-table-view-ui-virtual', TableViewUI);
customElements.define('dv-kanban-view-ui', KanbanViewUI);
customElements.define( customElements.define(
'affine-database-virtual-cell-container', 'affine-database-virtual-cell-container',
DatabaseCellContainer DatabaseCellContainer
@@ -6,29 +6,29 @@ import { html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { TableViewAreaSelection } from '../../../selection'; import { TableViewAreaSelection } from '../../../selection';
import type { VirtualTableView } from '../../table-view'; import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
import type { TableGridGroup } from '../../types'; import type { TableGridGroup } from '../../types';
import * as styles from './group-footer-css'; import * as styles from './group-footer-css';
export class TableGroupFooter extends WithDisposable(ShadowlessElement) { export class TableGroupFooter extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor tableView!: VirtualTableView; accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false }) @property({ attribute: false })
accessor gridGroup!: TableGridGroup; accessor gridGroup!: TableGridGroup;
group$ = computed(() => { group$ = computed(() => {
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find( return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
g => g.key === this.gridGroup.groupId g => g.key === this.gridGroup.groupId
); );
}); });
get selectionController() { get selectionController() {
return this.tableView.selectionController; return this.tableViewLogic.selectionController;
} }
get tableViewManager() { get tableViewManager() {
return this.tableView.props.view; return this.tableViewLogic.view;
} }
override connectedCallback() { override connectedCallback() {
@@ -10,7 +10,7 @@ import { html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { TableViewAreaSelection } from '../../../selection'; import { TableViewAreaSelection } from '../../../selection';
import type { VirtualTableView } from '../../table-view'; import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
import type { TableGridGroup } from '../../types'; import type { TableGridGroup } from '../../types';
import * as styles from './group-header-css'; import * as styles from './group-header-css';
import { GroupTitle } from './group-title'; import { GroupTitle } from './group-title';
@@ -18,7 +18,7 @@ export class TableGroupHeader extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
@property({ attribute: false }) @property({ attribute: false })
accessor tableView!: VirtualTableView; accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false }) @property({ attribute: false })
accessor gridGroup!: TableGridGroup; accessor gridGroup!: TableGridGroup;
@@ -35,7 +35,7 @@ export class TableGroupHeader extends SignalWatcher(
} }
group$ = computed(() => { group$ = computed(() => {
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find( return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
g => g.key === this.gridGroup.groupId g => g.key === this.gridGroup.groupId
); );
}); });
@@ -45,11 +45,11 @@ export class TableGroupHeader extends SignalWatcher(
}); });
get tableViewManager() { get tableViewManager() {
return this.tableView.props.view; return this.tableViewLogic.view;
} }
get selectionController() { get selectionController() {
return this.tableView.selectionController; return this.tableViewLogic.selectionController;
} }
private readonly clickAddRowInStart = () => { private readonly clickAddRowInStart = () => {
@@ -123,7 +123,7 @@ export class TableGroupHeader extends SignalWatcher(
return html` return html`
${this.renderGroupHeader()} ${this.renderGroupHeader()}
<virtual-table-header <virtual-table-header
.tableViewManager="${this.tableViewManager}" .tableViewLogic="${this.tableViewLogic}"
></virtual-table-header> ></virtual-table-header>
`; `;
} }
@@ -6,6 +6,7 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { PlusIcon } from '@blocksuite/icons/lit'; import { PlusIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import { css } from '@emotion/css';
import { nothing } from 'lit'; import { nothing } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
@@ -13,9 +14,13 @@ import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import { renderUniLit } from '../../../../../../core'; import { renderUniLit } from '../../../../../../core';
import type { TableSingleView } from '../../../../table-view-manager'; import { LEFT_TOOL_BAR_WIDTH } from '../../../../consts';
import { cellDivider } from '../../../../styles';
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
import * as styles from './column-header-css'; import * as styles from './column-header-css';
const leftBarStyle = css({
width: LEFT_TOOL_BAR_WIDTH,
});
export class VirtualTableHeader extends SignalWatcher( export class VirtualTableHeader extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
@@ -79,9 +84,7 @@ export class VirtualTableHeader extends SignalWatcher(
override render() { override render() {
return html` return html`
<div class="${styles.columnHeader} database-row"> <div class="${styles.columnHeader} database-row">
${this.readonly ${this.readonly ? nothing : html` <div class="${leftBarStyle}"></div>`}
? nothing
: html` <div class="data-view-table-left-bar"></div>`}
${repeat( ${repeat(
this.tableViewManager.properties$.value, this.tableViewManager.properties$.value,
column => column.id, column => column.id,
@@ -97,9 +100,9 @@ export class VirtualTableHeader extends SignalWatcher(
data-column-index="${index}" data-column-index="${index}"
class="${styles.column} ${styles.cell}" class="${styles.column} ${styles.cell}"
.column="${column}" .column="${column}"
.tableViewManager="${this.tableViewManager}" .tableViewLogic="${this.tableViewLogic}"
></virtual-database-header-column> ></virtual-database-header-column>
<div class="cell-divider" style="height: auto;"></div> <div class="${cellDivider}" style="height: auto;"></div>
`; `;
} }
)} )}
@@ -118,7 +121,11 @@ export class VirtualTableHeader extends SignalWatcher(
accessor scaleDiv!: HTMLDivElement; accessor scaleDiv!: HTMLDivElement;
@property({ attribute: false }) @property({ attribute: false })
accessor tableViewManager!: TableSingleView; accessor tableViewLogic!: VirtualTableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -13,6 +13,7 @@ import type {
TableProperty, TableProperty,
TableSingleView, TableSingleView,
} from '../../../../table-view-manager'; } from '../../../../table-view-manager';
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
export class DataViewColumnPreview extends SignalWatcher( export class DataViewColumnPreview extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
`; `;
get tableViewManager(): TableSingleView { get tableViewManager(): TableSingleView {
return this.column.view as TableSingleView; return this.tableViewLogic.view;
} }
private renderGroup(rows: Row[]) { private renderGroup(rows: Row[]) {
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
)};box-shadow: var(--affine-shadow-2);" )};box-shadow: var(--affine-shadow-2);"
> >
<affine-database-header-column <affine-database-header-column
.tableViewManager="${this.tableViewManager}" .tableViewLogic="${this.tableViewLogic}"
.column="${this.column}" .column="${this.column}"
></affine-database-header-column> ></affine-database-header-column>
${repeat(rows, (id, index) => { ${repeat(rows, (id, index) => {
const height = this.container.querySelector( const height = this.container.querySelector(
`affine-database-cell-container[data-row-id="${id}"]` `dv-table-view-cell-container[data-row-id="${id}"]`
)?.clientHeight; )?.clientHeight;
const style = styleMap({ const style = styleMap({
height: height + 'px', height: height + 'px',
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
)}" )}"
> >
<div style="${style}"> <div style="${style}">
<affine-database-cell-container <dv-table-view-cell-container
.column="${this.column}" .column="${this.column}"
.view="${this.tableViewManager}" .tableViewLogic="${this.tableViewLogic}"
.rowId="${id}" .rowId="${id}"
.columnId="${this.column.id}" .columnId="${this.column.id}"
.rowIndex="${index}" .rowIndex="${index}"
.columnIndex="${columnIndex}" .columnIndex="${columnIndex}"
></affine-database-cell-container> ></dv-table-view-cell-container>
</div> </div>
</div>`; </div>`;
})} })}
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor group: Group | undefined = undefined; accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor tableViewLogic!: VirtualTableViewUILogic;
} }
declare global { declare global {
@@ -45,10 +45,8 @@ import {
ShowQuickSettingBarKey, ShowQuickSettingBarKey,
} from '../../../../../../widget-presets/quick-setting-bar/context'; } from '../../../../../../widget-presets/quick-setting-bar/context';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts';
import type { import type { TableProperty } from '../../../../table-view-manager';
TableProperty, import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
TableSingleView,
} from '../../../../table-view-manager';
import { import {
getTableGroupRect, getTableGroupRect,
getVerticalIndicator, getVerticalIndicator,
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
const sortUtils = createSortUtils( const sortUtils = createSortUtils(
sortTrait, sortTrait,
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {}) this.tableViewLogic.eventTrace ?? (() => {})
); );
const sortList = sortUtils.sortList$.value; const sortList = sortUtils.sortList$.value;
const existingIndex = sortList.findIndex( const existingIndex = sortList.findIndex(
@@ -398,28 +396,25 @@ export class DatabaseHeaderColumn extends SignalWatcher(
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
const table = this.closest('affine-database-table'); this.disposables.add(
if (table) { this.tableViewLogic.handleEvent('dragStart', context => {
this.disposables.add( if (this.tableViewManager.readonly$.value) {
table.props.handleEvent('dragStart', context => { return;
if (this.tableViewManager.readonly$.value) { }
return; const event = context.get('pointerState').raw;
} const target = event.target;
const event = context.get('pointerState').raw; if (
const target = event.target; target instanceof Element &&
if ( this.widthDragBar.value?.contains(target)
target instanceof Element && ) {
this.widthDragBar.value?.contains(target) event.preventDefault();
) { event.stopPropagation();
event.preventDefault(); this.widthDragStart(event);
event.stopPropagation(); return true;
this.widthDragStart(event); }
return true; return false;
} })
return false; );
})
);
}
} }
override render() { override render() {
@@ -481,7 +476,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd'; accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
@property({ attribute: false }) @property({ attribute: false })
accessor tableViewManager!: TableSingleView; accessor tableViewLogic!: VirtualTableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
} }
function numberFormatConfig(column: Property): MenuConfig { function numberFormatConfig(column: Property): MenuConfig {
@@ -10,13 +10,12 @@ import type {
CellRenderProps, CellRenderProps,
DataViewCellLifeCycle, DataViewCellLifeCycle,
} from '../../../../core/property'; } from '../../../../core/property';
import type { SingleView } from '../../../../core/view-manager/single-view';
import { import {
TableViewAreaSelection, TableViewAreaSelection,
TableViewRowSelection, TableViewRowSelection,
type TableViewSelectionWithType, type TableViewSelectionWithType,
} from '../../selection'; } from '../../selection';
import type { VirtualTableView } from '../table-view'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
import type { TableGridCell } from '../types'; import type { TableGridCell } from '../types';
import { popRowMenu } from './menu'; import { popRowMenu } from './menu';
import { rowSelectedBg } from './row-header-css'; import { rowSelectedBg } from './row-header-css';
@@ -82,7 +81,7 @@ export class DatabaseCellContainer extends SignalWatcher(
} }
private get selectionView() { private get selectionView() {
return this.tableView?.selectionController; return this.tableViewLogic.selectionController;
} }
get rowSelected$() { get rowSelected$() {
@@ -104,11 +103,7 @@ export class DatabaseCellContainer extends SignalWatcher(
rows: [row], rows: [row],
}); });
} }
popRowMenu( popRowMenu(this.tableViewLogic, popupTargetFromElement(this), selection);
this.tableView.props.dataViewEle,
popupTargetFromElement(this),
selection
);
}; };
override connectedCallback() { override connectedCallback() {
@@ -216,10 +211,11 @@ export class DatabaseCellContainer extends SignalWatcher(
accessor gridCell!: TableGridCell; accessor gridCell!: TableGridCell;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: SingleView; accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false }) get view() {
accessor tableView!: VirtualTableView; return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -12,19 +12,19 @@ import {
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { html } from 'lit'; import { html } from 'lit';
import type { DataViewRenderer } from '../../../../core/data-view';
import { TableViewRowSelection } from '../../selection'; import { TableViewRowSelection } from '../../selection';
import type { TableSelectionController } from '../controller/selection'; import type { TableSelectionController } from '../controller/selection';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
export const openDetail = ( export const openDetail = (
dataViewEle: DataViewRenderer, tableViewLogic: VirtualTableViewUILogic,
rowId: string, rowId: string,
selection: TableSelectionController selection: TableSelectionController
) => { ) => {
const old = selection.selection; const old = selection.selection;
selection.selection = undefined; selection.selection = undefined;
dataViewEle.openDetailPanel({ tableViewLogic.root.openDetailPanel({
view: selection.host.props.view, view: tableViewLogic.view,
rowId: rowId, rowId: rowId,
onClose: () => { onClose: () => {
selection.selection = old; selection.selection = old;
@@ -33,7 +33,7 @@ export const openDetail = (
}; };
export const popRowMenu = ( export const popRowMenu = (
dataViewEle: DataViewRenderer, tableViewLogic: VirtualTableViewUILogic,
ele: PopupTarget, ele: PopupTarget,
selectionController: TableSelectionController selectionController: TableSelectionController
) => { ) => {
@@ -55,7 +55,7 @@ export const popRowMenu = (
${CopyIcon()} ${CopyIcon()}
</div>`, </div>`,
select: () => { select: () => {
selectionController.host.clipboardController.copy(); tableViewLogic.clipboardController.copy();
}, },
}), }),
], ],
@@ -85,7 +85,7 @@ export const popRowMenu = (
name: 'Expand Row', name: 'Expand Row',
prefix: ExpandFullIcon(), prefix: ExpandFullIcon(),
select: () => { select: () => {
openDetail(dataViewEle, row.id, selectionController); openDetail(tableViewLogic, row.id, selectionController);
}, },
}), }),
menu.group({ menu.group({
@@ -7,14 +7,17 @@ import { nothing } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { VirtualTableView } from '../table-view.js';
import type { TableGridCell } from '../types.js'; import type { TableGridCell } from '../types.js';
import * as styles from './row-header-css.js'; import * as styles from './row-header-css.js';
export class TableRowHeader extends SignalWatcher( export class TableRowHeader extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
get view() {
return this.tableViewLogic.view;
}
override connectedCallback(): void { override connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this.disposables.add( this.disposables.add(
@@ -42,7 +45,7 @@ export class TableRowHeader extends SignalWatcher(
}; };
get selectionController() { get selectionController() {
return this.tableView.selectionController; return this.tableViewLogic.selectionController;
} }
get rowSelected$() { get rowSelected$() {
@@ -115,10 +118,7 @@ export class TableRowHeader extends SignalWatcher(
accessor gridCell!: TableGridCell; accessor gridCell!: TableGridCell;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: TableSingleView; accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
} }
declare global { declare global {
@@ -5,8 +5,7 @@ import { effect } from '@preact/signals-core';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js'; import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { VirtualTableView } from '../table-view.js';
import type { TableGridCell } from '../types.js'; import type { TableGridCell } from '../types.js';
import * as styles from './row-header-css.js'; import * as styles from './row-header-css.js';
@@ -56,10 +55,7 @@ export class TableRowLast extends SignalWatcher(
accessor gridCell!: TableGridCell; accessor gridCell!: TableGridCell;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: TableSingleView; accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
} }
declare global { declare global {
@@ -3,19 +3,27 @@ import {
popMenu, popMenu,
popupTargetFromElement, popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu'; } from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit'; import { AddCursorIcon } from '@blocksuite/icons/lit';
import { computed, signal } from '@preact/signals-core'; import { computed, signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import type { TemplateResult } from 'lit';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import * as dv from '../../../core/common/dv-css.js'; import { dv } from '../../../core/common/dv-css.js';
import { import {
type GroupTrait, type GroupTrait,
groupTraitKey, groupTraitKey,
} from '../../../core/group-by/trait.js'; } from '../../../core/group-by/trait.js';
import { type DataViewInstance, renderUniLit } from '../../../core/index.js'; import {
import { DataViewBase } from '../../../core/view/data-view-base.js'; createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import { import {
type TableSingleView, type TableSingleView,
TableViewRowSelection, TableViewRowSelection,
@@ -26,9 +34,9 @@ import { TableClipboardController } from './controller/clipboard.js';
import { TableDragController } from './controller/drag.js'; import { TableDragController } from './controller/drag.js';
import { TableHotkeysController } from './controller/hotkeys.js'; import { TableHotkeysController } from './controller/hotkeys.js';
import { TableSelectionController } from './controller/selection.js'; import { TableSelectionController } from './controller/selection.js';
import { TableGroupFooter } from './group/bottom/group-footer'; import { TableGroupFooter } from './group/bottom/group-footer.js';
import { TableGroupHeader } from './group/top/group-header'; import { TableGroupHeader } from './group/top/group-header.js';
import { DatabaseCellContainer } from './row/cell'; import { DatabaseCellContainer } from './row/cell.js';
import { TableRowHeader } from './row/row-header.js'; import { TableRowHeader } from './row/row-header.js';
import { TableRowLast } from './row/row-last.js'; import { TableRowLast } from './row/row-last.js';
import * as styles from './table-view-css.js'; import * as styles from './table-view-css.js';
@@ -43,15 +51,83 @@ import {
GridVirtualScroll, GridVirtualScroll,
} from './virtual/virtual-scroll.js'; } from './virtual/virtual-scroll.js';
export class VirtualTableView extends DataViewBase< export class VirtualTableViewUILogic extends DataViewUILogicBase<
TableSingleView, TableSingleView,
TableViewSelectionWithType TableViewSelectionWithType
> { > {
ui$ = signal<TableViewUI | undefined>();
clipboardController = new TableClipboardController(this); clipboardController = new TableClipboardController(this);
dragController = new TableDragController(this); dragController = new TableDragController(this);
hotkeysController = new TableHotkeysController(this); hotkeysController = new TableHotkeysController(this);
selectionController = new TableSelectionController(this);
virtualScroll$ = signal<TableGrid>();
yScrollContainer: HTMLElement | undefined;
columns$ = computed(() => {
return [
{
id: 'row-header',
width: LEFT_TOOL_BAR_WIDTH,
},
...this.view.properties$.value.map(property => ({
id: property.id,
width: property.width$.value + 1,
})),
{
id: 'row-last',
width: 40,
},
];
});
groupTrait$ = computed(() => {
return this.view.traitGet(groupTraitKey);
});
groups$ = computed(() => {
const groupTrait = this.groupTrait$.value;
if (!groupTrait?.groupsDataList$.value) {
return [
{
id: '',
rows: this.view.rowIds$.value,
},
];
}
return groupTrait.groupsDataList$.value.map(group => ({
id: group.key,
rows: group.rows.map(v => v.rowId),
}));
});
clearSelection = () => {
this.selectionController.clear();
};
addRow = (position: InsertToPosition) => {
return this.view.rowAdd(position);
};
focusFirstCell = () => {
this.selectionController.focusFirstCell();
};
showIndicator = (evt: MouseEvent) => {
return this.dragController.showIndicator(evt) != null;
};
hideIndicator = () => {
this.dragController.dropPreview.remove();
};
moveTo = (id: string, evt: MouseEvent) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
};
onWheel = (event: WheelEvent) => { onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) { if (event.metaKey || event.ctrlKey) {
@@ -80,13 +156,12 @@ export class VirtualTableView extends DataViewBase<
onComplete: text => { onComplete: text => {
const column = groupHelper.property$.value; const column = groupHelper.property$.value;
if (column) { if (column) {
column.dataUpdate( column.dataUpdate(() =>
() => addGroup({
addGroup({ text,
text, oldData: column.data$.value,
oldData: column.data$.value, dataSource: this.view.manager.dataSource,
dataSource: this.props.view.manager.dataSource, })
}) as never
); );
} }
}, },
@@ -103,92 +178,8 @@ export class VirtualTableView extends DataViewBase<
</div>`; </div>`;
}; };
selectionController = new TableSelectionController(this); initVirtualScroll(yScrollContainer: HTMLElement, ui: TableViewUI) {
yScrollContainer: HTMLElement | undefined; const virtualScroll = new GridVirtualScroll<
get expose(): DataViewInstance {
return {
clearSelection: () => {
this.selectionController.clear();
},
addRow: position => {
if (this.readonly) return;
const rowId = this.props.view.rowAdd(position);
if (rowId) {
this.props.dataViewEle.openDetailPanel({
view: this.props.view,
rowId,
});
}
return rowId;
},
focusFirstCell: () => {
this.selectionController.focusFirstCell();
},
showIndicator: evt => {
return this.dragController.showIndicator(evt) != null;
},
hideIndicator: () => {
this.dragController.dropPreview.remove();
},
moveTo: (id, evt) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.props.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
},
getSelection: () => {
return this.selectionController.selection;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
private get readonly() {
return this.props.view.readonly$.value;
}
columns$ = computed(() => {
return [
{
id: 'row-header',
width: LEFT_TOOL_BAR_WIDTH,
},
...this.props.view.properties$.value.map(property => ({
id: property.id,
width: property.width$.value + 1,
})),
{
id: 'row-last',
width: 40,
},
];
});
groupTrait$ = computed(() => {
return this.props.view.traitGet(groupTraitKey);
});
groups$ = computed(() => {
const groupTrait = this.groupTrait$.value;
if (!groupTrait?.groupsDataList$.value) {
return [
{
id: '',
rows: this.props.view.rowIds$.value,
},
];
}
return groupTrait.groupsDataList$.value.map(group => ({
id: group.key,
rows: group.rows.map(v => v.rowId),
}));
});
virtualScroll$ = signal<TableGrid>();
private initVirtualScroll(yScrollContainer: HTMLElement) {
this.virtualScroll$.value = new GridVirtualScroll<
TableGroupData, TableGroupData,
TableRowData, TableRowData,
TableCellData TableCellData
@@ -213,7 +204,7 @@ export class VirtualTableView extends DataViewBase<
return row.cells$.value.some(cell => cell.data.hover$.value); return row.cells$.value.some(cell => cell.data.hover$.value);
}), }),
selected$: computed(() => { selected$: computed(() => {
const selection = this.props.selection$.value; const selection = this.selection$.value;
if (!selection || selection.selectionType !== 'row') { if (!selection || selection.selectionType !== 'row') {
return false; return false;
} }
@@ -234,34 +225,31 @@ export class VirtualTableView extends DataViewBase<
if (cell.columnId === 'row-header') { if (cell.columnId === 'row-header') {
wrapper.style.borderBottom = `1px solid ${cssVarV2.database.border}`; wrapper.style.borderBottom = `1px solid ${cssVarV2.database.border}`;
const rowHeader = new TableRowHeader(); const rowHeader = new TableRowHeader();
rowHeader.view = this.props.view;
rowHeader.gridCell = cell; rowHeader.gridCell = cell;
rowHeader.tableView = this; rowHeader.tableViewLogic = this;
return rowHeader; return rowHeader;
} }
if (cell.columnId === 'row-last') { if (cell.columnId === 'row-last') {
const rowLast = new TableRowLast(); const rowLast = new TableRowLast();
rowLast.view = this.props.view;
rowLast.gridCell = cell; rowLast.gridCell = cell;
rowLast.tableView = this; rowLast.tableViewLogic = this;
return rowLast; return rowLast;
} }
const cellContainer = new DatabaseCellContainer(); const cellContainer = new DatabaseCellContainer();
cellContainer.view = this.props.view;
cellContainer.gridCell = cell; cellContainer.gridCell = cell;
cellContainer.tableView = this; cellContainer.tableViewLogic = this;
return cellContainer; return cellContainer;
}, },
createGroup: { createGroup: {
top: gridGroup => { top: gridGroup => {
const groupHeader = new TableGroupHeader(); const groupHeader = new TableGroupHeader();
groupHeader.tableView = this; groupHeader.tableViewLogic = this;
groupHeader.gridGroup = gridGroup; groupHeader.gridGroup = gridGroup;
return groupHeader; return groupHeader;
}, },
bottom: gridGroup => { bottom: gridGroup => {
const groupFooter = new TableGroupFooter(); const groupFooter = new TableGroupFooter();
groupFooter.tableView = this; groupFooter.tableViewLogic = this;
groupFooter.gridGroup = gridGroup; groupFooter.gridGroup = gridGroup;
return groupFooter; return groupFooter;
}, },
@@ -269,26 +257,40 @@ export class VirtualTableView extends DataViewBase<
fixedRowHeight$: signal(undefined), fixedRowHeight$: signal(undefined),
yScrollContainer, yScrollContainer,
}); });
this.yScrollContainer = yScrollContainer;
this.virtualScroll$.value = virtualScroll;
requestAnimationFrame(() => { requestAnimationFrame(() => {
const virtualScroll = this.virtualScroll$.value;
if (virtualScroll) { if (virtualScroll) {
virtualScroll.init(); virtualScroll.init();
this.disposables.add(() => virtualScroll.dispose()); ui.disposables.add(() => virtualScroll.dispose());
} }
}); });
} }
renderer = createUniComponentFromWebComponent(TableViewUI);
}
export class TableViewUI extends DataViewUIBase<VirtualTableViewUILogic> {
private renderTable() { private renderTable() {
return this.virtualScroll$.value?.content; return this.logic.virtualScroll$.value?.content;
} }
override connectedCallback(): void { override connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this.initVirtualScroll(getScrollContainer(this, 'y') ?? document.body); this.logic.ui$.value = this;
this.logic.clipboardController.hostConnected();
this.logic.dragController.hostConnected();
this.logic.hotkeysController.hostConnected();
this.logic.selectionController.hostConnected();
const scrollContainer = getScrollContainer(this, 'y') ?? document.body;
this.logic.initVirtualScroll(scrollContainer, this);
this.classList.add(styles.tableView); this.classList.add(styles.tableView);
} }
override render() { override render(): TemplateResult {
const vPadding = this.props.virtualPadding$.value; const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({ const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`, marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`, marginRight: `-${vPadding}px`,
@@ -298,11 +300,11 @@ export class VirtualTableView extends DataViewBase<
paddingRight: `${vPadding}px`, paddingRight: `${vPadding}px`,
}); });
return html` return html`
${renderUniLit(this.props.headerWidget, { ${renderUniLit(this.logic.root.config.headerWidget, {
dataViewInstance: this.expose, dataViewLogic: this.logic,
})} })}
<div class="${styles.tableContainer}" style="${wrapperStyle}"> <div class="${styles.tableContainer}" style="${wrapperStyle}">
<div class="${styles.tableBlockTable}" @wheel="${this.onWheel}"> <div class="${styles.tableBlockTable}" @wheel="${this.logic.onWheel}">
<div class="${styles.tableContainer2}" style="${containerStyle}"> <div class="${styles.tableContainer2}" style="${containerStyle}">
${this.renderTable()} ${this.renderTable()}
</div> </div>
@@ -314,6 +316,6 @@ export class VirtualTableView extends DataViewBase<
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'affine-virtual-table': VirtualTableView; 'dv-table-view-ui-virtual': TableViewUI;
} }
} }
@@ -61,8 +61,8 @@ export class BatchTaskManager {
private run(): void { private run(): void {
let totalBatchCount = this.totalBatchSize; let totalBatchCount = this.totalBatchSize;
// let skipCount = 0; let skipCount = 0;
// let tasksExecuted = false; let tasksExecuted = false;
const runTaskArr = this.queues.map(() => 0); const runTaskArr = this.queues.map(() => 0);
for (let i = this.queues.length - 1; i >= 0; i--) { for (let i = this.queues.length - 1; i >= 0; i--) {
const queue = this.queues[i]; const queue = this.queues[i];
@@ -82,22 +82,22 @@ export class BatchTaskManager {
if (result !== false) { if (result !== false) {
totalBatchCount--; totalBatchCount--;
priorityBatchCount--; priorityBatchCount--;
// tasksExecuted = true; tasksExecuted = true;
runTaskArr[i] = (runTaskArr[i] ?? 0) + 1; runTaskArr[i] = (runTaskArr[i] ?? 0) + 1;
} }
} }
} }
// if (tasksExecuted) { if (tasksExecuted) {
// console.log( console.log(
// 'run task count', 'run task count',
// ...runTaskArr, ...runTaskArr,
// 'skip count', 'skip count',
// skipCount, skipCount,
// 'total task count', 'total task count',
// ...this.queues.map(arr => arr.size) ...this.queues.map(arr => arr.size)
// ); );
// } }
const hasRemainingTasks = this.queues.some(queue => !queue.isEmpty()); const hasRemainingTasks = this.queues.some(queue => !queue.isEmpty());
@@ -9,19 +9,18 @@ import type {
CellRenderProps, CellRenderProps,
DataViewCellLifeCycle, DataViewCellLifeCycle,
} from '../../../core/property/index.js'; } from '../../../core/property/index.js';
import type { SingleView } from '../../../core/view-manager/single-view.js';
import { import {
TableViewAreaSelection, TableViewAreaSelection,
type TableViewSelectionWithType, type TableViewSelectionWithType,
} from '../selection'; } from '../selection';
import type { TableProperty } from '../table-view-manager.js'; import type { TableProperty } from '../table-view-manager.js';
import type { TableGroup } from './group.js'; import type { TableGroup } from './group.js';
import type { TableViewUILogic } from './table-view-ui-logic.js';
export class DatabaseCellContainer extends SignalWatcher( export class TableViewCellContainer extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
) { ) {
static override styles = css` static override styles = css`
affine-database-cell-container { dv-table-view-cell-container {
display: flex; display: flex;
align-items: start; align-items: start;
width: 100%; width: 100%;
@@ -30,16 +29,16 @@ export class DatabaseCellContainer extends SignalWatcher(
outline: none; outline: none;
} }
affine-database-cell-container * { dv-table-view-cell-container * {
box-sizing: border-box; box-sizing: border-box;
} }
affine-database-cell-container uni-lit > *:first-child { dv-table-view-cell-container uni-lit > *:first-child {
padding: 6px; padding: 6px;
} }
`; `;
private readonly _cell = signal<DataViewCellLifeCycle>(); private readonly _cell$ = signal<DataViewCellLifeCycle>();
@property({ attribute: false }) @property({ attribute: false })
accessor column!: TableProperty; accessor column!: TableProperty;
@@ -55,7 +54,7 @@ export class DatabaseCellContainer extends SignalWatcher(
if (this.view.readonly$.value) { if (this.view.readonly$.value) {
return; return;
} }
const selectionView = this.selectionView; const selectionView = this.selectionController;
if (selectionView) { if (selectionView) {
const selection = selectionView.selection; const selection = selectionView.selection;
if (selection && this.isSelected(selection) && editing) { if (selection && this.isSelected(selection) && editing) {
@@ -81,20 +80,15 @@ export class DatabaseCellContainer extends SignalWatcher(
}; };
get cell(): DataViewCellLifeCycle | undefined { get cell(): DataViewCellLifeCycle | undefined {
return this._cell.value; return this._cell$.value;
} }
private get groupKey() { private get groupKey() {
return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key; return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key;
} }
private get selectionView() { private get selectionController() {
return this.closest('affine-database-table')?.selectionController; return this.tableViewLogic.selectionController;
}
get table() {
const table = this.closest('affine-database-table');
return table;
} }
override connectedCallback() { override connectedCallback() {
@@ -134,7 +128,7 @@ export class DatabaseCellContainer extends SignalWatcher(
}; };
return renderUniLit(view, props, { return renderUniLit(view, props, {
ref: this._cell, ref: this._cell$,
style: { style: {
display: 'contents', display: 'contents',
}, },
@@ -152,12 +146,16 @@ export class DatabaseCellContainer extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor rowIndex!: number; accessor rowIndex!: number;
get view() {
return this.tableViewLogic.view;
}
@property({ attribute: false }) @property({ attribute: false })
accessor view!: SingleView; accessor tableViewLogic!: TableViewUILogic;
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'affine-database-cell-container': DatabaseCellContainer; 'dv-table-view-cell-container': TableViewCellContainer;
} }
} }
@@ -9,7 +9,7 @@ import {
type TableViewSelection, type TableViewSelection,
type TableViewSelectionWithType, type TableViewSelectionWithType,
} from '../../selection'; } from '../../selection';
import type { DataViewTable } from '../table-view.js'; import type { TableViewUILogic } from '../table-view-ui-logic.js';
const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table'; const BLOCKSUITE_DATABASE_TABLE = 'blocksuite/database/table';
type JsonAreaData = string[][]; type JsonAreaData = string[][];
@@ -20,9 +20,7 @@ export class TableClipboardController implements ReactiveController {
tableSelection: TableViewSelectionWithType, tableSelection: TableViewSelectionWithType,
isCut = false isCut = false
) => { ) => {
const table = this.host; const area = getSelectedArea(tableSelection, this.logic);
const area = getSelectedArea(tableSelection, table);
if (!area) { if (!area) {
return; return;
} }
@@ -44,7 +42,7 @@ export class TableClipboardController implements ReactiveController {
} }
} }
if (deleteRows.length) { if (deleteRows.length) {
this.props.view.rowsDelete(deleteRows); this.logic.view.rowsDelete(deleteRows);
} }
} }
this.clipboard this.clipboard
@@ -79,12 +77,11 @@ export class TableClipboardController implements ReactiveController {
private readonly _onPaste = async (_context: UIEventStateContext) => { private readonly _onPaste = async (_context: UIEventStateContext) => {
const event = _context.get('clipboardState').raw; const event = _context.get('clipboardState').raw;
event.stopPropagation(); event.stopPropagation();
const view = this.host;
const clipboardData = event.clipboardData; const clipboardData = event.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selectionController.selection;
if (TableViewRowSelection.is(tableSelection)) { if (TableViewRowSelection.is(tableSelection)) {
return; return;
} }
@@ -97,7 +94,7 @@ export class TableClipboardController implements ReactiveController {
if (dataString) { if (dataString) {
// If internal format data exists, use it // If internal format data exists, use it
const jsonAreaData = JSON.parse(dataString) as JsonAreaData; const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
pasteToCells(view, jsonAreaData, tableSelection); pasteToCells(this.logic, jsonAreaData, tableSelection);
return true; return true;
} }
} catch { } catch {
@@ -115,7 +112,7 @@ export class TableClipboardController implements ReactiveController {
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows .filter(row => row.some(cell => cell !== '')); // Filter out empty rows
if (rows.length > 0) { if (rows.length > 0) {
pasteToCells(view, rows, tableSelection); pasteToCells(this.logic, rows, tableSelection);
} }
} }
} }
@@ -124,27 +121,29 @@ export class TableClipboardController implements ReactiveController {
}; };
private get clipboard() { private get clipboard() {
return this.props.clipboard; return this.logic.root.config.clipboard;
} }
private get notification() { private get notification() {
return this.props.notification; return this.logic.root.config.notification;
}
get props() {
return this.host.props;
} }
private get readonly() { private get readonly() {
return this.props.view.readonly$.value; return this.logic.view.readonly$.value;
} }
constructor(public host: DataViewTable) { constructor(public logic: TableViewUILogic) {}
host.addController(this);
get host() {
return this.logic.ui$.value;
}
get selectionController() {
return this.logic.selectionController;
} }
copy() { copy() {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selectionController.selection;
if (!tableSelection) { if (!tableSelection) {
return; return;
} }
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
} }
cut() { cut() {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selectionController.selection;
if (!tableSelection) { if (!tableSelection) {
return; return;
} }
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
} }
hostConnected() { hostConnected() {
this.host.disposables.add( this.host?.disposables.add(
this.props.handleEvent('copy', _ctx => { this.logic.handleEvent('copy', _ctx => {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selectionController.selection;
if (!tableSelection) return false; if (!tableSelection) return false;
this._onCopy(tableSelection); this._onCopy(tableSelection);
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
}) })
); );
this.host.disposables.add( this.host?.disposables.add(
this.props.handleEvent('cut', _ctx => { this.logic.handleEvent('cut', _ctx => {
const tableSelection = this.host.selectionController.selection; const tableSelection = this.selectionController.selection;
if (!tableSelection) return false; if (!tableSelection) return false;
this._onCut(tableSelection); this._onCut(tableSelection);
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
}) })
); );
this.host.disposables.add( this.host?.disposables.add(
this.props.handleEvent('paste', ctx => { this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false; if (this.readonly) return false;
this._onPaste(ctx).catch(console.error); this._onPaste(ctx).catch(console.error);
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
function getSelectedArea( function getSelectedArea(
selection: TableViewSelection, selection: TableViewSelection,
table: DataViewTable table: TableViewUILogic
): SelectedArea | undefined { ): SelectedArea | undefined {
const view = table.props.view; const view = table.view;
if (TableViewRowSelection.is(selection)) { if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rows(selection) const rows = TableViewRowSelection.rows(selection)
.map(row => { .map(row => {
@@ -283,7 +282,7 @@ function getTargetRangeFromSelection(
} }
function pasteToCells( function pasteToCells(
table: DataViewTable, table: TableViewUILogic,
rows: JsonAreaData, rows: JsonAreaData,
selection: TableViewAreaSelection selection: TableViewAreaSelection
) { ) {
@@ -8,7 +8,7 @@ import * as Y from 'yjs';
import { t } from '../../../../core/index.js'; import { t } from '../../../../core/index.js';
import type { TableViewAreaSelection } from '../../selection'; import type { TableViewAreaSelection } from '../../selection';
import type { DataViewTable } from '../table-view.js'; import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class DragToFillElement extends ShadowlessElement { export class DragToFillElement extends ShadowlessElement {
static override styles = css` static override styles = css`
@@ -49,12 +49,12 @@ export class DragToFillElement extends ShadowlessElement {
} }
export function fillSelectionWithFocusCellData( export function fillSelectionWithFocusCellData(
host: DataViewTable, logic: TableViewUILogic,
selection: TableViewAreaSelection selection: TableViewAreaSelection
) { ) {
const { groupKey, rowsSelection, columnsSelection, focus } = selection; const { groupKey, rowsSelection, columnsSelection, focus } = selection;
const focusCell = host.selectionController.getCellContainer( const focusCell = logic.selectionController.getCellContainer(
groupKey, groupKey,
focus.rowIndex, focus.rowIndex,
focus.columnIndex focus.columnIndex
@@ -78,7 +78,7 @@ export function fillSelectionWithFocusCellData(
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i++) {
if (i === focus.rowIndex) continue; if (i === focus.rowIndex) continue;
const cellContainer = host.selectionController.getCellContainer( const cellContainer = logic.selectionController.getCellContainer(
groupKey, groupKey,
i, i,
draggingColIdx draggingColIdx
@@ -5,7 +5,7 @@ import type { ReactiveController } from 'lit';
import { startDrag } from '../../../../core/utils/drag.js'; import { startDrag } from '../../../../core/utils/drag.js';
import { TableRowView } from '../row/row.js'; import { TableRowView } from '../row/row.js';
import type { DataViewTable } from '../table-view.js'; import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class TableDragController implements ReactiveController { export class TableDragController implements ReactiveController {
dragStart = (rowView: TableRowView, evt: PointerEvent) => { dragStart = (rowView: TableRowView, evt: PointerEvent) => {
@@ -32,8 +32,8 @@ export class TableDragController implements ReactiveController {
onDrag: () => undefined, onDrag: () => undefined,
onMove: evt => { onMove: evt => {
preview.display(evt.x - offsetLeft, evt.y - offsetTop); preview.display(evt.x - offsetLeft, evt.y - offsetTop);
if (!this.host.contains(evt.target as Node)) { if (!this.host?.contains(evt.target as Node)) {
const callback = this.host.props.onDrag; const callback = this.logic.root.config.onDrag;
if (callback) { if (callback) {
this.dropPreview.remove(); this.dropPreview.remove();
return { return {
@@ -66,7 +66,7 @@ export class TableDragController implements ReactiveController {
return; return;
} }
if (result.type === 'self') { if (result.type === 'self') {
const row = this.host.props.view.rowGetOrCreate(rowView.rowId); const row = this.logic.view.rowGetOrCreate(rowView.rowId);
row.move(result.position, fromGroup, result.groupKey); row.move(result.position, fromGroup, result.groupKey);
} }
}, },
@@ -88,9 +88,9 @@ export class TableDragController implements ReactiveController {
| undefined => { | undefined => {
const y = evt.y; const y = evt.y;
const tableRect = this.host const tableRect = this.host
.querySelector('affine-data-view-table-group') ?.querySelector('affine-data-view-table-group')
?.getBoundingClientRect(); ?.getBoundingClientRect();
const rows = this.host.querySelectorAll('data-view-table-row'); const rows = this.host?.querySelectorAll('data-view-table-row');
if (!rows || !tableRect || y < tableRect.top) { if (!rows || !tableRect || y < tableRect.top) {
return; return;
} }
@@ -124,21 +124,23 @@ export class TableDragController implements ReactiveController {
return position; return position;
}; };
constructor(private readonly host: DataViewTable) { constructor(private readonly logic: TableViewUILogic) {}
this.host.addController(this);
get host() {
return this.logic.ui$.value;
} }
hostConnected() { hostConnected() {
if (this.host.props.view.readonly$.value) { this.host?.disposables.add(
return; this.logic.handleEvent('dragStart', context => {
} if (this.logic.view.readonly$.value) {
this.host.disposables.add( return;
this.host.props.handleEvent('dragStart', context => { }
const event = context.get('pointerState').raw; const event = context.get('pointerState').raw;
const target = event.target; const target = event.target;
if ( if (
target instanceof Element && target instanceof Element &&
this.host.contains(target) && this.host?.contains(target) &&
target.closest('.data-view-table-view-drag-handler') target.closest('.data-view-table-view-drag-handler')
) { ) {
event.preventDefault(); event.preventDefault();
@@ -158,10 +160,9 @@ export class TableDragController implements ReactiveController {
const createDragPreview = (row: TableRowView, x: number, y: number) => { const createDragPreview = (row: TableRowView, x: number, y: number) => {
const div = document.createElement('div'); const div = document.createElement('div');
const cloneRow = new TableRowView(); const cloneRow = new TableRowView();
cloneRow.view = row.view;
cloneRow.rowIndex = row.rowIndex; cloneRow.rowIndex = row.rowIndex;
cloneRow.rowId = row.rowId; cloneRow.rowId = row.rowId;
cloneRow.dataViewEle = row.dataViewEle; cloneRow.tableViewLogic = row.tableViewLogic;
div.append(cloneRow); div.append(cloneRow);
div.className = 'with-data-view-css-variable'; div.className = 'with-data-view-css-variable';
div.style.width = `${row.getBoundingClientRect().width}px`; div.style.width = `${row.getBoundingClientRect().width}px`;
@@ -3,20 +3,22 @@ import type { ReactiveController } from 'lit';
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection'; import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
import { popRowMenu } from '../menu.js'; import { popRowMenu } from '../menu.js';
import type { DataViewTable } from '../table-view.js'; import type { TableViewUILogic } from '../table-view-ui-logic';
export class TableHotkeysController implements ReactiveController { export class TableHotkeysController implements ReactiveController {
get selectionController() { get selectionController() {
return this.host.selectionController; return this.logic.selectionController;
} }
constructor(private readonly host: DataViewTable) { constructor(private readonly logic: TableViewUILogic) {}
this.host.addController(this);
get host() {
return this.logic.ui$.value;
} }
hostConnected() { hostConnected() {
this.host.disposables.add( this.host?.disposables.add(
this.host.props.bindHotkey({ this.logic.bindHotkey({
Backspace: () => { Backspace: () => {
const selection = this.selectionController.selection; const selection = this.selectionController.selection;
if (!selection) { if (!selection) {
@@ -25,7 +27,7 @@ export class TableHotkeysController implements ReactiveController {
if (TableViewRowSelection.is(selection)) { if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rowsIds(selection); const rows = TableViewRowSelection.rowsIds(selection);
this.selectionController.selection = undefined; this.selectionController.selection = undefined;
this.host.props.view.rowsDelete(rows); this.logic.view.rowsDelete(rows);
return; return;
} }
const { const {
@@ -334,14 +336,14 @@ export class TableHotkeysController implements ReactiveController {
context.get('keyboardState').raw.preventDefault(); context.get('keyboardState').raw.preventDefault();
this.selectionController.selection = TableViewRowSelection.create({ this.selectionController.selection = TableViewRowSelection.create({
rows: rows:
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap( this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
group => group =>
group?.rows.map(row => ({ group?.rows.map(row => ({
groupKey: group.key, groupKey: group.key,
id: row.rowId, id: row.rowId,
})) ?? [] })) ?? []
) ?? ) ??
this.host.props.view.rows$.value.map(row => ({ this.logic.view.rows$.value.map(row => ({
groupKey: undefined, groupKey: undefined,
id: row.rowId, id: row.rowId,
})), })),
@@ -377,7 +379,7 @@ export class TableHotkeysController implements ReactiveController {
rows: [row], rows: [row],
}); });
popRowMenu( popRowMenu(
this.host.props.dataViewEle, this.logic,
popupTargetFromElement(cell), popupTargetFromElement(cell),
this.selectionController this.selectionController
); );
@@ -18,10 +18,10 @@ import {
type TableViewSelection, type TableViewSelection,
type TableViewSelectionWithType, type TableViewSelectionWithType,
} from '../../selection'; } from '../../selection';
import type { DatabaseCellContainer } from '../cell.js'; import type { TableViewCellContainer } from '../cell.js';
import type { TableGroup } from '../group.js'; import type { TableGroup } from '../group.js';
import type { TableRowView } from '../row/row.js'; import type { TableRowView } from '../row/row.js';
import type { DataViewTable } from '../table-view.js'; import type { TableViewUILogic } from '../table-view-ui-logic.js';
import { import {
DragToFillElement, DragToFillElement,
fillSelectionWithFocusCellData, fillSelectionWithFocusCellData,
@@ -87,41 +87,44 @@ export class TableSelectionController implements ReactiveController {
); );
const cell = container?.cell; const cell = container?.cell;
const isEditing = cell ? cell.beforeEnterEditMode() : true; const isEditing = cell ? cell.beforeEnterEditMode() : true;
this.host.props.setSelection({ this.logic.setSelection({
...selection, ...selection,
isEditing, isEditing,
}); });
} else { } else {
this.host.props.setSelection(selection); this.logic.setSelection(selection);
} }
} }
get tableContainer() { get tableContainer() {
return this.host.querySelector('.affine-database-table-container'); return this.logic.tableContainer$.value;
} }
get view() { get view() {
return this.host.props.view; return this.logic.view;
} }
get viewData() { get viewData() {
return this.view; return this.view;
} }
constructor(public host: DataViewTable) { constructor(public logic: TableViewUILogic) {
host.addController(this);
this.__selectionElement = new SelectionElement(); this.__selectionElement = new SelectionElement();
this.__selectionElement.controller = this; this.__selectionElement.controller = this;
} }
get host() {
return this.logic.ui$.value;
}
private clearSelection() { private clearSelection() {
this.host.props.setSelection(); this.logic.setSelection();
} }
private handleDragEvent() { private handleDragEvent() {
this.host.disposables.add( this.host?.disposables.add(
this.host.props.handleEvent('dragStart', context => { this.logic.handleEvent('dragStart', context => {
if (this.host.props.view.readonly$.value) { if (this.logic.view.readonly$.value) {
return; return;
} }
const event = context.get('pointerState').raw; const event = context.get('pointerState').raw;
@@ -152,8 +155,8 @@ export class TableSelectionController implements ReactiveController {
} }
private handleSelectionChange() { private handleSelectionChange() {
this.host.disposables.add( this.host?.disposables.add(
this.host.props.selection$.subscribe(tableSelection => { this.logic.selection$.subscribe(tableSelection => {
if (!this.isValidSelection(tableSelection)) { if (!this.isValidSelection(tableSelection)) {
this.selection = undefined; this.selection = undefined;
return; return;
@@ -238,7 +241,7 @@ export class TableSelectionController implements ReactiveController {
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows ? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
: this.view.rows$.value; : this.view.rows$.value;
requestAnimationFrame(() => { requestAnimationFrame(() => {
const index = this.host.props.view.properties$.value.findIndex( const index = this.logic.view.properties$.value.findIndex(
v => v.type$.value === 'title' v => v.type$.value === 'title'
); );
this.selection = TableViewAreaSelection.create({ this.selection = TableViewAreaSelection.create({
@@ -254,14 +257,14 @@ export class TableSelectionController implements ReactiveController {
private resolveDragStartTarget( private resolveDragStartTarget(
target: HTMLElement target: HTMLElement
): [cell: DatabaseCellContainer | null, fillValues: boolean] { ): [cell: TableViewCellContainer | null, fillValues: boolean] {
let cell: DatabaseCellContainer | null; let cell: TableViewCellContainer | null;
const fillValues = !!target.dataset.dragToFill; const fillValues = !!target.dataset.dragToFill;
if (fillValues) { if (fillValues) {
const focusCellContainer = this.getFocusCellContainer(); const focusCellContainer = this.getFocusCellContainer();
cell = focusCellContainer ?? null; cell = focusCellContainer ?? null;
} else { } else {
cell = target.closest('affine-database-cell-container'); cell = target.closest('dv-table-view-cell-container');
} }
return [cell, fillValues]; return [cell, fillValues];
} }
@@ -295,7 +298,7 @@ export class TableSelectionController implements ReactiveController {
const rows = this.rows(groupKey); const rows = this.rows(groupKey);
const cells = rows const cells = rows
?.item(0) ?.item(0)
.querySelectorAll('affine-database-cell-container'); .querySelectorAll('dv-table-view-cell-container');
return (x1: number, x2: number, y1: number, y2: number) => { return (x1: number, x2: number, y1: number, y2: number) => {
const rowOffsets: number[] = Array.from(rows ?? []).map( const rowOffsets: number[] = Array.from(rows ?? []).map(
@@ -394,7 +397,7 @@ export class TableSelectionController implements ReactiveController {
?.querySelectorAll('data-view-table-row') ?? [] ?.querySelectorAll('data-view-table-row') ?? []
); );
const cells = Array.from( const cells = Array.from(
row?.querySelectorAll('affine-database-cell-container') ?? [] row?.querySelectorAll('dv-table-view-cell-container') ?? []
); );
if (!row || !rows || !cells) { if (!row || !rows || !cells) {
return; return;
@@ -432,7 +435,7 @@ export class TableSelectionController implements ReactiveController {
} }
} }
rows[rowIndex] rows[rowIndex]
?.querySelectorAll('affine-database-cell-container') ?.querySelectorAll('dv-table-view-cell-container')
?.item(columnIndex) ?.item(columnIndex)
?.selectCurrentCell(false); ?.selectCurrentCell(false);
} }
@@ -441,10 +444,10 @@ export class TableSelectionController implements ReactiveController {
groupKey: string | undefined, groupKey: string | undefined,
rowIndex: number, rowIndex: number,
columnIndex: number columnIndex: number
): DatabaseCellContainer | undefined { ): TableViewCellContainer | undefined {
const row = this.rows(groupKey)?.item(rowIndex); const row = this.rows(groupKey)?.item(rowIndex);
return row return row
?.querySelectorAll('affine-database-cell-container') ?.querySelectorAll('dv-table-view-cell-container')
.item(columnIndex); .item(columnIndex);
} }
@@ -479,7 +482,7 @@ export class TableSelectionController implements ReactiveController {
if (!topRow || !bottomRow) { if (!topRow || !bottomRow) {
return; return;
} }
const topCells = topRow.querySelectorAll('affine-database-cell-container'); const topCells = topRow.querySelectorAll('dv-table-view-cell-container');
const leftCell = topCells.item(left); const leftCell = topCells.item(left);
const rightCell = topCells.item(right); const rightCell = topCells.item(right);
if (!leftCell || !rightCell) { if (!leftCell || !rightCell) {
@@ -752,7 +755,7 @@ export class TableSelectionController implements ReactiveController {
const max = const max =
(this.rows(newSelection.groupKey) (this.rows(newSelection.groupKey)
?.item(0) ?.item(0)
.querySelectorAll('affine-database-cell-container').length ?? 0) - 1; .querySelectorAll('dv-table-view-cell-container').length ?? 0) - 1;
newSelection.columnsSelection.end = Math.min( newSelection.columnsSelection.end = Math.min(
max, max,
newSelection.columnsSelection.end + 1 newSelection.columnsSelection.end + 1
@@ -810,7 +813,7 @@ export class TableSelectionController implements ReactiveController {
startDrag( startDrag(
evt: PointerEvent, evt: PointerEvent,
cell: DatabaseCellContainer, cell: TableViewCellContainer,
fillValues?: boolean fillValues?: boolean
) { ) {
const groupKey = cell.closest<TableGroup>('affine-data-view-table-group') const groupKey = cell.closest<TableGroup>('affine-data-view-table-group')
@@ -882,7 +885,7 @@ export class TableSelectionController implements ReactiveController {
if (fillValues && this.selection) { if (fillValues && this.selection) {
this.__dragToFillElement.dragging = false; this.__dragToFillElement.dragging = false;
fillSelectionWithFocusCellData( fillSelectionWithFocusCellData(
this.host, this.logic,
TableViewAreaSelection.create({ TableViewAreaSelection.create({
groupKey: groupKey, groupKey: groupKey,
rowsSelection: selection.row, rowsSelection: selection.row,
@@ -997,7 +1000,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
selectionRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>(); selectionRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
get selection$() { get selection$() {
return this.controller.host.props.selection$; return this.controller.logic.selection$;
} }
clearAreaStyle() { clearAreaStyle() {
@@ -1049,7 +1052,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
this.cancelSelectionUpdate(); this.cancelSelectionUpdate();
if ( if (
selection?.selectionType === 'area' && selection?.selectionType === 'area' &&
!this.controller.host.props.view.readonly$.value !this.controller.logic.view.readonly$.value
) { ) {
this.updateAreaSelectionStyle( this.updateAreaSelectionStyle(
selection.groupKey, selection.groupKey,
@@ -1,4 +1,4 @@
import { DatabaseCellContainer } from './cell.js'; import { TableViewCellContainer } from './cell.js';
import { DragToFillElement } from './controller/drag-to-fill.js'; import { DragToFillElement } from './controller/drag-to-fill.js';
import { SelectionElement } from './controller/selection.js'; import { SelectionElement } from './controller/selection.js';
import { TableGroup } from './group.js'; import { TableGroup } from './group.js';
@@ -9,15 +9,12 @@ import { DatabaseNumberFormatBar } from './header/number-format-bar.js';
import { TableVerticalIndicator } from './header/vertical-indicator.js'; import { TableVerticalIndicator } from './header/vertical-indicator.js';
import { TableRowView } from './row/row.js'; import { TableRowView } from './row/row.js';
import { RowSelectCheckbox } from './row/row-select-checkbox.js'; import { RowSelectCheckbox } from './row/row-select-checkbox.js';
import { DataViewTable } from './table-view.js'; import { TableViewUI } from './table-view-ui-logic.js';
export function pcEffects() { export function pcEffects() {
customElements.define('affine-database-table', DataViewTable); customElements.define('dv-table-view-ui', TableViewUI);
customElements.define('affine-data-view-table-group', TableGroup); customElements.define('affine-data-view-table-group', TableGroup);
customElements.define( customElements.define('dv-table-view-cell-container', TableViewCellContainer);
'affine-database-cell-container',
DatabaseCellContainer
);
customElements.define('affine-database-column-header', DatabaseColumnHeader); customElements.define('affine-database-column-header', DatabaseColumnHeader);
customElements.define( customElements.define(
'affine-data-view-column-preview', 'affine-data-view-column-preview',
@@ -12,7 +12,6 @@ import { css, html, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js'; import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js'; import type { Group } from '../../../core/group-by/trait.js';
import type { Row } from '../../../core/index.js'; import type { Row } from '../../../core/index.js';
@@ -21,10 +20,9 @@ import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js';
import { linearMove } from '../../../core/utils/wc-dnd/utils/linear-move.js'; import { linearMove } from '../../../core/utils/wc-dnd/utils/linear-move.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js'; import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import { TableViewAreaSelection } from '../selection'; import { TableViewAreaSelection } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import { DataViewColumnPreview } from './header/column-renderer.js'; import { DataViewColumnPreview } from './header/column-renderer.js';
import { getVerticalIndicator } from './header/vertical-indicator.js'; import { getVerticalIndicator } from './header/vertical-indicator.js';
import type { DataViewTable } from './table-view.js'; import type { TableViewUILogic } from './table-view-ui-logic.js';
const styles = css` const styles = css`
affine-data-view-table-group:hover .group-header-op { affine-data-view-table-group:hover .group-header-op {
@@ -71,7 +69,7 @@ export class TableGroup extends SignalWatcher(
private readonly clickAddRow = () => { private readonly clickAddRow = () => {
this.view.rowAdd('end', this.group?.key); this.view.rowAdd('end', this.group?.key);
const selectionController = this.viewEle.selectionController; const selectionController = this.tableViewLogic.selectionController;
selectionController.selection = undefined; selectionController.selection = undefined;
requestAnimationFrame(() => { requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex( const index = this.view.properties$.value.findIndex(
@@ -90,7 +88,7 @@ export class TableGroup extends SignalWatcher(
private readonly clickAddRowInStart = () => { private readonly clickAddRowInStart = () => {
this.view.rowAdd('start', this.group?.key); this.view.rowAdd('start', this.group?.key);
const selectionController = this.viewEle.selectionController; const selectionController = this.tableViewLogic.selectionController;
selectionController.selection = undefined; selectionController.selection = undefined;
requestAnimationFrame(() => { requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex( const index = this.view.properties$.value.findIndex(
@@ -152,9 +150,6 @@ export class TableGroup extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor group: Group | undefined = undefined; accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor view!: TableSingleView;
dndContext = createDndContext({ dndContext = createDndContext({
activators: defaultActivators, activators: defaultActivators,
container: this, container: this,
@@ -187,6 +182,7 @@ export class TableGroup extends SignalWatcher(
preview.column = column; preview.column = column;
preview.group = this.group; preview.group = this.group;
preview.container = this; preview.container = this;
preview.tableViewLogic = this.tableViewLogic;
preview.style.position = 'absolute'; preview.style.position = 'absolute';
preview.style.zIndex = '999'; preview.style.zIndex = '999';
const scale = this.dndContext.scale$.value; const scale = this.dndContext.scale$.value;
@@ -246,7 +242,7 @@ export class TableGroup extends SignalWatcher(
return html` return html`
<affine-database-column-header <affine-database-column-header
.renderGroupHeader="${this.renderGroupHeader}" .renderGroupHeader="${this.renderGroupHeader}"
.tableViewManager="${this.view}" .tableViewLogic="${this.tableViewLogic}"
></affine-database-column-header> ></affine-database-column-header>
<div class="affine-database-block-rows"> <div class="affine-database-block-rows">
${repeat( ${repeat(
@@ -256,8 +252,7 @@ export class TableGroup extends SignalWatcher(
return html` <data-view-table-row return html` <data-view-table-row
data-row-index="${idx}" data-row-index="${idx}"
data-row-id="${row.rowId}" data-row-id="${row.rowId}"
.dataViewEle="${this.dataViewEle}" .tableViewLogic="${this.tableViewLogic}"
.view="${this.view}"
.rowId="${row.rowId}" .rowId="${row.rowId}"
.rowIndex="${idx}" .rowIndex="${idx}"
></data-view-table-row>`; ></data-view-table-row>`;
@@ -278,7 +273,10 @@ export class TableGroup extends SignalWatcher(
${PlusIcon()}<span style="font-size: 12px">New Record</span> ${PlusIcon()}<span style="font-size: 12px">New Record</span>
</div> </div>
</div>`} </div>`}
<affine-database-column-stats .view="${this.view}" .group="${this.group}"> <affine-database-column-stats
.tableViewLogic="${this.tableViewLogic}"
.group="${this.group}"
>
</affine-database-column-stats> </affine-database-column-stats>
`; `;
} }
@@ -292,14 +290,15 @@ export class TableGroup extends SignalWatcher(
return this.renderRows(this.rows); return this.renderRows(this.rows);
} }
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@query('.affine-database-block-rows') @query('.affine-database-block-rows')
accessor rowsContainer: HTMLElement | null = null; accessor rowsContainer: HTMLElement | null = null;
@property({ attribute: false }) @property({ attribute: false })
accessor viewEle!: DataViewTable; accessor tableViewLogic!: TableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -9,8 +9,10 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js'; import { cellDivider } from '../../styles.js';
import type { TableGroup } from '../group.js'; import type { TableGroup } from '../group.js';
import { tableStyle } from '../table-view-style';
import { type TableViewUILogic } from '../table-view-ui-logic.js';
import { styles } from './styles.js'; import { styles } from './styles.js';
export class DatabaseColumnHeader extends SignalWatcher( export class DatabaseColumnHeader extends SignalWatcher(
@@ -31,7 +33,6 @@ export class DatabaseColumnHeader extends SignalWatcher(
editLastColumnTitle = () => { editLastColumnTitle = () => {
const columns = this.querySelectorAll('affine-database-header-column'); const columns = this.querySelectorAll('affine-database-header-column');
const column = columns.item(columns.length - 1); const column = columns.item(columns.length - 1);
column.scrollIntoView({ block: 'nearest', inline: 'nearest' });
column.editTitle(); column.editTitle();
}; };
@@ -88,7 +89,7 @@ export class DatabaseColumnHeader extends SignalWatcher(
<div class="affine-database-column-header database-row"> <div class="affine-database-column-header database-row">
${this.readonly ${this.readonly
? nothing ? nothing
: html`<div class="data-view-table-left-bar"></div>`} : html`<div class="${tableStyle.leftToolBarStyle}"></div>`}
${repeat( ${repeat(
this.tableViewManager.properties$.value, this.tableViewManager.properties$.value,
column => column.id, column => column.id,
@@ -104,9 +105,9 @@ export class DatabaseColumnHeader extends SignalWatcher(
data-column-index="${index}" data-column-index="${index}"
class="affine-database-column database-cell" class="affine-database-column database-cell"
.column="${column}" .column="${column}"
.tableViewManager="${this.tableViewManager}" .tableViewLogic="${this.tableViewLogic}"
></affine-database-header-column> ></affine-database-header-column>
<div class="cell-divider" style="height: auto;"></div> <div class="${cellDivider}" style="height: auto;"></div>
`; `;
} }
)} )}
@@ -128,7 +129,11 @@ export class DatabaseColumnHeader extends SignalWatcher(
accessor scaleDiv!: HTMLDivElement; accessor scaleDiv!: HTMLDivElement;
@property({ attribute: false }) @property({ attribute: false })
accessor tableViewManager!: TableSingleView; accessor tableViewLogic!: TableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -13,6 +13,7 @@ import type {
TableProperty, TableProperty,
TableSingleView, TableSingleView,
} from '../../table-view-manager.js'; } from '../../table-view-manager.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class DataViewColumnPreview extends SignalWatcher( export class DataViewColumnPreview extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
`; `;
get tableViewManager(): TableSingleView { get tableViewManager(): TableSingleView {
return this.column.view as TableSingleView; return this.tableViewLogic.view;
} }
private renderGroup(rows: Row[]) { private renderGroup(rows: Row[]) {
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
)};box-shadow: var(--affine-shadow-2);" )};box-shadow: var(--affine-shadow-2);"
> >
<affine-database-header-column <affine-database-header-column
.tableViewManager="${this.tableViewManager}" .tableViewLogic="${this.tableViewLogic}"
.column="${this.column}" .column="${this.column}"
></affine-database-header-column> ></affine-database-header-column>
${repeat(rows, (id, index) => { ${repeat(rows, (id, index) => {
const height = this.container.querySelector( const height = this.container.querySelector(
`affine-database-cell-container[data-row-id="${id}"]` `dv-table-view-cell-container[data-row-id="${id}"]`
)?.clientHeight; )?.clientHeight;
const style = styleMap({ const style = styleMap({
height: height + 'px', height: height + 'px',
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
)}" )}"
> >
<div style="${style}"> <div style="${style}">
<affine-database-cell-container <dv-table-view-cell-container
.column="${this.column}" .column="${this.column}"
.view="${this.tableViewManager}" .tableViewLogic="${this.tableViewLogic}"
.rowId="${id}" .rowId="${id}"
.columnId="${this.column.id}" .columnId="${this.column.id}"
.rowIndex="${index}" .rowIndex="${index}"
.columnIndex="${columnIndex}" .columnIndex="${columnIndex}"
></affine-database-cell-container> ></dv-table-view-cell-container>
</div> </div>
</div>`; </div>`;
})} })}
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor group: Group | undefined = undefined; accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor tableViewLogic!: TableViewUILogic;
} }
declare global { declare global {
@@ -45,10 +45,8 @@ import {
ShowQuickSettingBarKey, ShowQuickSettingBarKey,
} from '../../../../widget-presets/quick-setting-bar/context.js'; } from '../../../../widget-presets/quick-setting-bar/context.js';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js';
import type { import type { TableProperty } from '../../table-view-manager.js';
TableProperty, import type { TableViewUILogic } from '../table-view-ui-logic.js';
TableSingleView,
} from '../../table-view-manager.js';
import { import {
getTableGroupRect, getTableGroupRect,
getVerticalIndicator, getVerticalIndicator,
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
const sortUtils = createSortUtils( const sortUtils = createSortUtils(
sortTrait, sortTrait,
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {}) this.tableViewLogic.eventTrace
); );
const sortList = sortUtils.sortList$.value; const sortList = sortUtils.sortList$.value;
const existingIndex = sortList.findIndex( const existingIndex = sortList.findIndex(
@@ -398,10 +396,10 @@ export class DatabaseHeaderColumn extends SignalWatcher(
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
const table = this.closest('affine-database-table'); const table = this.closest('dv-table-view-ui');
if (table) { if (table) {
this.disposables.add( this.disposables.add(
table.props.handleEvent('dragStart', context => { table.logic.handleEvent('dragStart', context => {
if (this.tableViewManager.readonly$.value) { if (this.tableViewManager.readonly$.value) {
return; return;
} }
@@ -481,7 +479,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd'; accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
@property({ attribute: false }) @property({ attribute: false })
accessor tableViewManager!: TableSingleView; accessor tableViewLogic!: TableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
} }
function numberFormatConfig(column: Property): MenuConfig { function numberFormatConfig(column: Property): MenuConfig {
@@ -12,19 +12,19 @@ import {
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { html } from 'lit'; import { html } from 'lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { TableViewRowSelection } from '../selection'; import { TableViewRowSelection } from '../selection';
import type { TableSelectionController } from './controller/selection.js'; import type { TableSelectionController } from './controller/selection.js';
import type { TableViewUILogic } from './table-view-ui-logic.js';
export const openDetail = ( export const openDetail = (
dataViewEle: DataViewRenderer, tableViewLogic: TableViewUILogic,
rowId: string, rowId: string,
selection: TableSelectionController selection: TableSelectionController
) => { ) => {
const old = selection.selection; const old = selection.selection;
selection.selection = undefined; selection.selection = undefined;
dataViewEle.openDetailPanel({ tableViewLogic.root.openDetailPanel({
view: selection.host.props.view, view: selection.logic.view,
rowId: rowId, rowId: rowId,
onClose: () => { onClose: () => {
selection.selection = old; selection.selection = old;
@@ -33,7 +33,7 @@ export const openDetail = (
}; };
export const popRowMenu = ( export const popRowMenu = (
dataViewEle: DataViewRenderer, tableViewLogic: TableViewUILogic,
ele: PopupTarget, ele: PopupTarget,
selectionController: TableSelectionController selectionController: TableSelectionController
) => { ) => {
@@ -55,7 +55,7 @@ export const popRowMenu = (
${CopyIcon()} ${CopyIcon()}
</div>`, </div>`,
select: () => { select: () => {
selectionController.host.clipboardController.copy(); selectionController.logic.clipboardController.copy();
}, },
}), }),
], ],
@@ -85,7 +85,7 @@ export const popRowMenu = (
name: 'Expand Row', name: 'Expand Row',
prefix: ExpandFullIcon(), prefix: ExpandFullIcon(),
select: () => { select: () => {
openDetail(dataViewEle, row.id, selectionController); openDetail(tableViewLogic, row.id, selectionController);
}, },
}), }),
menu.group({ menu.group({
@@ -1,15 +1,13 @@
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { CheckBoxCheckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit'; import { CheckBoxCheckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std'; import { ShadowlessElement } from '@blocksuite/std';
import { computed, type ReadonlySignal } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { css, html } from 'lit'; import { css, html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { import { TableViewRowSelection } from '../../selection';
TableViewRowSelection, import type { TableViewUILogic } from '../table-view-ui-logic';
type TableViewSelectionWithType,
} from '../../selection';
export class RowSelectCheckbox extends SignalWatcher( export class RowSelectCheckbox extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
@@ -42,10 +40,10 @@ export class RowSelectCheckbox extends SignalWatcher(
accessor rowId!: string; accessor rowId!: string;
@property({ attribute: false }) @property({ attribute: false })
accessor selection!: ReadonlySignal<TableViewSelectionWithType | undefined>; accessor tableViewLogic!: TableViewUILogic;
isSelected$ = computed(() => { isSelected$ = computed(() => {
const selection = this.selection.value; const selection = this.tableViewLogic.selection$.value;
if (!selection || selection.selectionType !== 'row') { if (!selection || selection.selectionType !== 'row') {
return false; return false;
} }
@@ -58,7 +56,7 @@ export class RowSelectCheckbox extends SignalWatcher(
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.disposables.addFromEvent(this, 'click', () => { this.disposables.addFromEvent(this, 'click', () => {
this.closest('affine-database-table')?.selectionController.toggleRow( this.tableViewLogic.selectionController.toggleRow(
this.rowId, this.rowId,
this.groupKey this.groupKey
); );
@@ -9,14 +9,14 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js'; import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../../core/data-view.js';
import { import {
TableViewRowSelection, TableViewRowSelection,
type TableViewSelection, type TableViewSelection,
} from '../../selection'; } from '../../selection';
import type { TableSingleView } from '../../table-view-manager.js'; import { cellDivider } from '../../styles';
import type { TableGroup } from '../group.js'; import type { TableGroup } from '../group.js';
import { openDetail, popRowMenu } from '../menu.js'; import { openDetail, popRowMenu } from '../menu.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class TableRowView extends SignalWatcher( export class TableRowView extends SignalWatcher(
WithDisposable(ShadowlessElement) WithDisposable(ShadowlessElement)
@@ -138,7 +138,7 @@ export class TableRowView extends SignalWatcher(
} }
e.preventDefault(); e.preventDefault();
const ele = e.target as HTMLElement; const ele = e.target as HTMLElement;
const cell = ele.closest('affine-database-cell-container'); const cell = ele.closest('dv-table-view-cell-container');
const row = { id: this.rowId, groupKey: this.groupKey }; const row = { id: this.rowId, groupKey: this.groupKey };
if (!TableViewRowSelection.includes(selection.selection, row)) { if (!TableViewRowSelection.includes(selection.selection, row)) {
selection.selection = TableViewRowSelection.create({ selection.selection = TableViewRowSelection.create({
@@ -150,7 +150,7 @@ export class TableRowView extends SignalWatcher(
(e.target as HTMLElement).closest('.database-cell') ?? // for last add btn cell (e.target as HTMLElement).closest('.database-cell') ?? // for last add btn cell
(e.target as HTMLElement); (e.target as HTMLElement);
popRowMenu(this.dataViewEle, popupTargetFromElement(target), selection); popRowMenu(this.tableViewLogic, popupTargetFromElement(target), selection);
}; };
setSelection = (selection?: TableViewSelection) => { setSelection = (selection?: TableViewSelection) => {
@@ -164,7 +164,7 @@ export class TableRowView extends SignalWatcher(
} }
get selectionController() { get selectionController() {
return this.closest('affine-database-table')?.selectionController; return this.tableViewLogic.selectionController;
} }
override connectedCallback() { override connectedCallback() {
@@ -193,7 +193,7 @@ export class TableRowView extends SignalWatcher(
></div> ></div>
</div> </div>
<row-select-checkbox <row-select-checkbox
.selection="${this.dataViewEle.config.selection$}" .tableViewLogic="${this.tableViewLogic}"
.rowId="${this.rowId}" .rowId="${this.rowId}"
.groupKey="${this.groupKey}" .groupKey="${this.groupKey}"
></row-select-checkbox> ></row-select-checkbox>
@@ -212,7 +212,11 @@ export class TableRowView extends SignalWatcher(
rows: [{ id: this.rowId, groupKey: this.groupKey }], rows: [{ id: this.rowId, groupKey: this.groupKey }],
}) })
); );
openDetail(this.dataViewEle, this.rowId, this.selectionController); openDetail(
this.tableViewLogic,
this.rowId,
this.selectionController
);
}; };
const openMenu = (e: MouseEvent) => { const openMenu = (e: MouseEvent) => {
if (!this.selectionController) { if (!this.selectionController) {
@@ -234,20 +238,20 @@ export class TableRowView extends SignalWatcher(
); );
} }
popRowMenu( popRowMenu(
this.dataViewEle, this.tableViewLogic,
popupTargetFromElement(ele), popupTargetFromElement(ele),
this.selectionController this.selectionController
); );
}; };
return html` return html`
<div style="display: flex;"> <div style="display: flex;">
<affine-database-cell-container <dv-table-view-cell-container
class="database-cell" class="database-cell"
style=${styleMap({ style=${styleMap({
width: `${column.width$.value}px`, width: `${column.width$.value}px`,
border: i === 0 ? 'none' : undefined, border: i === 0 ? 'none' : undefined,
})} })}
.view="${view}" .tableViewLogic="${this.tableViewLogic}"
.column="${column}" .column="${column}"
.rowId="${this.rowId}" .rowId="${this.rowId}"
data-row-id="${this.rowId}" data-row-id="${this.rowId}"
@@ -258,8 +262,8 @@ export class TableRowView extends SignalWatcher(
.columnIndex="${i}" .columnIndex="${i}"
data-column-index="${i}" data-column-index="${i}"
> >
</affine-database-cell-container> </dv-table-view-cell-container>
<div class="cell-divider"></div> <div class="${cellDivider}"></div>
</div> </div>
${!column.readonly$.value && ${!column.readonly$.value &&
column.view.mainProperties$.value.titleColumn === column.id column.view.mainProperties$.value.titleColumn === column.id
@@ -282,7 +286,7 @@ export class TableRowView extends SignalWatcher(
} }
@property({ attribute: false }) @property({ attribute: false })
accessor dataViewEle!: DataViewRenderer; accessor tableViewLogic!: TableViewUILogic;
@property({ attribute: false }) @property({ attribute: false })
accessor rowId!: string; accessor rowId!: string;
@@ -290,8 +294,9 @@ export class TableRowView extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor rowIndex!: number; accessor rowIndex!: number;
@property({ attribute: false }) get view() {
accessor view!: TableSingleView; return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -0,0 +1,112 @@
import { css } from '@emotion/css';
import { LEFT_TOOL_BAR_WIDTH } from '../consts';
export const tableViewStyle = css({
position: 'relative',
display: 'flex',
flexDirection: 'column',
'& *': {
boxSizing: 'border-box',
},
});
export const tableWrapperStyle = css({
overflowY: 'auto',
});
export const tableScrollContainerStyle = css({
position: 'relative',
width: '100%',
paddingBottom: '4px',
zIndex: 1,
overflowX: 'scroll',
overflowY: 'hidden',
'&:hover': {
paddingBottom: '0px',
},
'&::-webkit-scrollbar': {
WebkitAppearance: 'none',
display: 'block',
},
'&::-webkit-scrollbar:horizontal': {
height: '4px',
},
'&::-webkit-scrollbar-thumb': {
borderRadius: '2px',
backgroundColor: 'transparent',
},
'&:hover::-webkit-scrollbar:horizontal': {
height: '8px',
},
'&:hover::-webkit-scrollbar-thumb': {
borderRadius: '16px',
backgroundColor: 'var(--affine-black-30)',
},
'&:hover::-webkit-scrollbar-track': {
backgroundColor: 'var(--affine-hover-color)',
},
'.affine-database-table-container': {
position: 'relative',
width: 'fit-content',
minWidth: '100%',
},
});
export const tableGroupsContainerStyle = css({
display: 'flex',
flexDirection: 'column',
gap: '16px',
});
export const addGroupStyle = css({
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '6px 12px 6px 8px',
color: 'var(--affine-text-secondary-color)',
fontSize: '12px',
lineHeight: '20px',
position: 'sticky',
left: `${LEFT_TOOL_BAR_WIDTH}px`,
borderRadius: '8px',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'var(--affine-hover-color)',
},
});
export const addGroupIconStyle = css({
display: 'flex',
width: '16px',
height: '16px',
'& svg': {
width: '16px',
height: '16px',
fill: 'var(--affine-icon-color)',
},
});
const cellDividerStyle = css({
width: '1px',
height: '100%',
backgroundColor: 'var(--affine-border-color)',
});
const leftToolBarStyle = css({
display: 'flex',
alignItems: 'center',
position: 'sticky',
zIndex: 1,
left: 0,
width: `${LEFT_TOOL_BAR_WIDTH}px`,
flexShrink: 0,
});
export const tableStyle = {
leftToolBarStyle,
cellDividerStyle,
};
@@ -0,0 +1,220 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { 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';
import type { GroupTrait } from '../../../core/group-by/trait.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import { TableClipboardController } from './controller/clipboard.js';
import { TableDragController } from './controller/drag.js';
import { TableHotkeysController } from './controller/hotkeys.js';
import { TableSelectionController } from './controller/selection.js';
import {
addGroupIconStyle,
addGroupStyle,
tableGroupsContainerStyle,
tableScrollContainerStyle,
tableViewStyle,
tableWrapperStyle,
} from './table-view-style';
export class TableViewUILogic extends DataViewUILogicBase<
TableSingleView,
TableViewSelectionWithType
> {
ui$ = signal<TableViewUI>();
scrollContainer$ = signal<HTMLDivElement>();
tableContainer$ = signal<HTMLDivElement>();
clipboardController = new TableClipboardController(this);
dragController = new TableDragController(this);
hotkeysController = new TableHotkeysController(this);
selectionController = new TableSelectionController(this);
private get readonly() {
return this.view.readonly$.value;
}
clearSelection = () => {
this.selectionController.clear();
};
addRow = (position: InsertToPosition) => {
if (this.readonly) return;
const rowId = this.view.rowAdd(position);
if (rowId) {
this.root.openDetailPanel({
view: this.view,
rowId,
});
}
return rowId;
};
focusFirstCell = () => {
this.selectionController.focusFirstCell();
};
showIndicator = (evt: MouseEvent) => {
return this.dragController.showIndicator(evt) != null;
};
hideIndicator = () => {
this.dragController.dropPreview.remove();
};
moveTo = (id: string, evt: MouseEvent) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
};
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
return;
}
const ele = event.currentTarget;
if (ele instanceof HTMLElement) {
if (ele.scrollWidth === ele.clientWidth) {
return;
}
event.stopPropagation();
}
};
renderAddGroup = (groupHelper: GroupTrait) => {
const addGroup = groupHelper.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = groupHelper.property$.value;
if (column) {
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
}),
],
},
});
};
return html` <div style="display:flex;">
<div class="${addGroupStyle}" @click="${add}">
<div class="${addGroupIconStyle}">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
renderer = createUniComponentFromWebComponent(TableViewUI);
}
export class TableViewUI extends DataViewUIBase<TableViewUILogic> {
override connectedCallback(): void {
super.connectedCallback();
this.logic.ui$.value = this;
this.logic.clipboardController.hostConnected();
this.logic.dragController.hostConnected();
this.logic.hotkeysController.hostConnected();
this.logic.selectionController.hostConnected();
this.classList.add('affine-database-table', tableViewStyle);
this.dataset['testid'] = 'dv-table-view';
}
private renderTable() {
const groups = this.logic.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div class="${tableGroupsContainerStyle}">
${repeat(
groups,
v => v.key,
group => {
return html` <affine-data-view-table-group
data-group-key="${group.key}"
.tableViewLogic="${this.logic}"
.group="${group}"
></affine-data-view-table-group>`;
}
)}
${this.logic.renderAddGroup(this.logic.view.groupTrait)}
</div>
`;
}
return html` <affine-data-view-table-group
.tableViewLogic="${this.logic}"
></affine-data-view-table-group>`;
}
override render(): TemplateResult {
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${this.logic.headerWidget
? renderUniLit(this.logic.headerWidget, {
dataViewLogic: this.logic,
})
: ''}
<div class="${tableWrapperStyle}" style="${wrapperStyle}">
<div
${ref(this.logic.scrollContainer$)}
class="${tableScrollContainerStyle}"
@wheel="${this.logic.onWheel}"
>
<div
${ref(this.logic.tableContainer$)}
class="affine-database-table-container"
style="${containerStyle}"
>
${this.renderTable()}
</div>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'dv-table-view-ui': TableViewUI;
}
}
@@ -1,306 +0,0 @@
import {
menu,
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { cssVarV2 } from '@toeverything/theme/v2';
import { css, unsafeCSS } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { GroupTrait } from '../../../core/group-by/trait.js';
import type { DataViewInstance } from '../../../core/index.js';
import { renderUniLit } from '../../../core/utils/uni-component/uni-component.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { TableViewSelectionWithType } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import { TableClipboardController } from './controller/clipboard.js';
import { TableDragController } from './controller/drag.js';
import { TableHotkeysController } from './controller/hotkeys.js';
import { TableSelectionController } from './controller/selection.js';
const styles = css`
affine-database-table {
position: relative;
display: flex;
flex-direction: column;
}
affine-database-table * {
box-sizing: border-box;
}
.affine-database-table {
overflow-y: auto;
}
.affine-database-block-title-container {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
margin: 2px 0 2px;
}
.affine-database-block-table {
position: relative;
width: 100%;
padding-bottom: 4px;
z-index: 1;
overflow-x: scroll;
overflow-y: hidden;
}
.affine-database-block-table:hover {
padding-bottom: 0px;
}
.affine-database-block-table::-webkit-scrollbar {
-webkit-appearance: none;
display: block;
}
.affine-database-block-table::-webkit-scrollbar:horizontal {
height: 4px;
}
.affine-database-block-table::-webkit-scrollbar-thumb {
border-radius: 2px;
background-color: transparent;
}
.affine-database-block-table:hover::-webkit-scrollbar:horizontal {
height: 8px;
}
.affine-database-block-table:hover::-webkit-scrollbar-thumb {
border-radius: 16px;
background-color: var(--affine-black-30);
}
.affine-database-block-table:hover::-webkit-scrollbar-track {
background-color: var(--affine-hover-color);
}
.affine-database-table-container {
position: relative;
width: fit-content;
min-width: 100%;
}
.affine-database-block-tag-circle {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.affine-database-block-tag {
display: inline-flex;
border-radius: 11px;
align-items: center;
padding: 0 8px;
cursor: pointer;
}
.cell-divider {
width: 1px;
height: 100%;
background-color: ${unsafeCSS(cssVarV2.layer.insideBorder.border)};
}
.data-view-table-left-bar {
display: flex;
align-items: center;
position: sticky;
z-index: 1;
left: 0;
width: ${LEFT_TOOL_BAR_WIDTH}px;
flex-shrink: 0;
}
.affine-database-block-rows {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
`;
export class DataViewTable extends DataViewBase<
TableSingleView,
TableViewSelectionWithType
> {
static override styles = styles;
clipboardController = new TableClipboardController(this);
dragController = new TableDragController(this);
hotkeysController = new TableHotkeysController(this);
onWheel = (event: WheelEvent) => {
if (event.metaKey || event.ctrlKey) {
return;
}
const ele = event.currentTarget;
if (ele instanceof HTMLElement) {
if (ele.scrollWidth === ele.clientWidth) {
return;
}
event.stopPropagation();
}
};
renderAddGroup = (groupHelper: GroupTrait) => {
const addGroup = groupHelper.addGroup;
if (!addGroup) {
return;
}
const add = (e: MouseEvent) => {
const ele = e.currentTarget as HTMLElement;
popMenu(popupTargetFromElement(ele), {
options: {
items: [
menu.input({
onComplete: text => {
const column = groupHelper.property$.value;
if (column) {
column.dataUpdate(
() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.props.view.manager.dataSource,
}) as never
);
}
},
}),
],
},
});
};
return html` <div style="display:flex;">
<div
class="dv-hover dv-round-8"
style="display:flex;align-items:center;gap: 10px;padding: 6px 12px 6px 8px;color: var(--affine-text-secondary-color);font-size: 12px;line-height: 20px;position: sticky;left: ${LEFT_TOOL_BAR_WIDTH}px;"
@click="${add}"
>
<div class="dv-icon-16" style="display:flex;">${AddCursorIcon()}</div>
<div>New Group</div>
</div>
</div>`;
};
selectionController = new TableSelectionController(this);
get expose(): DataViewInstance {
return {
clearSelection: () => {
this.selectionController.clear();
},
addRow: position => {
if (this.readonly) return;
const rowId = this.props.view.rowAdd(position);
if (rowId) {
this.props.dataViewEle.openDetailPanel({
view: this.props.view,
rowId,
});
}
return rowId;
},
focusFirstCell: () => {
this.selectionController.focusFirstCell();
},
showIndicator: evt => {
return this.dragController.showIndicator(evt) != null;
},
hideIndicator: () => {
this.dragController.dropPreview.remove();
},
moveTo: (id, evt) => {
const result = this.dragController.getInsertPosition(evt);
if (result) {
const row = this.props.view.rowGetOrCreate(id);
row.move(result.position, undefined, result.groupKey);
}
},
getSelection: () => {
return this.selectionController.selection;
},
view: this.props.view,
eventTrace: this.props.eventTrace,
};
}
private get readonly() {
return this.props.view.readonly$.value;
}
private renderTable() {
const groups = this.props.view.groupTrait.groupsDataList$.value;
if (groups) {
return html`
<div style="display:flex;flex-direction: column;gap: 16px;">
${repeat(
groups,
v => v.key,
group => {
return html` <affine-data-view-table-group
data-group-key="${group.key}"
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
.group="${group}"
></affine-data-view-table-group>`;
}
)}
${this.renderAddGroup(this.props.view.groupTrait)}
</div>
`;
}
return html` <affine-data-view-table-group
.dataViewEle="${this.props.dataViewEle}"
.view="${this.props.view}"
.viewEle="${this}"
></affine-data-view-table-group>`;
}
override render() {
const vPadding = this.props.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
});
const containerStyle = styleMap({
paddingLeft: `${vPadding}px`,
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
})}
<div class="affine-database-table" style="${wrapperStyle}">
<div class="affine-database-block-table" @wheel="${this.onWheel}">
<div
class="affine-database-table-container"
style="${containerStyle}"
>
${this.renderTable()}
</div>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-table': DataViewTable;
}
}
@@ -1,14 +1,19 @@
import './pc/effect.js';
import './pc-virtual/effect.js'; import './pc-virtual/effect.js';
import './pc/effect.js';
import { createUniComponentFromWebComponent } from '../../core/utils/uni-component/uni-component.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import { tableViewModel } from './define.js'; import { tableViewModel } from './define.js';
import { MobileDataViewTable } from './mobile/table-view.js'; import { MobileTableViewUILogic } from './mobile/table-view-ui-logic.js';
import { TableViewSelector } from './table-view-selector.js'; import { TableViewUILogic } from './pc/table-view-ui-logic.js';
import { VirtualTableViewUILogic } from './pc-virtual/table-view-ui-logic';
export const tableViewMeta = tableViewModel.createMeta({ export const tableViewMeta = tableViewModel.createMeta({
view: createUniComponentFromWebComponent(TableViewSelector),
mobileView: createUniComponentFromWebComponent(MobileDataViewTable),
icon: createIcon('DatabaseTableViewIcon'), icon: createIcon('DatabaseTableViewIcon'),
pcLogic: view =>
// @ts-expect-error fixme: typesafe
view.manager.dataSource.featureFlags$.value.enable_table_virtual_scroll
? VirtualTableViewUILogic
: TableViewUILogic,
// @ts-expect-error fixme: typesafe
mobileLogic: () => MobileTableViewUILogic,
}); });
@@ -6,7 +6,7 @@ import { repeat } from 'lit/directives/repeat.js';
import type { Group } from '../../../core/group-by/trait.js'; import type { Group } from '../../../core/group-by/trait.js';
import { LEFT_TOOL_BAR_WIDTH, STATS_BAR_HEIGHT } from '../consts.js'; import { LEFT_TOOL_BAR_WIDTH, STATS_BAR_HEIGHT } from '../consts.js';
import type { TableSingleView } from '../table-view-manager.js'; import type { TableViewUILogic } from '../pc/table-view-ui-logic.js';
const styles = css` const styles = css`
affine-database-column-stats { affine-database-column-stats {
@@ -41,7 +41,11 @@ export class DataBaseColumnStats extends SignalWatcher(
accessor group: Group | undefined = undefined; accessor group: Group | undefined = undefined;
@property({ attribute: false }) @property({ attribute: false })
accessor view!: TableSingleView; accessor tableViewLogic!: TableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
} }
declare global { declare global {
@@ -0,0 +1,10 @@
import { DataBaseColumnStats } from './column-stats-bar.js';
import { DatabaseColumnStatsCell } from './column-stats-column.js';
export function statsEffects() {
customElements.define('affine-database-column-stats', DataBaseColumnStats);
customElements.define(
'affine-database-column-stats-cell',
DatabaseColumnStatsCell
);
}
@@ -0,0 +1,8 @@
import { css } from '@emotion/css';
import { cssVarV2 } from '@toeverything/theme/v2';
export const cellDivider = css({
height: '100%',
width: '1px',
backgroundColor: cssVarV2.database.border,
});
@@ -1,45 +0,0 @@
import { signal } from '@preact/signals-core';
import { html } from 'lit';
import { ref } from 'lit/directives/ref.js';
import { DataViewBase } from '../../core/view/data-view-base.js';
import type { DataViewTable } from './pc/table-view.js';
import type { VirtualTableView } from './pc-virtual/table-view.js';
import type { TableViewSelectionWithType } from './selection.js';
import type { TableSingleView } from './table-view-manager.js';
export class TableViewSelector extends DataViewBase<
TableSingleView,
TableViewSelectionWithType
> {
tableRef$ = signal<VirtualTableView | DataViewTable>();
// @ts-expect-error TODO: fix this
get expose() {
if (this.tableRef$.value) {
return this.tableRef$.value.expose;
}
}
override render() {
const flags = this.props.view.manager.dataSource.featureFlags$.value;
if (flags.enable_table_virtual_scroll) {
return html`<affine-virtual-table
${ref(this.tableRef$)}
.props=${this.props}
></affine-virtual-table>`;
}
return html`<affine-database-table
${ref(this.tableRef$)}
.props=${this.props}
></affine-database-table>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-table-selector': TableViewSelector;
}
}
@@ -5,7 +5,7 @@ import { filterTraitKey } from '../../../core/filter/trait.js';
import type { DataViewWidgetProps } from '../../../core/widget/types.js'; import type { DataViewWidgetProps } from '../../../core/widget/types.js';
export const renderFilterBar = (props: DataViewWidgetProps) => { export const renderFilterBar = (props: DataViewWidgetProps) => {
const filterTrait = props.dataViewInstance.view.traitGet(filterTraitKey); const filterTrait = props.dataViewLogic.view.traitGet(filterTraitKey);
if (!filterTrait) { if (!filterTrait) {
return; return;
} }
@@ -15,7 +15,7 @@ import { renderSortBar } from './sort/index.js';
export const widgetQuickSettingBar = defineUniComponent( export const widgetQuickSettingBar = defineUniComponent(
(props: DataViewWidgetProps) => { (props: DataViewWidgetProps) => {
const view = props.dataViewInstance.view; const view = props.dataViewLogic.view;
const barList = [renderSortBar(props), renderFilterBar(props)].filter( const barList = [renderSortBar(props), renderFilterBar(props)].filter(
Boolean Boolean
); );
@@ -8,7 +8,7 @@ import type { DataViewWidgetProps } from '../../../core/widget/types.js';
import { popSortRoot } from './root-panel.js'; import { popSortRoot } from './root-panel.js';
export const renderSortBar = (props: DataViewWidgetProps) => { export const renderSortBar = (props: DataViewWidgetProps) => {
const sortTrait = props.dataViewInstance.view.traitGet(sortTraitKey); const sortTrait = props.dataViewLogic.view.traitGet(sortTraitKey);
if (!sortTrait) { if (!sortTrait) {
return; return;
} }
@@ -19,7 +19,7 @@ export const renderSortBar = (props: DataViewWidgetProps) => {
const text = count === 1 ? html`1 Sort` : html`${count} Sorts`; const text = count === 1 ? html`1 Sort` : html`${count} Sorts`;
const click = (event: MouseEvent) => { const click = (event: MouseEvent) => {
popSortRoot(popupTargetFromElement(event.currentTarget as HTMLElement), { popSortRoot(popupTargetFromElement(event.currentTarget as HTMLElement), {
sortUtils: createSortUtils(sortTrait, props.dataViewInstance.eventTrace), sortUtils: createSortUtils(sortTrait, props.dataViewLogic.eventTrace),
}); });
}; };
return html` <data-view-component-button return html` <data-view-component-button
@@ -9,10 +9,8 @@ import { styleMap } from 'lit/directives/style-map.js';
import { stopPropagation } from '../../../../core/utils/event.js'; import { stopPropagation } from '../../../../core/utils/event.js';
import { WidgetBase } from '../../../../core/widget/widget-base.js'; import { WidgetBase } from '../../../../core/widget/widget-base.js';
import type { import type { KanbanViewUILogic } from '../../../../view-presets/kanban/pc/kanban-view-ui-logic.js';
KanbanSingleView, import type { VirtualTableViewUILogic } from '../../../../view-presets/table/pc-virtual/table-view-ui-logic.js';
TableSingleView,
} from '../../../../view-presets/index.js';
const styles = css` const styles = css`
.affine-database-search-container { .affine-database-search-container {
@@ -89,13 +87,13 @@ const styles = css`
`; `;
export class DataViewHeaderToolsSearch extends WidgetBase< export class DataViewHeaderToolsSearch extends WidgetBase<
TableSingleView | KanbanSingleView VirtualTableViewUILogic | KanbanViewUILogic
> { > {
static override styles = styles; static override styles = styles;
private readonly _clearSearch = () => { private readonly _clearSearch = () => {
this._searchInput.value = ''; this._searchInput.value = '';
this.view.setSearch(''); this.dataViewLogic.view.setSearch('');
this.preventBlur = true; this.preventBlur = true;
setTimeout(() => { setTimeout(() => {
this.preventBlur = false; this.preventBlur = false;
@@ -110,7 +108,7 @@ export class DataViewHeaderToolsSearch extends WidgetBase<
private readonly _onSearch = (event: InputEvent) => { private readonly _onSearch = (event: InputEvent) => {
const el = event.target as HTMLInputElement; const el = event.target as HTMLInputElement;
const inputValue = el.value.trim(); const inputValue = el.value.trim();
this.view.setSearch(inputValue); this.dataViewLogic.view.setSearch(inputValue);
}; };
private readonly _onSearchBlur = () => { private readonly _onSearchBlur = () => {
@@ -124,7 +122,7 @@ export class DataViewHeaderToolsSearch extends WidgetBase<
if (event.key === 'Escape') { if (event.key === 'Escape') {
if (this._searchInput.value) { if (this._searchInput.value) {
this._searchInput.value = ''; this._searchInput.value = '';
this.view.setSearch(''); this.dataViewLogic.view.setSearch('');
} else { } else {
this.showSearch = false; this.showSearch = false;
} }
@@ -43,7 +43,7 @@ export class DataViewHeaderToolsSort extends WidgetBase {
sortUtils$ = computed(() => { sortUtils$ = computed(() => {
const sortTrait = this.view.traitGet(sortTraitKey); const sortTrait = this.view.traitGet(sortTraitKey);
if (sortTrait) { if (sortTrait) {
return createSortUtils(sortTrait, this.dataViewInstance.eventTrace); return createSortUtils(sortTrait, this.dataViewLogic.eventTrace);
} }
return; return;
}); });
@@ -19,22 +19,22 @@ const styles = css`
export class DataViewHeaderToolsAddRow extends WidgetBase { export class DataViewHeaderToolsAddRow extends WidgetBase {
static override styles = styles; static override styles = styles;
private readonly _onAddNewRecord = () => { private readonly onAddNewRecord = () => {
if (this.readonly) return; if (this.readonly$.value) return;
this.viewMethods.addRow?.('start'); this.dataViewLogic.addRow?.('start');
}; };
private get readonly() { private get readonly$() {
return this.view.readonly$.value; return this.view.readonly$;
} }
override render() { override render() {
if (this.readonly) { if (this.readonly$.value) {
return; return;
} }
return html` <data-view-component-button return html` <data-view-component-button
class="affine-database-toolbar-item new-record" class="affine-database-toolbar-item new-record"
.onClick="${this._onAddNewRecord}" .onClick="${this.onAddNewRecord}"
.icon="${PlusIcon()}" .icon="${PlusIcon()}"
.text="${IS_MOBILE .text="${IS_MOBILE
? html`<span style="font-weight: 500">New</span>` ? html`<span style="font-weight: 500">New</span>`
@@ -29,7 +29,7 @@ import {
} from '../../../../core/group-by/setting.js'; } from '../../../../core/group-by/setting.js';
import { groupTraitKey } from '../../../../core/group-by/trait.js'; import { groupTraitKey } from '../../../../core/group-by/trait.js';
import { import {
type DataViewInstance, type DataViewUILogicBase,
emptyFilterGroup, emptyFilterGroup,
popCreateFilter, popCreateFilter,
renderUniLit, renderUniLit,
@@ -73,7 +73,7 @@ export class DataViewHeaderToolsViewOptions extends WidgetBase {
}; };
openMoreAction = (target: PopupTarget) => { openMoreAction = (target: PopupTarget) => {
popViewOptions(target, this.dataViewInstance); popViewOptions(target, this.dataViewLogic);
}; };
override render() { override render() {
@@ -96,10 +96,10 @@ declare global {
} }
const createSettingMenus = ( const createSettingMenus = (
target: PopupTarget, target: PopupTarget,
dataViewInstance: DataViewInstance, dataViewLogic: DataViewUILogicBase,
reopen: () => void reopen: () => void
) => { ) => {
const view = dataViewInstance.view; const view = dataViewLogic.view;
const settingItems: MenuConfig[] = []; const settingItems: MenuConfig[] = [];
settingItems.push( settingItems.push(
menu.action({ menu.action({
@@ -177,7 +177,7 @@ const createSettingMenus = (
const sortList = sortTrait.sortList$.value; const sortList = sortTrait.sortList$.value;
const sortUtils = createSortUtils( const sortUtils = createSortUtils(
sortTrait, sortTrait,
dataViewInstance.eventTrace dataViewLogic.eventTrace
); );
if (!sortList.length) { if (!sortList.length) {
popCreateSort(target, { popCreateSort(target, {
@@ -225,12 +225,12 @@ const createSettingMenus = (
}; };
export const popViewOptions = ( export const popViewOptions = (
target: PopupTarget, target: PopupTarget,
dataViewInstance: DataViewInstance, dataViewLogic: DataViewUILogicBase,
onClose?: () => void onClose?: () => void
) => { ) => {
const view = dataViewInstance.view; const view = dataViewLogic.view;
const reopen = () => { const reopen = () => {
popViewOptions(target, dataViewInstance); popViewOptions(target, dataViewLogic);
}; };
const items: MenuConfig[] = []; const items: MenuConfig[] = [];
items.push( items.push(
@@ -291,7 +291,7 @@ export const popViewOptions = (
return; return;
} }
view.manager.viewChangeType(id, meta.type); view.manager.viewChangeType(id, meta.type);
dataViewInstance.clearSelection(); dataViewLogic.clearSelection();
}, },
class: {}, class: {},
}; };
@@ -345,7 +345,7 @@ export const popViewOptions = (
items.push( items.push(
menu.group({ menu.group({
items: createSettingMenus(target, dataViewInstance, reopen), items: createSettingMenus(target, dataViewLogic, reopen),
}) })
); );
items.push( items.push(
@@ -52,7 +52,7 @@ export class DataViewHeaderTools extends WidgetBase {
return html` <div class="${classList}"> return html` <div class="${classList}">
${repeat(tools ?? [], uni => { ${repeat(tools ?? [], uni => {
return renderUniLit(uni, { return renderUniLit(uni, {
dataViewInstance: this.dataViewInstance, dataViewLogic: this.dataViewLogic,
}); });
})} })}
</div>`; </div>`;
@@ -264,7 +264,7 @@ export class DataViewHeaderViews extends WidgetBase {
<div <div
class="${classList}" class="${classList}"
style="margin-right: 4px;" style="margin-right: 4px;"
@click="${(event: MouseEvent) => this._clickView(event, id)}" @click="${(event: MouseEvent) => this.clickView(event, id)}"
> >
<uni-lit class="icon" .uni="${this.getRenderer(id)?.icon}"></uni-lit> <uni-lit class="icon" .uni="${this.getRenderer(id)?.icon}"></uni-lit>
<div class="name">${view?.name}</div> <div class="name">${view?.name}</div>
@@ -281,7 +281,7 @@ export class DataViewHeaderViews extends WidgetBase {
return this.dataSource.viewMetaGetById(viewId)?.renderer; return this.dataSource.viewMetaGetById(viewId)?.renderer;
} }
_clickView(event: MouseEvent, id: string) { private clickView(event: MouseEvent, id: string) {
if (this.viewManager.currentViewId$.value !== id) { if (this.viewManager.currentViewId$.value !== id) {
this.viewManager.setCurrentView(id); this.viewManager.setCurrentView(id);
this.onChangeView?.(id); this.onChangeView?.(id);
@@ -18,6 +18,29 @@ export type DatabaseViewEvents = {
DatabaseSortClear: { DatabaseSortClear: {
rulesCount: number; rulesCount: number;
}; };
CreateDatabaseView: {
viewType: string; // e.g., 'table', 'gallery'
};
ChangeDatabaseLayout: {
fromViewType: string;
toViewType: string;
};
};
export type DatabasePropertyEvents = {
CreateDatabaseProperty: {
propertyType: string; // e.g., 'text', 'number', 'select'
};
};
export type DatabaseFilterEvents = {
CreateDatabaseFilter: {};
};
export type DatabaseGroupEvents = {
CreateDatabaseGroup: {
groupBy: string;
};
}; };
export type DatabaseEvents = { export type DatabaseEvents = {
@@ -40,7 +63,10 @@ export interface DatabaseAllSortEvents {
} }
export type DatabaseAllViewEvents = DatabaseViewEvents & export type DatabaseAllViewEvents = DatabaseViewEvents &
WithParams<DatabaseAllSortEvents, SortParams>; WithParams<DatabaseAllSortEvents, SortParams> &
DatabasePropertyEvents &
DatabaseFilterEvents &
DatabaseGroupEvents;
export type DatabaseAllEvents = DatabaseEvents & export type DatabaseAllEvents = DatabaseEvents &
WithParams<DatabaseAllViewEvents, ViewParams>; WithParams<DatabaseAllViewEvents, ViewParams>;

Some files were not shown because too many files have changed in this diff Show More