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
@@ -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 './pc/effect.js';
export * from './pc/table-view.js';
export * from './pc-virtual/effect.js';
export * from './renderer.js';
export * from './selection.js';
export * from './table-view-manager.js';
export * from './table-view-selector.js';
@@ -8,10 +8,10 @@ import {
type CellRenderProps,
type DataViewCellLifeCycle,
renderUniLit,
type SingleView,
} from '../../../core/index.js';
import { TableViewAreaSelection } from '../selection';
import type { TableProperty } from '../table-view-manager.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export class MobileTableCell extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -48,7 +48,7 @@ export class MobileTableCell extends SignalWatcher(
});
isSelectionEditing$ = computed(() => {
const selection = this.table?.props.selection$.value;
const selection = this.tableViewLogic.selection$.value;
if (selection?.selectionType !== 'area') {
return false;
}
@@ -68,8 +68,8 @@ export class MobileTableCell extends SignalWatcher(
if (this.view.readonly$.value) {
return;
}
const setSelection = this.table?.props.setSelection;
const viewId = this.table?.props.view.id;
const setSelection = this.tableViewLogic.setSelection;
const viewId = this.tableViewLogic.view.id;
if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) {
return;
@@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher(
return this.closest('mobile-table-group')?.group?.key;
}
private get table() {
return this.closest('mobile-data-view-table');
}
override connectedCallback() {
super.connectedCallback();
if (this.column.readonly$.value) return;
@@ -160,7 +156,11 @@ export class MobileTableCell extends SignalWatcher(
accessor rowIndex!: number;
@property({ attribute: false })
accessor view!: SingleView;
accessor tableViewLogic!: MobileTableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
}
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 { cssVarV2 } from '@toeverything/theme/v2';
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 type { DataViewRenderer } from '../../../core/data-view.js';
import { GroupTitle } from '../../../core/group-by/group-title.js';
import type { Group } from '../../../core/group-by/trait.js';
import type { Row } from '../../../core/index.js';
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import type { DataViewTable } from '../pc/table-view.js';
import { TableViewAreaSelection } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
const styles = css`
.data-view-table-group-add-row {
@@ -54,40 +51,10 @@ export class MobileTableGroup extends SignalWatcher(
private readonly clickAddRow = () => {
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 = () => {
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) => {
@@ -150,8 +117,7 @@ export class MobileTableGroup extends SignalWatcher(
return html` <mobile-table-row
data-row-index="${idx}"
data-row-id="${row.rowId}"
.dataViewEle="${this.dataViewEle}"
.view="${this.view}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${row.rowId}"
.rowIndex="${idx}"
></mobile-table-row>`;
@@ -172,8 +138,6 @@ export class MobileTableGroup extends SignalWatcher(
${PlusIcon()}<span style="font-size: 12px">New Record</span>
</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);
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@query('.affine-database-block-rows')
accessor rowsContainer: HTMLElement | null = null;
@property({ attribute: false })
accessor view!: TableSingleView;
accessor tableViewLogic!: MobileTableViewUILogic;
@property({ attribute: false })
accessor viewEle!: DataViewTable;
get view() {
return this.tableViewLogic.view;
}
}
declare global {
@@ -8,6 +8,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import { cellDivider } from '../styles.js';
import type { TableSingleView } from '../table-view-manager.js';
export class MobileTableHeader extends SignalWatcher(
@@ -60,7 +61,7 @@ export class MobileTableHeader extends SignalWatcher(
.column="${column}"
.tableViewManager="${this.tableViewManager}"
></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';
import { DeleteIcon, ExpandFullIcon } from '@blocksuite/icons/lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { SingleView } from '../../../core/index.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export const popMobileRowMenu = (
target: PopupTarget,
rowId: string,
dataViewEle: DataViewRenderer,
tableViewLogic: MobileTableViewUILogic,
view: SingleView
) => {
popFilterableSimpleMenu(target, [
@@ -21,7 +21,7 @@ export const popMobileRowMenu = (
name: 'Expand Row',
prefix: ExpandFullIcon(),
select: () => {
dataViewEle.openDetailPanel({
tableViewLogic.root.openDetailPanel({
view: view,
rowId: rowId,
});
@@ -10,9 +10,9 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../core/data-view.js';
import type { TableSingleView } from '../table-view-manager.js';
import { cellDivider } from '../styles.js';
import { popMobileRowMenu } from './menu.js';
import type { MobileTableViewUILogic } from './table-view-ui-logic.js';
export class MobileTableRow extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -73,7 +73,7 @@ export class MobileTableRow extends SignalWatcher(
v => v.id,
(column, i) => {
const clickDetail = () => {
this.dataViewEle.openDetailPanel({
this.tableViewLogic.root.openDetailPanel({
view: this.view,
rowId: this.rowId,
});
@@ -83,7 +83,7 @@ export class MobileTableRow extends SignalWatcher(
popMobileRowMenu(
popupTargetFromElement(ele),
this.rowId,
this.dataViewEle,
this.tableViewLogic,
this.view
);
};
@@ -95,7 +95,7 @@ export class MobileTableRow extends SignalWatcher(
width: `${column.width$.value}px`,
border: i === 0 ? 'none' : undefined,
})}
.view="${view}"
.tableViewLogic="${this.tableViewLogic}"
.column="${column}"
.rowId="${this.rowId}"
data-row-id="${this.rowId}"
@@ -107,7 +107,7 @@ export class MobileTableRow extends SignalWatcher(
data-column-index="${i}"
>
</mobile-table-cell>
<div class="cell-divider"></div>
<div class="${cellDivider}"></div>
</div>
${!column.readonly$.value &&
column.view.mainProperties$.value.titleColumn === column.id
@@ -130,7 +130,7 @@ export class MobileTableRow extends SignalWatcher(
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
accessor tableViewLogic!: MobileTableViewUILogic;
@property({ attribute: false })
accessor rowId!: string;
@@ -138,8 +138,9 @@ export class MobileTableRow extends SignalWatcher(
@property({ attribute: false })
accessor rowIndex!: number;
@property({ attribute: false })
accessor view!: TableSingleView;
get view() {
return this.tableViewLogic.view;
}
}
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 { ReactiveController } from 'lit';
@@ -9,20 +10,18 @@ import {
type TableViewSelection,
type TableViewSelectionWithType,
} 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';
type JsonAreaData = string[][];
const TEXT = 'text/plain';
export class TableClipboardController implements ReactiveController {
disposables = new DisposableGroup();
private readonly _onCopy = (
tableSelection: TableViewSelectionWithType,
isCut = false
) => {
const table = this.host;
const area = getSelectedArea(tableSelection, table);
const area = getSelectedArea(tableSelection, this.logic);
if (!area) {
return;
}
@@ -44,7 +43,7 @@ export class TableClipboardController implements ReactiveController {
}
}
if (deleteRows.length) {
this.props.view.rowsDelete(deleteRows);
this.logic.view.rowsDelete(deleteRows);
}
}
this.clipboard
@@ -79,12 +78,11 @@ export class TableClipboardController implements ReactiveController {
private readonly _onPaste = async (_context: UIEventStateContext) => {
const event = _context.get('clipboardState').raw;
event.stopPropagation();
const view = this.host;
const clipboardData = event.clipboardData;
if (!clipboardData) return;
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selection;
if (TableViewRowSelection.is(tableSelection)) {
return;
}
@@ -97,7 +95,7 @@ export class TableClipboardController implements ReactiveController {
if (dataString) {
// If internal format data exists, use it
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
pasteToCells(view, jsonAreaData, tableSelection);
pasteToCells(this.logic, jsonAreaData, tableSelection);
return true;
}
} catch {
@@ -115,7 +113,7 @@ export class TableClipboardController implements ReactiveController {
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
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() {
return this.props.clipboard;
return this.logic.root.config.clipboard;
}
private get notification() {
return this.props.notification;
}
get props() {
return this.host.props;
return this.logic.root.config.notification;
}
private get readonly() {
return this.props.view.readonly$.value;
return this.logic.view.readonly$.value;
}
constructor(public host: VirtualTableView) {
host.addController(this);
constructor(public logic: VirtualTableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
get selection() {
return this.logic.selectionController.selection;
}
copy() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selection;
if (!tableSelection) {
return;
}
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
}
cut() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selection;
if (!tableSelection) {
return;
}
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
}
hostConnected() {
this.host.disposables.add(
this.props.handleEvent('copy', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.disposables.add(
this.logic.handleEvent('copy', _ctx => {
const tableSelection = this.selection;
if (!tableSelection) return false;
this._onCopy(tableSelection);
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('cut', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.disposables.add(
this.logic.handleEvent('cut', _ctx => {
const tableSelection = this.selection;
if (!tableSelection) return false;
this._onCut(tableSelection);
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('paste', ctx => {
this.disposables.add(
this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false;
this._onPaste(ctx).catch(console.error);
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
function getSelectedArea(
selection: TableViewSelection,
table: VirtualTableView
table: VirtualTableViewUILogic
): SelectedArea | undefined {
const view = table.props.view;
const view = table.view;
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rows(selection)
.map(row => {
@@ -282,7 +281,7 @@ function getTargetRangeFromSelection(
}
function pasteToCells(
table: VirtualTableView,
table: VirtualTableViewUILogic,
rows: JsonAreaData,
selection: TableViewAreaSelection
) {
@@ -8,7 +8,7 @@ import * as Y from 'yjs';
import { t } from '../../../../core/index.js';
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 {
static override styles = css`
@@ -55,12 +55,12 @@ declare global {
}
export function fillSelectionWithFocusCellData(
host: VirtualTableView,
logic: VirtualTableViewUILogic,
selection: TableViewAreaSelection
) {
const { groupKey, rowsSelection, columnsSelection, focus } = selection;
const focusCell = host.selectionController.getCellContainer(
const focusCell = logic.selectionController.getCellContainer(
groupKey,
focus.rowIndex,
focus.columnIndex
@@ -85,7 +85,7 @@ export function fillSelectionWithFocusCellData(
for (let i = start; i <= end; i++) {
if (i === focus.rowIndex) continue;
const cellContainer = host.selectionController.getCellContainer(
const cellContainer = logic.selectionController.getCellContainer(
groupKey,
i,
draggingColIdx
@@ -3,8 +3,7 @@
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import type { ReactiveController } from 'lit';
// import { startDrag } from '../../../../core/utils/drag.js';
import type { VirtualTableView } from '../table-view';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
export class TableDragController implements ReactiveController {
// dragStart = (row: TableRow, evt: PointerEvent) => {
@@ -91,9 +90,9 @@ export class TableDragController implements ReactiveController {
| undefined => {
const y = evt.y;
const tableRect = this.host
.querySelector('affine-data-view-table-group')
?.querySelector('affine-data-view-table-group')
?.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) {
return;
}
@@ -127,12 +126,14 @@ export class TableDragController implements ReactiveController {
return position;
};
constructor(private readonly host: VirtualTableView) {
this.host.addController(this);
constructor(private readonly logic: VirtualTableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
if (this.host.props.view.readonly$.value) {
if (this.logic.view.readonly$.value) {
return;
}
// this.host.disposables.add(
@@ -1,22 +1,26 @@
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { ReactiveController } from 'lit';
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
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 {
disposables = new DisposableGroup();
get selectionController() {
return this.host.selectionController;
return this.logic.selectionController;
}
constructor(private readonly host: VirtualTableView) {
this.host.addController(this);
constructor(private readonly logic: VirtualTableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
this.host.disposables.add(
this.host.props.bindHotkey({
this.disposables.add(
this.logic.bindHotkey({
Backspace: () => {
const selection = this.selectionController.selection;
if (!selection) {
@@ -25,7 +29,7 @@ export class TableHotkeysController implements ReactiveController {
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rowsIds(selection);
this.selectionController.selection = undefined;
this.host.props.view.rowsDelete(rows);
this.logic.view.rowsDelete(rows);
return;
}
const {
@@ -334,14 +338,14 @@ export class TableHotkeysController implements ReactiveController {
context.get('keyboardState').raw.preventDefault();
this.selectionController.selection = TableViewRowSelection.create({
rows:
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap(
this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
group =>
group?.rows.map(row => ({
groupKey: group.key,
id: row.rowId,
})) ?? []
) ??
this.host.props.view.rows$.value.map(row => ({
this.logic.view.rows$.value.map(row => ({
groupKey: undefined,
id: row.rowId,
})),
@@ -377,7 +381,7 @@ export class TableHotkeysController implements ReactiveController {
rows: [row],
});
popRowMenu(
this.host.props.dataViewEle,
this.logic,
popupTargetFromElement(cell),
this.selectionController
);
@@ -1,4 +1,5 @@
import { getRangeByPositions } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { computed, type ReadonlySignal } from '@preact/signals-core';
@@ -18,7 +19,7 @@ import {
type TableViewSelectionWithType,
} from '../../selection';
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 {
DragToFillElement,
@@ -26,6 +27,7 @@ import {
} from './drag-to-fill.js';
export class TableSelectionController implements ReactiveController {
disposables = new DisposableGroup();
private _tableViewSelection?: TableViewSelectionWithType;
private readonly getFocusCellContainer = () => {
@@ -85,12 +87,12 @@ export class TableSelectionController implements ReactiveController {
);
const cell = container?.cell;
const isEditing = cell ? cell.beforeEnterEditMode() : true;
this.host.props.setSelection({
this.logic.setSelection({
...selection,
isEditing,
});
} else {
this.host.props.setSelection(selection);
this.logic.setSelection(selection);
}
}
@@ -99,27 +101,26 @@ export class TableSelectionController implements ReactiveController {
}
get view() {
return this.host.props.view;
return this.logic.view;
}
get viewData() {
return this.view;
}
constructor(public host: VirtualTableView) {
host.addController(this);
constructor(public logic: VirtualTableViewUILogic) {
this.__selectionElement = new SelectionElement();
this.__selectionElement.controller = this;
}
get host() {
return this.logic.ui$.value;
}
private clearSelection() {
this.host.props.setSelection();
this.logic.setSelection();
}
private handleDragEvent() {
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
if (this.host.props.view.readonly$.value) {
this.disposables.add(
this.logic.handleEvent('dragStart', context => {
if (this.logic.view.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
@@ -150,8 +151,8 @@ export class TableSelectionController implements ReactiveController {
}
private handleSelectionChange() {
this.host.disposables.add(
this.host.props.selection$.subscribe(tableSelection => {
this.disposables.add(
this.logic.selection$.subscribe(tableSelection => {
if (!this.isValidSelection(tableSelection)) {
this.selection = undefined;
return;
@@ -236,7 +237,7 @@ export class TableSelectionController implements ReactiveController {
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
: this.view.rows$.value;
requestAnimationFrame(() => {
const index = this.host.props.view.properties$.value.findIndex(
const index = this.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
this.selection = TableViewAreaSelection.create({
@@ -625,7 +626,7 @@ export class TableSelectionController implements ReactiveController {
}
get virtualScroll() {
return this.host.virtualScroll$.value;
return this.logic.virtualScroll$.value;
}
getGroup(groupKey: string | undefined) {
@@ -913,7 +914,7 @@ export class TableSelectionController implements ReactiveController {
if (fillValues && this.selection) {
this.__dragToFillElement.dragging = false;
fillSelectionWithFocusCellData(
this.host,
this.logic,
TableViewAreaSelection.create({
groupKey: groupKey,
rowsSelection: selection.row,
@@ -1024,7 +1025,7 @@ export class SelectionElement extends SignalWatcher(
if (left == null || top == null || width == null || height == null) {
return;
}
const paddingLeft = this.controller.host.props.virtualPadding$.value;
const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
return {
left: left + paddingLeft,
top,
@@ -1054,7 +1055,7 @@ export class SelectionElement extends SignalWatcher(
if (!rect) {
return;
}
const paddingLeft = this.controller.host.props.virtualPadding$.value;
const paddingLeft = this.controller.logic.root.config.virtualPadding$.value;
return {
left: rect.left + paddingLeft,
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$() {
return this.controller.host.props.selection$;
return this.controller.logic.selection$;
}
override render() {
@@ -1,3 +1,4 @@
import { KanbanViewUI } from '../../kanban/pc/kanban-view-ui-logic';
import { DragToFillElement } from './controller/drag-to-fill';
import { SelectionElement } from './controller/selection';
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 { TableRowHeader } from './row/row-header';
import { TableRowLast } from './row/row-last';
import { VirtualTableView } from './table-view';
import { TableViewUI } from './table-view-ui-logic';
import { VirtualElementWrapper } from './virtual/virtual-cell';
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(
'affine-database-virtual-cell-container',
DatabaseCellContainer
@@ -6,29 +6,29 @@ import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { TableViewAreaSelection } from '../../../selection';
import type { VirtualTableView } from '../../table-view';
import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
import type { TableGridGroup } from '../../types';
import * as styles from './group-footer-css';
export class TableGroupFooter extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor gridGroup!: TableGridGroup;
group$ = computed(() => {
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find(
return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
g => g.key === this.gridGroup.groupId
);
});
get selectionController() {
return this.tableView.selectionController;
return this.tableViewLogic.selectionController;
}
get tableViewManager() {
return this.tableView.props.view;
return this.tableViewLogic.view;
}
override connectedCallback() {
@@ -10,7 +10,7 @@ import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { TableViewAreaSelection } from '../../../selection';
import type { VirtualTableView } from '../../table-view';
import type { VirtualTableViewUILogic } from '../../table-view-ui-logic';
import type { TableGridGroup } from '../../types';
import * as styles from './group-header-css';
import { GroupTitle } from './group-title';
@@ -18,7 +18,7 @@ export class TableGroupHeader extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor gridGroup!: TableGridGroup;
@@ -35,7 +35,7 @@ export class TableGroupHeader extends SignalWatcher(
}
group$ = computed(() => {
return this.tableView.groupTrait$.value?.groupsDataList$.value?.find(
return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find(
g => g.key === this.gridGroup.groupId
);
});
@@ -45,11 +45,11 @@ export class TableGroupHeader extends SignalWatcher(
});
get tableViewManager() {
return this.tableView.props.view;
return this.tableViewLogic.view;
}
get selectionController() {
return this.tableView.selectionController;
return this.tableViewLogic.selectionController;
}
private readonly clickAddRowInStart = () => {
@@ -123,7 +123,7 @@ export class TableGroupHeader extends SignalWatcher(
return html`
${this.renderGroupHeader()}
<virtual-table-header
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
></virtual-table-header>
`;
}
@@ -6,6 +6,7 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { PlusIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { css } from '@emotion/css';
import { nothing } from 'lit';
import { property, query } from 'lit/decorators.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 { 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';
const leftBarStyle = css({
width: LEFT_TOOL_BAR_WIDTH,
});
export class VirtualTableHeader extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
@@ -79,9 +84,7 @@ export class VirtualTableHeader extends SignalWatcher(
override render() {
return html`
<div class="${styles.columnHeader} database-row">
${this.readonly
? nothing
: html` <div class="data-view-table-left-bar"></div>`}
${this.readonly ? nothing : html` <div class="${leftBarStyle}"></div>`}
${repeat(
this.tableViewManager.properties$.value,
column => column.id,
@@ -97,9 +100,9 @@ export class VirtualTableHeader extends SignalWatcher(
data-column-index="${index}"
class="${styles.column} ${styles.cell}"
.column="${column}"
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
></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;
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: VirtualTableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
declare global {
@@ -13,6 +13,7 @@ import type {
TableProperty,
TableSingleView,
} from '../../../../table-view-manager';
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
export class DataViewColumnPreview extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
`;
get tableViewManager(): TableSingleView {
return this.column.view as TableSingleView;
return this.tableViewLogic.view;
}
private renderGroup(rows: Row[]) {
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
)};box-shadow: var(--affine-shadow-2);"
>
<affine-database-header-column
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.column="${this.column}"
></affine-database-header-column>
${repeat(rows, (id, index) => {
const height = this.container.querySelector(
`affine-database-cell-container[data-row-id="${id}"]`
`dv-table-view-cell-container[data-row-id="${id}"]`
)?.clientHeight;
const style = styleMap({
height: height + 'px',
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
)}"
>
<div style="${style}">
<affine-database-cell-container
<dv-table-view-cell-container
.column="${this.column}"
.view="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${id}"
.columnId="${this.column.id}"
.rowIndex="${index}"
.columnIndex="${columnIndex}"
></affine-database-cell-container>
></dv-table-view-cell-container>
</div>
</div>`;
})}
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor tableViewLogic!: VirtualTableViewUILogic;
}
declare global {
@@ -45,10 +45,8 @@ import {
ShowQuickSettingBarKey,
} from '../../../../../../widget-presets/quick-setting-bar/context';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts';
import type {
TableProperty,
TableSingleView,
} from '../../../../table-view-manager';
import type { TableProperty } from '../../../../table-view-manager';
import type { VirtualTableViewUILogic } from '../../../table-view-ui-logic';
import {
getTableGroupRect,
getVerticalIndicator,
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
const sortUtils = createSortUtils(
sortTrait,
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {})
this.tableViewLogic.eventTrace ?? (() => {})
);
const sortList = sortUtils.sortList$.value;
const existingIndex = sortList.findIndex(
@@ -398,28 +396,25 @@ export class DatabaseHeaderColumn extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
const table = this.closest('affine-database-table');
if (table) {
this.disposables.add(
table.props.handleEvent('dragStart', context => {
if (this.tableViewManager.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
const target = event.target;
if (
target instanceof Element &&
this.widthDragBar.value?.contains(target)
) {
event.preventDefault();
event.stopPropagation();
this.widthDragStart(event);
return true;
}
return false;
})
);
}
this.disposables.add(
this.tableViewLogic.handleEvent('dragStart', context => {
if (this.tableViewManager.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
const target = event.target;
if (
target instanceof Element &&
this.widthDragBar.value?.contains(target)
) {
event.preventDefault();
event.stopPropagation();
this.widthDragStart(event);
return true;
}
return false;
})
);
}
override render() {
@@ -481,7 +476,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: VirtualTableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
function numberFormatConfig(column: Property): MenuConfig {
@@ -10,13 +10,12 @@ import type {
CellRenderProps,
DataViewCellLifeCycle,
} from '../../../../core/property';
import type { SingleView } from '../../../../core/view-manager/single-view';
import {
TableViewAreaSelection,
TableViewRowSelection,
type TableViewSelectionWithType,
} from '../../selection';
import type { VirtualTableView } from '../table-view';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
import type { TableGridCell } from '../types';
import { popRowMenu } from './menu';
import { rowSelectedBg } from './row-header-css';
@@ -82,7 +81,7 @@ export class DatabaseCellContainer extends SignalWatcher(
}
private get selectionView() {
return this.tableView?.selectionController;
return this.tableViewLogic.selectionController;
}
get rowSelected$() {
@@ -104,11 +103,7 @@ export class DatabaseCellContainer extends SignalWatcher(
rows: [row],
});
}
popRowMenu(
this.tableView.props.dataViewEle,
popupTargetFromElement(this),
selection
);
popRowMenu(this.tableViewLogic, popupTargetFromElement(this), selection);
};
override connectedCallback() {
@@ -216,10 +211,11 @@ export class DatabaseCellContainer extends SignalWatcher(
accessor gridCell!: TableGridCell;
@property({ attribute: false })
accessor view!: SingleView;
accessor tableViewLogic!: VirtualTableViewUILogic;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
get view() {
return this.tableViewLogic.view;
}
}
declare global {
@@ -12,19 +12,19 @@ import {
} from '@blocksuite/icons/lit';
import { html } from 'lit';
import type { DataViewRenderer } from '../../../../core/data-view';
import { TableViewRowSelection } from '../../selection';
import type { TableSelectionController } from '../controller/selection';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic';
export const openDetail = (
dataViewEle: DataViewRenderer,
tableViewLogic: VirtualTableViewUILogic,
rowId: string,
selection: TableSelectionController
) => {
const old = selection.selection;
selection.selection = undefined;
dataViewEle.openDetailPanel({
view: selection.host.props.view,
tableViewLogic.root.openDetailPanel({
view: tableViewLogic.view,
rowId: rowId,
onClose: () => {
selection.selection = old;
@@ -33,7 +33,7 @@ export const openDetail = (
};
export const popRowMenu = (
dataViewEle: DataViewRenderer,
tableViewLogic: VirtualTableViewUILogic,
ele: PopupTarget,
selectionController: TableSelectionController
) => {
@@ -55,7 +55,7 @@ export const popRowMenu = (
${CopyIcon()}
</div>`,
select: () => {
selectionController.host.clipboardController.copy();
tableViewLogic.clipboardController.copy();
},
}),
],
@@ -85,7 +85,7 @@ export const popRowMenu = (
name: 'Expand Row',
prefix: ExpandFullIcon(),
select: () => {
openDetail(dataViewEle, row.id, selectionController);
openDetail(tableViewLogic, row.id, selectionController);
},
}),
menu.group({
@@ -7,14 +7,17 @@ import { nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js';
import type { VirtualTableView } from '../table-view.js';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { TableGridCell } from '../types.js';
import * as styles from './row-header-css.js';
export class TableRowHeader extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
get view() {
return this.tableViewLogic.view;
}
override connectedCallback(): void {
super.connectedCallback();
this.disposables.add(
@@ -42,7 +45,7 @@ export class TableRowHeader extends SignalWatcher(
};
get selectionController() {
return this.tableView.selectionController;
return this.tableViewLogic.selectionController;
}
get rowSelected$() {
@@ -115,10 +118,7 @@ export class TableRowHeader extends SignalWatcher(
accessor gridCell!: TableGridCell;
@property({ attribute: false })
accessor view!: TableSingleView;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
}
declare global {
@@ -5,8 +5,7 @@ import { effect } from '@preact/signals-core';
import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import type { TableSingleView } from '../../table-view-manager.js';
import type { VirtualTableView } from '../table-view.js';
import type { VirtualTableViewUILogic } from '../table-view-ui-logic.js';
import type { TableGridCell } from '../types.js';
import * as styles from './row-header-css.js';
@@ -56,10 +55,7 @@ export class TableRowLast extends SignalWatcher(
accessor gridCell!: TableGridCell;
@property({ attribute: false })
accessor view!: TableSingleView;
@property({ attribute: false })
accessor tableView!: VirtualTableView;
accessor tableViewLogic!: VirtualTableViewUILogic;
}
declare global {
@@ -3,19 +3,27 @@ import {
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { AddCursorIcon } from '@blocksuite/icons/lit';
import { computed, signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import type { TemplateResult } from 'lit';
import { styleMap } from 'lit/directives/style-map.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 {
type GroupTrait,
groupTraitKey,
} from '../../../core/group-by/trait.js';
import { type DataViewInstance, renderUniLit } from '../../../core/index.js';
import { DataViewBase } from '../../../core/view/data-view-base.js';
import {
createUniComponentFromWebComponent,
renderUniLit,
} from '../../../core/index.js';
import {
DataViewUIBase,
DataViewUILogicBase,
} from '../../../core/view/data-view-base.js';
import {
type TableSingleView,
TableViewRowSelection,
@@ -26,9 +34,9 @@ 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 { TableGroupFooter } from './group/bottom/group-footer';
import { TableGroupHeader } from './group/top/group-header';
import { DatabaseCellContainer } from './row/cell';
import { TableGroupFooter } from './group/bottom/group-footer.js';
import { TableGroupHeader } from './group/top/group-header.js';
import { DatabaseCellContainer } from './row/cell.js';
import { TableRowHeader } from './row/row-header.js';
import { TableRowLast } from './row/row-last.js';
import * as styles from './table-view-css.js';
@@ -43,15 +51,83 @@ import {
GridVirtualScroll,
} from './virtual/virtual-scroll.js';
export class VirtualTableView extends DataViewBase<
export class VirtualTableViewUILogic extends DataViewUILogicBase<
TableSingleView,
TableViewSelectionWithType
> {
ui$ = signal<TableViewUI | undefined>();
clipboardController = new TableClipboardController(this);
dragController = new TableDragController(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) => {
if (event.metaKey || event.ctrlKey) {
@@ -80,13 +156,12 @@ export class VirtualTableView extends DataViewBase<
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
column.dataUpdate(() =>
addGroup({
text,
oldData: column.data$.value,
dataSource: this.view.manager.dataSource,
})
);
}
},
@@ -103,92 +178,8 @@ export class VirtualTableView extends DataViewBase<
</div>`;
};
selectionController = new TableSelectionController(this);
yScrollContainer: HTMLElement | undefined;
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<
initVirtualScroll(yScrollContainer: HTMLElement, ui: TableViewUI) {
const virtualScroll = new GridVirtualScroll<
TableGroupData,
TableRowData,
TableCellData
@@ -213,7 +204,7 @@ export class VirtualTableView extends DataViewBase<
return row.cells$.value.some(cell => cell.data.hover$.value);
}),
selected$: computed(() => {
const selection = this.props.selection$.value;
const selection = this.selection$.value;
if (!selection || selection.selectionType !== 'row') {
return false;
}
@@ -234,34 +225,31 @@ export class VirtualTableView extends DataViewBase<
if (cell.columnId === 'row-header') {
wrapper.style.borderBottom = `1px solid ${cssVarV2.database.border}`;
const rowHeader = new TableRowHeader();
rowHeader.view = this.props.view;
rowHeader.gridCell = cell;
rowHeader.tableView = this;
rowHeader.tableViewLogic = this;
return rowHeader;
}
if (cell.columnId === 'row-last') {
const rowLast = new TableRowLast();
rowLast.view = this.props.view;
rowLast.gridCell = cell;
rowLast.tableView = this;
rowLast.tableViewLogic = this;
return rowLast;
}
const cellContainer = new DatabaseCellContainer();
cellContainer.view = this.props.view;
cellContainer.gridCell = cell;
cellContainer.tableView = this;
cellContainer.tableViewLogic = this;
return cellContainer;
},
createGroup: {
top: gridGroup => {
const groupHeader = new TableGroupHeader();
groupHeader.tableView = this;
groupHeader.tableViewLogic = this;
groupHeader.gridGroup = gridGroup;
return groupHeader;
},
bottom: gridGroup => {
const groupFooter = new TableGroupFooter();
groupFooter.tableView = this;
groupFooter.tableViewLogic = this;
groupFooter.gridGroup = gridGroup;
return groupFooter;
},
@@ -269,26 +257,40 @@ export class VirtualTableView extends DataViewBase<
fixedRowHeight$: signal(undefined),
yScrollContainer,
});
this.yScrollContainer = yScrollContainer;
this.virtualScroll$.value = virtualScroll;
requestAnimationFrame(() => {
const virtualScroll = this.virtualScroll$.value;
if (virtualScroll) {
virtualScroll.init();
this.disposables.add(() => virtualScroll.dispose());
ui.disposables.add(() => virtualScroll.dispose());
}
});
}
renderer = createUniComponentFromWebComponent(TableViewUI);
}
export class TableViewUI extends DataViewUIBase<VirtualTableViewUILogic> {
private renderTable() {
return this.virtualScroll$.value?.content;
return this.logic.virtualScroll$.value?.content;
}
override connectedCallback(): void {
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);
}
override render() {
const vPadding = this.props.virtualPadding$.value;
override render(): TemplateResult {
const vPadding = this.logic.root.config.virtualPadding$.value;
const wrapperStyle = styleMap({
marginLeft: `-${vPadding}px`,
marginRight: `-${vPadding}px`,
@@ -298,11 +300,11 @@ export class VirtualTableView extends DataViewBase<
paddingRight: `${vPadding}px`,
});
return html`
${renderUniLit(this.props.headerWidget, {
dataViewInstance: this.expose,
${renderUniLit(this.logic.root.config.headerWidget, {
dataViewLogic: this.logic,
})}
<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}">
${this.renderTable()}
</div>
@@ -314,6 +316,6 @@ export class VirtualTableView extends DataViewBase<
declare global {
interface HTMLElementTagNameMap {
'affine-virtual-table': VirtualTableView;
'dv-table-view-ui-virtual': TableViewUI;
}
}
@@ -61,8 +61,8 @@ export class BatchTaskManager {
private run(): void {
let totalBatchCount = this.totalBatchSize;
// let skipCount = 0;
// let tasksExecuted = false;
let skipCount = 0;
let tasksExecuted = false;
const runTaskArr = this.queues.map(() => 0);
for (let i = this.queues.length - 1; i >= 0; i--) {
const queue = this.queues[i];
@@ -82,22 +82,22 @@ export class BatchTaskManager {
if (result !== false) {
totalBatchCount--;
priorityBatchCount--;
// tasksExecuted = true;
tasksExecuted = true;
runTaskArr[i] = (runTaskArr[i] ?? 0) + 1;
}
}
}
// if (tasksExecuted) {
// console.log(
// 'run task count',
// ...runTaskArr,
// 'skip count',
// skipCount,
// 'total task count',
// ...this.queues.map(arr => arr.size)
// );
// }
if (tasksExecuted) {
console.log(
'run task count',
...runTaskArr,
'skip count',
skipCount,
'total task count',
...this.queues.map(arr => arr.size)
);
}
const hasRemainingTasks = this.queues.some(queue => !queue.isEmpty());
@@ -9,19 +9,18 @@ import type {
CellRenderProps,
DataViewCellLifeCycle,
} from '../../../core/property/index.js';
import type { SingleView } from '../../../core/view-manager/single-view.js';
import {
TableViewAreaSelection,
type TableViewSelectionWithType,
} from '../selection';
import type { TableProperty } from '../table-view-manager.js';
import type { TableGroup } from './group.js';
export class DatabaseCellContainer extends SignalWatcher(
import type { TableViewUILogic } from './table-view-ui-logic.js';
export class TableViewCellContainer extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
affine-database-cell-container {
dv-table-view-cell-container {
display: flex;
align-items: start;
width: 100%;
@@ -30,16 +29,16 @@ export class DatabaseCellContainer extends SignalWatcher(
outline: none;
}
affine-database-cell-container * {
dv-table-view-cell-container * {
box-sizing: border-box;
}
affine-database-cell-container uni-lit > *:first-child {
dv-table-view-cell-container uni-lit > *:first-child {
padding: 6px;
}
`;
private readonly _cell = signal<DataViewCellLifeCycle>();
private readonly _cell$ = signal<DataViewCellLifeCycle>();
@property({ attribute: false })
accessor column!: TableProperty;
@@ -55,7 +54,7 @@ export class DatabaseCellContainer extends SignalWatcher(
if (this.view.readonly$.value) {
return;
}
const selectionView = this.selectionView;
const selectionView = this.selectionController;
if (selectionView) {
const selection = selectionView.selection;
if (selection && this.isSelected(selection) && editing) {
@@ -81,20 +80,15 @@ export class DatabaseCellContainer extends SignalWatcher(
};
get cell(): DataViewCellLifeCycle | undefined {
return this._cell.value;
return this._cell$.value;
}
private get groupKey() {
return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key;
}
private get selectionView() {
return this.closest('affine-database-table')?.selectionController;
}
get table() {
const table = this.closest('affine-database-table');
return table;
private get selectionController() {
return this.tableViewLogic.selectionController;
}
override connectedCallback() {
@@ -134,7 +128,7 @@ export class DatabaseCellContainer extends SignalWatcher(
};
return renderUniLit(view, props, {
ref: this._cell,
ref: this._cell$,
style: {
display: 'contents',
},
@@ -152,12 +146,16 @@ export class DatabaseCellContainer extends SignalWatcher(
@property({ attribute: false })
accessor rowIndex!: number;
get view() {
return this.tableViewLogic.view;
}
@property({ attribute: false })
accessor view!: SingleView;
accessor tableViewLogic!: TableViewUILogic;
}
declare global {
interface HTMLElementTagNameMap {
'affine-database-cell-container': DatabaseCellContainer;
'dv-table-view-cell-container': TableViewCellContainer;
}
}
@@ -9,7 +9,7 @@ import {
type TableViewSelection,
type TableViewSelectionWithType,
} 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';
type JsonAreaData = string[][];
@@ -20,9 +20,7 @@ export class TableClipboardController implements ReactiveController {
tableSelection: TableViewSelectionWithType,
isCut = false
) => {
const table = this.host;
const area = getSelectedArea(tableSelection, table);
const area = getSelectedArea(tableSelection, this.logic);
if (!area) {
return;
}
@@ -44,7 +42,7 @@ export class TableClipboardController implements ReactiveController {
}
}
if (deleteRows.length) {
this.props.view.rowsDelete(deleteRows);
this.logic.view.rowsDelete(deleteRows);
}
}
this.clipboard
@@ -79,12 +77,11 @@ export class TableClipboardController implements ReactiveController {
private readonly _onPaste = async (_context: UIEventStateContext) => {
const event = _context.get('clipboardState').raw;
event.stopPropagation();
const view = this.host;
const clipboardData = event.clipboardData;
if (!clipboardData) return;
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selectionController.selection;
if (TableViewRowSelection.is(tableSelection)) {
return;
}
@@ -97,7 +94,7 @@ export class TableClipboardController implements ReactiveController {
if (dataString) {
// If internal format data exists, use it
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
pasteToCells(view, jsonAreaData, tableSelection);
pasteToCells(this.logic, jsonAreaData, tableSelection);
return true;
}
} catch {
@@ -115,7 +112,7 @@ export class TableClipboardController implements ReactiveController {
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
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() {
return this.props.clipboard;
return this.logic.root.config.clipboard;
}
private get notification() {
return this.props.notification;
}
get props() {
return this.host.props;
return this.logic.root.config.notification;
}
private get readonly() {
return this.props.view.readonly$.value;
return this.logic.view.readonly$.value;
}
constructor(public host: DataViewTable) {
host.addController(this);
constructor(public logic: TableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
get selectionController() {
return this.logic.selectionController;
}
copy() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selectionController.selection;
if (!tableSelection) {
return;
}
@@ -152,7 +151,7 @@ export class TableClipboardController implements ReactiveController {
}
cut() {
const tableSelection = this.host.selectionController.selection;
const tableSelection = this.selectionController.selection;
if (!tableSelection) {
return;
}
@@ -160,9 +159,9 @@ export class TableClipboardController implements ReactiveController {
}
hostConnected() {
this.host.disposables.add(
this.props.handleEvent('copy', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.host?.disposables.add(
this.logic.handleEvent('copy', _ctx => {
const tableSelection = this.selectionController.selection;
if (!tableSelection) return false;
this._onCopy(tableSelection);
@@ -170,9 +169,9 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('cut', _ctx => {
const tableSelection = this.host.selectionController.selection;
this.host?.disposables.add(
this.logic.handleEvent('cut', _ctx => {
const tableSelection = this.selectionController.selection;
if (!tableSelection) return false;
this._onCut(tableSelection);
@@ -180,8 +179,8 @@ export class TableClipboardController implements ReactiveController {
})
);
this.host.disposables.add(
this.props.handleEvent('paste', ctx => {
this.host?.disposables.add(
this.logic.handleEvent('paste', ctx => {
if (this.readonly) return false;
this._onPaste(ctx).catch(console.error);
@@ -193,9 +192,9 @@ export class TableClipboardController implements ReactiveController {
function getSelectedArea(
selection: TableViewSelection,
table: DataViewTable
table: TableViewUILogic
): SelectedArea | undefined {
const view = table.props.view;
const view = table.view;
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rows(selection)
.map(row => {
@@ -283,7 +282,7 @@ function getTargetRangeFromSelection(
}
function pasteToCells(
table: DataViewTable,
table: TableViewUILogic,
rows: JsonAreaData,
selection: TableViewAreaSelection
) {
@@ -8,7 +8,7 @@ import * as Y from 'yjs';
import { t } from '../../../../core/index.js';
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 {
static override styles = css`
@@ -49,12 +49,12 @@ export class DragToFillElement extends ShadowlessElement {
}
export function fillSelectionWithFocusCellData(
host: DataViewTable,
logic: TableViewUILogic,
selection: TableViewAreaSelection
) {
const { groupKey, rowsSelection, columnsSelection, focus } = selection;
const focusCell = host.selectionController.getCellContainer(
const focusCell = logic.selectionController.getCellContainer(
groupKey,
focus.rowIndex,
focus.columnIndex
@@ -78,7 +78,7 @@ export function fillSelectionWithFocusCellData(
for (let i = start; i <= end; i++) {
if (i === focus.rowIndex) continue;
const cellContainer = host.selectionController.getCellContainer(
const cellContainer = logic.selectionController.getCellContainer(
groupKey,
i,
draggingColIdx
@@ -5,7 +5,7 @@ import type { ReactiveController } from 'lit';
import { startDrag } from '../../../../core/utils/drag.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 {
dragStart = (rowView: TableRowView, evt: PointerEvent) => {
@@ -32,8 +32,8 @@ export class TableDragController implements ReactiveController {
onDrag: () => undefined,
onMove: evt => {
preview.display(evt.x - offsetLeft, evt.y - offsetTop);
if (!this.host.contains(evt.target as Node)) {
const callback = this.host.props.onDrag;
if (!this.host?.contains(evt.target as Node)) {
const callback = this.logic.root.config.onDrag;
if (callback) {
this.dropPreview.remove();
return {
@@ -66,7 +66,7 @@ export class TableDragController implements ReactiveController {
return;
}
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);
}
},
@@ -88,9 +88,9 @@ export class TableDragController implements ReactiveController {
| undefined => {
const y = evt.y;
const tableRect = this.host
.querySelector('affine-data-view-table-group')
?.querySelector('affine-data-view-table-group')
?.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) {
return;
}
@@ -124,21 +124,23 @@ export class TableDragController implements ReactiveController {
return position;
};
constructor(private readonly host: DataViewTable) {
this.host.addController(this);
constructor(private readonly logic: TableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
if (this.host.props.view.readonly$.value) {
return;
}
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
this.host?.disposables.add(
this.logic.handleEvent('dragStart', context => {
if (this.logic.view.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
const target = event.target;
if (
target instanceof Element &&
this.host.contains(target) &&
this.host?.contains(target) &&
target.closest('.data-view-table-view-drag-handler')
) {
event.preventDefault();
@@ -158,10 +160,9 @@ export class TableDragController implements ReactiveController {
const createDragPreview = (row: TableRowView, x: number, y: number) => {
const div = document.createElement('div');
const cloneRow = new TableRowView();
cloneRow.view = row.view;
cloneRow.rowIndex = row.rowIndex;
cloneRow.rowId = row.rowId;
cloneRow.dataViewEle = row.dataViewEle;
cloneRow.tableViewLogic = row.tableViewLogic;
div.append(cloneRow);
div.className = 'with-data-view-css-variable';
div.style.width = `${row.getBoundingClientRect().width}px`;
@@ -3,20 +3,22 @@ import type { ReactiveController } from 'lit';
import { TableViewAreaSelection, TableViewRowSelection } from '../../selection';
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 {
get selectionController() {
return this.host.selectionController;
return this.logic.selectionController;
}
constructor(private readonly host: DataViewTable) {
this.host.addController(this);
constructor(private readonly logic: TableViewUILogic) {}
get host() {
return this.logic.ui$.value;
}
hostConnected() {
this.host.disposables.add(
this.host.props.bindHotkey({
this.host?.disposables.add(
this.logic.bindHotkey({
Backspace: () => {
const selection = this.selectionController.selection;
if (!selection) {
@@ -25,7 +27,7 @@ export class TableHotkeysController implements ReactiveController {
if (TableViewRowSelection.is(selection)) {
const rows = TableViewRowSelection.rowsIds(selection);
this.selectionController.selection = undefined;
this.host.props.view.rowsDelete(rows);
this.logic.view.rowsDelete(rows);
return;
}
const {
@@ -334,14 +336,14 @@ export class TableHotkeysController implements ReactiveController {
context.get('keyboardState').raw.preventDefault();
this.selectionController.selection = TableViewRowSelection.create({
rows:
this.host.props.view.groupTrait.groupsDataList$.value?.flatMap(
this.logic.view.groupTrait.groupsDataList$.value?.flatMap(
group =>
group?.rows.map(row => ({
groupKey: group.key,
id: row.rowId,
})) ?? []
) ??
this.host.props.view.rows$.value.map(row => ({
this.logic.view.rows$.value.map(row => ({
groupKey: undefined,
id: row.rowId,
})),
@@ -377,7 +379,7 @@ export class TableHotkeysController implements ReactiveController {
rows: [row],
});
popRowMenu(
this.host.props.dataViewEle,
this.logic,
popupTargetFromElement(cell),
this.selectionController
);
@@ -18,10 +18,10 @@ import {
type TableViewSelection,
type TableViewSelectionWithType,
} from '../../selection';
import type { DatabaseCellContainer } from '../cell.js';
import type { TableViewCellContainer } from '../cell.js';
import type { TableGroup } from '../group.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 {
DragToFillElement,
fillSelectionWithFocusCellData,
@@ -87,41 +87,44 @@ export class TableSelectionController implements ReactiveController {
);
const cell = container?.cell;
const isEditing = cell ? cell.beforeEnterEditMode() : true;
this.host.props.setSelection({
this.logic.setSelection({
...selection,
isEditing,
});
} else {
this.host.props.setSelection(selection);
this.logic.setSelection(selection);
}
}
get tableContainer() {
return this.host.querySelector('.affine-database-table-container');
return this.logic.tableContainer$.value;
}
get view() {
return this.host.props.view;
return this.logic.view;
}
get viewData() {
return this.view;
}
constructor(public host: DataViewTable) {
host.addController(this);
constructor(public logic: TableViewUILogic) {
this.__selectionElement = new SelectionElement();
this.__selectionElement.controller = this;
}
get host() {
return this.logic.ui$.value;
}
private clearSelection() {
this.host.props.setSelection();
this.logic.setSelection();
}
private handleDragEvent() {
this.host.disposables.add(
this.host.props.handleEvent('dragStart', context => {
if (this.host.props.view.readonly$.value) {
this.host?.disposables.add(
this.logic.handleEvent('dragStart', context => {
if (this.logic.view.readonly$.value) {
return;
}
const event = context.get('pointerState').raw;
@@ -152,8 +155,8 @@ export class TableSelectionController implements ReactiveController {
}
private handleSelectionChange() {
this.host.disposables.add(
this.host.props.selection$.subscribe(tableSelection => {
this.host?.disposables.add(
this.logic.selection$.subscribe(tableSelection => {
if (!this.isValidSelection(tableSelection)) {
this.selection = undefined;
return;
@@ -238,7 +241,7 @@ export class TableSelectionController implements ReactiveController {
? this.view.groupTrait.groupDataMap$.value?.[groupKey]?.rows
: this.view.rows$.value;
requestAnimationFrame(() => {
const index = this.host.props.view.properties$.value.findIndex(
const index = this.logic.view.properties$.value.findIndex(
v => v.type$.value === 'title'
);
this.selection = TableViewAreaSelection.create({
@@ -254,14 +257,14 @@ export class TableSelectionController implements ReactiveController {
private resolveDragStartTarget(
target: HTMLElement
): [cell: DatabaseCellContainer | null, fillValues: boolean] {
let cell: DatabaseCellContainer | null;
): [cell: TableViewCellContainer | null, fillValues: boolean] {
let cell: TableViewCellContainer | null;
const fillValues = !!target.dataset.dragToFill;
if (fillValues) {
const focusCellContainer = this.getFocusCellContainer();
cell = focusCellContainer ?? null;
} else {
cell = target.closest('affine-database-cell-container');
cell = target.closest('dv-table-view-cell-container');
}
return [cell, fillValues];
}
@@ -295,7 +298,7 @@ export class TableSelectionController implements ReactiveController {
const rows = this.rows(groupKey);
const cells = rows
?.item(0)
.querySelectorAll('affine-database-cell-container');
.querySelectorAll('dv-table-view-cell-container');
return (x1: number, x2: number, y1: number, y2: number) => {
const rowOffsets: number[] = Array.from(rows ?? []).map(
@@ -394,7 +397,7 @@ export class TableSelectionController implements ReactiveController {
?.querySelectorAll('data-view-table-row') ?? []
);
const cells = Array.from(
row?.querySelectorAll('affine-database-cell-container') ?? []
row?.querySelectorAll('dv-table-view-cell-container') ?? []
);
if (!row || !rows || !cells) {
return;
@@ -432,7 +435,7 @@ export class TableSelectionController implements ReactiveController {
}
}
rows[rowIndex]
?.querySelectorAll('affine-database-cell-container')
?.querySelectorAll('dv-table-view-cell-container')
?.item(columnIndex)
?.selectCurrentCell(false);
}
@@ -441,10 +444,10 @@ export class TableSelectionController implements ReactiveController {
groupKey: string | undefined,
rowIndex: number,
columnIndex: number
): DatabaseCellContainer | undefined {
): TableViewCellContainer | undefined {
const row = this.rows(groupKey)?.item(rowIndex);
return row
?.querySelectorAll('affine-database-cell-container')
?.querySelectorAll('dv-table-view-cell-container')
.item(columnIndex);
}
@@ -479,7 +482,7 @@ export class TableSelectionController implements ReactiveController {
if (!topRow || !bottomRow) {
return;
}
const topCells = topRow.querySelectorAll('affine-database-cell-container');
const topCells = topRow.querySelectorAll('dv-table-view-cell-container');
const leftCell = topCells.item(left);
const rightCell = topCells.item(right);
if (!leftCell || !rightCell) {
@@ -752,7 +755,7 @@ export class TableSelectionController implements ReactiveController {
const max =
(this.rows(newSelection.groupKey)
?.item(0)
.querySelectorAll('affine-database-cell-container').length ?? 0) - 1;
.querySelectorAll('dv-table-view-cell-container').length ?? 0) - 1;
newSelection.columnsSelection.end = Math.min(
max,
newSelection.columnsSelection.end + 1
@@ -810,7 +813,7 @@ export class TableSelectionController implements ReactiveController {
startDrag(
evt: PointerEvent,
cell: DatabaseCellContainer,
cell: TableViewCellContainer,
fillValues?: boolean
) {
const groupKey = cell.closest<TableGroup>('affine-data-view-table-group')
@@ -882,7 +885,7 @@ export class TableSelectionController implements ReactiveController {
if (fillValues && this.selection) {
this.__dragToFillElement.dragging = false;
fillSelectionWithFocusCellData(
this.host,
this.logic,
TableViewAreaSelection.create({
groupKey: groupKey,
rowsSelection: selection.row,
@@ -997,7 +1000,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
selectionRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
get selection$() {
return this.controller.host.props.selection$;
return this.controller.logic.selection$;
}
clearAreaStyle() {
@@ -1049,7 +1052,7 @@ export class SelectionElement extends WithDisposable(ShadowlessElement) {
this.cancelSelectionUpdate();
if (
selection?.selectionType === 'area' &&
!this.controller.host.props.view.readonly$.value
!this.controller.logic.view.readonly$.value
) {
this.updateAreaSelectionStyle(
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 { SelectionElement } from './controller/selection.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 { TableRowView } from './row/row.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() {
customElements.define('affine-database-table', DataViewTable);
customElements.define('dv-table-view-ui', TableViewUI);
customElements.define('affine-data-view-table-group', TableGroup);
customElements.define(
'affine-database-cell-container',
DatabaseCellContainer
);
customElements.define('dv-table-view-cell-container', TableViewCellContainer);
customElements.define('affine-database-column-header', DatabaseColumnHeader);
customElements.define(
'affine-data-view-column-preview',
@@ -12,7 +12,6 @@ import { css, html, unsafeCSS } from 'lit';
import { property, query } from 'lit/decorators.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 type { Group } from '../../../core/group-by/trait.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 { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
import { TableViewAreaSelection } from '../selection';
import type { TableSingleView } from '../table-view-manager.js';
import { DataViewColumnPreview } from './header/column-renderer.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`
affine-data-view-table-group:hover .group-header-op {
@@ -71,7 +69,7 @@ export class TableGroup extends SignalWatcher(
private readonly clickAddRow = () => {
this.view.rowAdd('end', this.group?.key);
const selectionController = this.viewEle.selectionController;
const selectionController = this.tableViewLogic.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
@@ -90,7 +88,7 @@ export class TableGroup extends SignalWatcher(
private readonly clickAddRowInStart = () => {
this.view.rowAdd('start', this.group?.key);
const selectionController = this.viewEle.selectionController;
const selectionController = this.tableViewLogic.selectionController;
selectionController.selection = undefined;
requestAnimationFrame(() => {
const index = this.view.properties$.value.findIndex(
@@ -152,9 +150,6 @@ export class TableGroup extends SignalWatcher(
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor view!: TableSingleView;
dndContext = createDndContext({
activators: defaultActivators,
container: this,
@@ -187,6 +182,7 @@ export class TableGroup extends SignalWatcher(
preview.column = column;
preview.group = this.group;
preview.container = this;
preview.tableViewLogic = this.tableViewLogic;
preview.style.position = 'absolute';
preview.style.zIndex = '999';
const scale = this.dndContext.scale$.value;
@@ -246,7 +242,7 @@ export class TableGroup extends SignalWatcher(
return html`
<affine-database-column-header
.renderGroupHeader="${this.renderGroupHeader}"
.tableViewManager="${this.view}"
.tableViewLogic="${this.tableViewLogic}"
></affine-database-column-header>
<div class="affine-database-block-rows">
${repeat(
@@ -256,8 +252,7 @@ export class TableGroup extends SignalWatcher(
return html` <data-view-table-row
data-row-index="${idx}"
data-row-id="${row.rowId}"
.dataViewEle="${this.dataViewEle}"
.view="${this.view}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${row.rowId}"
.rowIndex="${idx}"
></data-view-table-row>`;
@@ -278,7 +273,10 @@ export class TableGroup extends SignalWatcher(
${PlusIcon()}<span style="font-size: 12px">New Record</span>
</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>
`;
}
@@ -292,14 +290,15 @@ export class TableGroup extends SignalWatcher(
return this.renderRows(this.rows);
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
@query('.affine-database-block-rows')
accessor rowsContainer: HTMLElement | null = null;
@property({ attribute: false })
accessor viewEle!: DataViewTable;
accessor tableViewLogic!: TableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
}
declare global {
@@ -9,8 +9,10 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.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 { tableStyle } from '../table-view-style';
import { type TableViewUILogic } from '../table-view-ui-logic.js';
import { styles } from './styles.js';
export class DatabaseColumnHeader extends SignalWatcher(
@@ -31,7 +33,6 @@ export class DatabaseColumnHeader extends SignalWatcher(
editLastColumnTitle = () => {
const columns = this.querySelectorAll('affine-database-header-column');
const column = columns.item(columns.length - 1);
column.scrollIntoView({ block: 'nearest', inline: 'nearest' });
column.editTitle();
};
@@ -88,7 +89,7 @@ export class DatabaseColumnHeader extends SignalWatcher(
<div class="affine-database-column-header database-row">
${this.readonly
? nothing
: html`<div class="data-view-table-left-bar"></div>`}
: html`<div class="${tableStyle.leftToolBarStyle}"></div>`}
${repeat(
this.tableViewManager.properties$.value,
column => column.id,
@@ -104,9 +105,9 @@ export class DatabaseColumnHeader extends SignalWatcher(
data-column-index="${index}"
class="affine-database-column database-cell"
.column="${column}"
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
></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;
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: TableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
declare global {
@@ -13,6 +13,7 @@ import type {
TableProperty,
TableSingleView,
} from '../../table-view-manager.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class DataViewColumnPreview extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -27,7 +28,7 @@ export class DataViewColumnPreview extends SignalWatcher(
`;
get tableViewManager(): TableSingleView {
return this.column.view as TableSingleView;
return this.tableViewLogic.view;
}
private renderGroup(rows: Row[]) {
@@ -39,12 +40,12 @@ export class DataViewColumnPreview extends SignalWatcher(
)};box-shadow: var(--affine-shadow-2);"
>
<affine-database-header-column
.tableViewManager="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.column="${this.column}"
></affine-database-header-column>
${repeat(rows, (id, index) => {
const height = this.container.querySelector(
`affine-database-cell-container[data-row-id="${id}"]`
`dv-table-view-cell-container[data-row-id="${id}"]`
)?.clientHeight;
const style = styleMap({
height: height + 'px',
@@ -55,14 +56,14 @@ export class DataViewColumnPreview extends SignalWatcher(
)}"
>
<div style="${style}">
<affine-database-cell-container
<dv-table-view-cell-container
.column="${this.column}"
.view="${this.tableViewManager}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${id}"
.columnId="${this.column.id}"
.rowIndex="${index}"
.columnIndex="${columnIndex}"
></affine-database-cell-container>
></dv-table-view-cell-container>
</div>
</div>`;
})}
@@ -85,6 +86,9 @@ export class DataViewColumnPreview extends SignalWatcher(
@property({ attribute: false })
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor tableViewLogic!: TableViewUILogic;
}
declare global {
@@ -45,10 +45,8 @@ import {
ShowQuickSettingBarKey,
} from '../../../../widget-presets/quick-setting-bar/context.js';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js';
import type {
TableProperty,
TableSingleView,
} from '../../table-view-manager.js';
import type { TableProperty } from '../../table-view-manager.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
import {
getTableGroupRect,
getVerticalIndicator,
@@ -173,7 +171,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
const sortUtils = createSortUtils(
sortTrait,
this.closest('affine-data-view-renderer')?.view?.eventTrace ?? (() => {})
this.tableViewLogic.eventTrace
);
const sortList = sortUtils.sortList$.value;
const existingIndex = sortList.findIndex(
@@ -398,10 +396,10 @@ export class DatabaseHeaderColumn extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
const table = this.closest('affine-database-table');
const table = this.closest('dv-table-view-ui');
if (table) {
this.disposables.add(
table.props.handleEvent('dragStart', context => {
table.logic.handleEvent('dragStart', context => {
if (this.tableViewManager.readonly$.value) {
return;
}
@@ -481,7 +479,11 @@ export class DatabaseHeaderColumn extends SignalWatcher(
accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd';
@property({ attribute: false })
accessor tableViewManager!: TableSingleView;
accessor tableViewLogic!: TableViewUILogic;
get tableViewManager() {
return this.tableViewLogic.view;
}
}
function numberFormatConfig(column: Property): MenuConfig {
@@ -12,19 +12,19 @@ import {
} from '@blocksuite/icons/lit';
import { html } from 'lit';
import type { DataViewRenderer } from '../../../core/data-view.js';
import { TableViewRowSelection } from '../selection';
import type { TableSelectionController } from './controller/selection.js';
import type { TableViewUILogic } from './table-view-ui-logic.js';
export const openDetail = (
dataViewEle: DataViewRenderer,
tableViewLogic: TableViewUILogic,
rowId: string,
selection: TableSelectionController
) => {
const old = selection.selection;
selection.selection = undefined;
dataViewEle.openDetailPanel({
view: selection.host.props.view,
tableViewLogic.root.openDetailPanel({
view: selection.logic.view,
rowId: rowId,
onClose: () => {
selection.selection = old;
@@ -33,7 +33,7 @@ export const openDetail = (
};
export const popRowMenu = (
dataViewEle: DataViewRenderer,
tableViewLogic: TableViewUILogic,
ele: PopupTarget,
selectionController: TableSelectionController
) => {
@@ -55,7 +55,7 @@ export const popRowMenu = (
${CopyIcon()}
</div>`,
select: () => {
selectionController.host.clipboardController.copy();
selectionController.logic.clipboardController.copy();
},
}),
],
@@ -85,7 +85,7 @@ export const popRowMenu = (
name: 'Expand Row',
prefix: ExpandFullIcon(),
select: () => {
openDetail(dataViewEle, row.id, selectionController);
openDetail(tableViewLogic, row.id, selectionController);
},
}),
menu.group({
@@ -1,15 +1,13 @@
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { CheckBoxCheckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit';
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 { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import {
TableViewRowSelection,
type TableViewSelectionWithType,
} from '../../selection';
import { TableViewRowSelection } from '../../selection';
import type { TableViewUILogic } from '../table-view-ui-logic';
export class RowSelectCheckbox extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -42,10 +40,10 @@ export class RowSelectCheckbox extends SignalWatcher(
accessor rowId!: string;
@property({ attribute: false })
accessor selection!: ReadonlySignal<TableViewSelectionWithType | undefined>;
accessor tableViewLogic!: TableViewUILogic;
isSelected$ = computed(() => {
const selection = this.selection.value;
const selection = this.tableViewLogic.selection$.value;
if (!selection || selection.selectionType !== 'row') {
return false;
}
@@ -58,7 +56,7 @@ export class RowSelectCheckbox extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
this.disposables.addFromEvent(this, 'click', () => {
this.closest('affine-database-table')?.selectionController.toggleRow(
this.tableViewLogic.selectionController.toggleRow(
this.rowId,
this.groupKey
);
@@ -9,14 +9,14 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
import type { DataViewRenderer } from '../../../../core/data-view.js';
import {
TableViewRowSelection,
type TableViewSelection,
} from '../../selection';
import type { TableSingleView } from '../../table-view-manager.js';
import { cellDivider } from '../../styles';
import type { TableGroup } from '../group.js';
import { openDetail, popRowMenu } from '../menu.js';
import type { TableViewUILogic } from '../table-view-ui-logic.js';
export class TableRowView extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -138,7 +138,7 @@ export class TableRowView extends SignalWatcher(
}
e.preventDefault();
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 };
if (!TableViewRowSelection.includes(selection.selection, row)) {
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);
popRowMenu(this.dataViewEle, popupTargetFromElement(target), selection);
popRowMenu(this.tableViewLogic, popupTargetFromElement(target), selection);
};
setSelection = (selection?: TableViewSelection) => {
@@ -164,7 +164,7 @@ export class TableRowView extends SignalWatcher(
}
get selectionController() {
return this.closest('affine-database-table')?.selectionController;
return this.tableViewLogic.selectionController;
}
override connectedCallback() {
@@ -193,7 +193,7 @@ export class TableRowView extends SignalWatcher(
></div>
</div>
<row-select-checkbox
.selection="${this.dataViewEle.config.selection$}"
.tableViewLogic="${this.tableViewLogic}"
.rowId="${this.rowId}"
.groupKey="${this.groupKey}"
></row-select-checkbox>
@@ -212,7 +212,11 @@ export class TableRowView extends SignalWatcher(
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) => {
if (!this.selectionController) {
@@ -234,20 +238,20 @@ export class TableRowView extends SignalWatcher(
);
}
popRowMenu(
this.dataViewEle,
this.tableViewLogic,
popupTargetFromElement(ele),
this.selectionController
);
};
return html`
<div style="display: flex;">
<affine-database-cell-container
<dv-table-view-cell-container
class="database-cell"
style=${styleMap({
width: `${column.width$.value}px`,
border: i === 0 ? 'none' : undefined,
})}
.view="${view}"
.tableViewLogic="${this.tableViewLogic}"
.column="${column}"
.rowId="${this.rowId}"
data-row-id="${this.rowId}"
@@ -258,8 +262,8 @@ export class TableRowView extends SignalWatcher(
.columnIndex="${i}"
data-column-index="${i}"
>
</affine-database-cell-container>
<div class="cell-divider"></div>
</dv-table-view-cell-container>
<div class="${cellDivider}"></div>
</div>
${!column.readonly$.value &&
column.view.mainProperties$.value.titleColumn === column.id
@@ -282,7 +286,7 @@ export class TableRowView extends SignalWatcher(
}
@property({ attribute: false })
accessor dataViewEle!: DataViewRenderer;
accessor tableViewLogic!: TableViewUILogic;
@property({ attribute: false })
accessor rowId!: string;
@@ -290,8 +294,9 @@ export class TableRowView extends SignalWatcher(
@property({ attribute: false })
accessor rowIndex!: number;
@property({ attribute: false })
accessor view!: TableSingleView;
get view() {
return this.tableViewLogic.view;
}
}
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/effect.js';
import { createUniComponentFromWebComponent } from '../../core/utils/uni-component/uni-component.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import { tableViewModel } from './define.js';
import { MobileDataViewTable } from './mobile/table-view.js';
import { TableViewSelector } from './table-view-selector.js';
import { MobileTableViewUILogic } from './mobile/table-view-ui-logic.js';
import { TableViewUILogic } from './pc/table-view-ui-logic.js';
import { VirtualTableViewUILogic } from './pc-virtual/table-view-ui-logic';
export const tableViewMeta = tableViewModel.createMeta({
view: createUniComponentFromWebComponent(TableViewSelector),
mobileView: createUniComponentFromWebComponent(MobileDataViewTable),
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 { 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`
affine-database-column-stats {
@@ -41,7 +41,11 @@ export class DataBaseColumnStats extends SignalWatcher(
accessor group: Group | undefined = undefined;
@property({ attribute: false })
accessor view!: TableSingleView;
accessor tableViewLogic!: TableViewUILogic;
get view() {
return this.tableViewLogic.view;
}
}
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;
}
}