mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-11 22:18:54 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { computed, effect } from '@preact/signals-core';
|
||||
import { css } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { createRef } from 'lit/directives/ref.js';
|
||||
|
||||
import {
|
||||
type CellRenderProps,
|
||||
type DataViewCellLifeCycle,
|
||||
renderUniLit,
|
||||
type SingleView,
|
||||
} from '../../../core/index.js';
|
||||
import type { TableColumn } from '../table-view-manager.js';
|
||||
import { TableAreaSelection } from '../types.js';
|
||||
|
||||
export class MobileTableCell extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
mobile-table-cell {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
mobile-table-cell * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
mobile-table-cell uni-lit > *:first-child {
|
||||
padding: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
private _cell = createRef<DataViewCellLifeCycle>();
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor column!: TableColumn;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowId!: string;
|
||||
|
||||
cell$ = computed(() => {
|
||||
return this.column.cellGet(this.rowId);
|
||||
});
|
||||
|
||||
isEditing$ = computed(() => {
|
||||
const selection = this.table?.props.selection$.value;
|
||||
if (selection?.selectionType !== 'area') {
|
||||
return false;
|
||||
}
|
||||
if (selection.groupKey !== this.groupKey) {
|
||||
return false;
|
||||
}
|
||||
if (selection.focus.columnIndex !== this.columnIndex) {
|
||||
return false;
|
||||
}
|
||||
if (selection.focus.rowIndex !== this.rowIndex) {
|
||||
return false;
|
||||
}
|
||||
return selection.isEditing;
|
||||
});
|
||||
|
||||
selectCurrentCell = (editing: boolean) => {
|
||||
if (this.view.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
const setSelection = this.table?.props.setSelection;
|
||||
const viewId = this.table?.props.view.id;
|
||||
if (setSelection && viewId) {
|
||||
if (editing && this.cell?.beforeEnterEditMode() === false) {
|
||||
return;
|
||||
}
|
||||
setSelection({
|
||||
viewId,
|
||||
type: 'table',
|
||||
...TableAreaSelection.create({
|
||||
groupKey: this.groupKey,
|
||||
focus: {
|
||||
rowIndex: this.rowIndex,
|
||||
columnIndex: this.columnIndex,
|
||||
},
|
||||
isEditing: editing,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
get cell(): DataViewCellLifeCycle | undefined {
|
||||
return this._cell.value;
|
||||
}
|
||||
|
||||
private get groupKey() {
|
||||
return this.closest('mobile-table-group')?.group?.key;
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.column.readonly$.value;
|
||||
}
|
||||
|
||||
private get table() {
|
||||
return this.closest('mobile-data-view-table');
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.column.readonly$.value) return;
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const isEditing = this.isEditing$.value;
|
||||
if (isEditing) {
|
||||
this.isEditing = true;
|
||||
this._cell.value?.onEnterEditMode();
|
||||
} else {
|
||||
this._cell.value?.onExitEditMode();
|
||||
this.isEditing = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
this.disposables.addFromEvent(this, 'click', () => {
|
||||
if (!this.isEditing) {
|
||||
this.selectCurrentCell(!this.column.readonly$.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const renderer = this.column.renderer$.value;
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
const { edit, view } = renderer;
|
||||
const uni = !this.readonly && this.isEditing && edit != null ? edit : view;
|
||||
this.view.lockRows(this.isEditing);
|
||||
this.dataset['editing'] = `${this.isEditing}`;
|
||||
const props: CellRenderProps = {
|
||||
cell: this.cell$.value,
|
||||
isEditing: this.isEditing,
|
||||
selectCurrentCell: this.selectCurrentCell,
|
||||
};
|
||||
|
||||
return renderUniLit(uni, props, {
|
||||
ref: this._cell,
|
||||
style: {
|
||||
display: 'contents',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor columnId!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor columnIndex!: number;
|
||||
|
||||
@state()
|
||||
accessor isEditing = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowIndex!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: SingleView;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-table-cell': MobileTableCell;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import {
|
||||
menu,
|
||||
type MenuConfig,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import {
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
InsertLeftIcon,
|
||||
InsertRightIcon,
|
||||
MoveLeftIcon,
|
||||
MoveRightIcon,
|
||||
ViewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { css } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { inputConfig, typeConfig } from '../../../core/common/property-menu.js';
|
||||
import type { Property } from '../../../core/view-manager/property.js';
|
||||
import type { NumberPropertyDataType } from '../../../property-presets/index.js';
|
||||
import { numberFormats } from '../../../property-presets/number/utils/formats.js';
|
||||
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../consts.js';
|
||||
import type { TableColumn, TableSingleView } from '../table-view-manager.js';
|
||||
|
||||
export class MobileTableColumnHeader extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.mobile-table-column-header {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-table-column-header-icon {
|
||||
font-size: 18px;
|
||||
color: ${unsafeCSSVarV2('database/textSecondary')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-table-column-header-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: ${unsafeCSSVarV2('database/textSecondary')};
|
||||
}
|
||||
`;
|
||||
|
||||
private _clickColumn = () => {
|
||||
if (this.tableViewManager.readonly$.value) {
|
||||
return;
|
||||
}
|
||||
this.popMenu();
|
||||
};
|
||||
|
||||
editTitle = () => {
|
||||
this._clickColumn();
|
||||
};
|
||||
|
||||
private popMenu(ele?: HTMLElement) {
|
||||
const enableNumberFormatting =
|
||||
this.tableViewManager.featureFlags$.value.enable_number_formatting;
|
||||
|
||||
popMenu(popupTargetFromElement(ele ?? this), {
|
||||
options: {
|
||||
title: {
|
||||
text: 'Property settings',
|
||||
},
|
||||
items: [
|
||||
inputConfig(this.column),
|
||||
typeConfig(this.column),
|
||||
// Number format begin
|
||||
...(enableNumberFormatting
|
||||
? [
|
||||
menu.subMenu({
|
||||
name: 'Number Format',
|
||||
hide: () =>
|
||||
!this.column.dataUpdate ||
|
||||
this.column.type$.value !== 'number',
|
||||
options: {
|
||||
title: {
|
||||
text: 'Number Format',
|
||||
},
|
||||
items: [
|
||||
numberFormatConfig(this.column),
|
||||
...numberFormats.map(format => {
|
||||
const data = (
|
||||
this.column as Property<
|
||||
number,
|
||||
NumberPropertyDataType
|
||||
>
|
||||
).data$.value;
|
||||
return menu.action({
|
||||
isSelected: data.format === format.type,
|
||||
prefix: html`<span
|
||||
style="font-size: var(--affine-font-base); scale: 1.2;"
|
||||
>${format.symbol}</span
|
||||
>`,
|
||||
name: format.label,
|
||||
select: () => {
|
||||
if (data.format === format.type) return;
|
||||
this.column.dataUpdate(() => ({
|
||||
format: format.type,
|
||||
}));
|
||||
},
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
// Number format end
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Hide In View',
|
||||
prefix: ViewIcon(),
|
||||
hide: () =>
|
||||
this.column.hide$.value ||
|
||||
this.column.type$.value === 'title',
|
||||
select: () => {
|
||||
this.column.hideSet(true);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Insert Left Column',
|
||||
prefix: InsertLeftIcon(),
|
||||
select: () => {
|
||||
this.tableViewManager.propertyAdd({
|
||||
id: this.column.id,
|
||||
before: true,
|
||||
});
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
const pre =
|
||||
this.previousElementSibling?.previousElementSibling;
|
||||
if (pre instanceof MobileTableColumnHeader) {
|
||||
pre.editTitle();
|
||||
pre.scrollIntoView({
|
||||
inline: 'nearest',
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Insert Right Column',
|
||||
prefix: InsertRightIcon(),
|
||||
select: () => {
|
||||
this.tableViewManager.propertyAdd({
|
||||
id: this.column.id,
|
||||
before: false,
|
||||
});
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
const next = this.nextElementSibling?.nextElementSibling;
|
||||
if (next instanceof MobileTableColumnHeader) {
|
||||
next.editTitle();
|
||||
next.scrollIntoView({
|
||||
inline: 'nearest',
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Move Left',
|
||||
prefix: MoveLeftIcon(),
|
||||
hide: () => this.column.isFirst,
|
||||
select: () => {
|
||||
const preId = this.tableViewManager.propertyPreGet(
|
||||
this.column.id
|
||||
)?.id;
|
||||
if (!preId) {
|
||||
return;
|
||||
}
|
||||
this.tableViewManager.propertyMove(this.column.id, {
|
||||
id: preId,
|
||||
before: true,
|
||||
});
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Move Right',
|
||||
prefix: MoveRightIcon(),
|
||||
hide: () => this.column.isLast,
|
||||
select: () => {
|
||||
const nextId = this.tableViewManager.propertyNextGet(
|
||||
this.column.id
|
||||
)?.id;
|
||||
if (!nextId) {
|
||||
return;
|
||||
}
|
||||
this.tableViewManager.propertyMove(this.column.id, {
|
||||
id: nextId,
|
||||
before: false,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Duplicate',
|
||||
prefix: DuplicateIcon(),
|
||||
hide: () =>
|
||||
!this.column.duplicate || this.column.type$.value === 'title',
|
||||
select: () => {
|
||||
this.column.duplicate?.();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete',
|
||||
prefix: DeleteIcon(),
|
||||
hide: () =>
|
||||
!this.column.delete || this.column.type$.value === 'title',
|
||||
select: () => {
|
||||
this.column.delete?.();
|
||||
},
|
||||
class: {
|
||||
'delete-item': true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const column = this.column;
|
||||
const style = styleMap({
|
||||
height: DEFAULT_COLUMN_TITLE_HEIGHT + 'px',
|
||||
});
|
||||
return html`
|
||||
<div
|
||||
style=${style}
|
||||
class="mobile-table-column-header"
|
||||
@click="${this._clickColumn}"
|
||||
>
|
||||
<uni-lit
|
||||
class="mobile-table-column-header-icon"
|
||||
.uni="${column.icon}"
|
||||
></uni-lit>
|
||||
<div class="mobile-table-column-header-name">${column.name$.value}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor column!: TableColumn;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewManager!: TableSingleView;
|
||||
}
|
||||
|
||||
function numberFormatConfig(column: Property): MenuConfig {
|
||||
return () =>
|
||||
html` <affine-database-number-format-bar
|
||||
.column="${column}"
|
||||
></affine-database-number-format-bar>`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-table-column-header': MobileTableColumnHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
menu,
|
||||
popFilterableSimpleMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { PlusIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html } 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 { GroupData } from '../../../core/group-by/trait.js';
|
||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||
import type { DataViewTable } from '../pc/table-view.js';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import { TableAreaSelection } from '../types.js';
|
||||
|
||||
const styles = css`
|
||||
.data-view-table-group-add-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid var(--affine-border-color);
|
||||
}
|
||||
|
||||
.data-view-table-group-add-row-button {
|
||||
position: sticky;
|
||||
left: ${8 + LEFT_TOOL_BAR_WIDTH}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
`;
|
||||
|
||||
export class MobileTableGroup extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = styles;
|
||||
|
||||
private clickAddRow = () => {
|
||||
this.view.rowAdd('end', this.group?.key);
|
||||
requestAnimationFrame(() => {
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
selectionController.selection = TableAreaSelection.create({
|
||||
groupKey: this.group?.key,
|
||||
focus: {
|
||||
rowIndex: this.rows.length - 1,
|
||||
columnIndex: index,
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private clickAddRowInStart = () => {
|
||||
this.view.rowAdd('start', this.group?.key);
|
||||
requestAnimationFrame(() => {
|
||||
const selectionController = this.viewEle.selectionController;
|
||||
const index = this.view.properties$.value.findIndex(
|
||||
v => v.type$.value === 'title'
|
||||
);
|
||||
selectionController.selection = TableAreaSelection.create({
|
||||
groupKey: this.group?.key,
|
||||
focus: {
|
||||
rowIndex: 0,
|
||||
columnIndex: index,
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private clickGroupOptions = (e: MouseEvent) => {
|
||||
const group = this.group;
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popFilterableSimpleMenu(popupTargetFromElement(ele), [
|
||||
menu.action({
|
||||
name: 'Ungroup',
|
||||
hide: () => group.value == null,
|
||||
select: () => {
|
||||
group.rows.forEach(id => {
|
||||
group.manager.removeFromGroup(id, group.key);
|
||||
});
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowDelete(group.rows);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
private renderGroupHeader = () => {
|
||||
if (!this.group) {
|
||||
return null;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
style="position: sticky;left: 0;width: max-content;padding: 6px 0;margin-bottom: 4px;display:flex;align-items:center;gap: 12px;max-width: 400px"
|
||||
>
|
||||
${GroupTitle(this.group, {
|
||||
readonly: this.view.readonly$.value,
|
||||
clickAdd: this.clickAddRowInStart,
|
||||
clickOps: this.clickGroupOptions,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
get rows() {
|
||||
return this.group?.rows ?? this.view.rows$.value;
|
||||
}
|
||||
|
||||
private renderRows(ids: string[]) {
|
||||
return html`
|
||||
<mobile-table-header
|
||||
.renderGroupHeader="${this.renderGroupHeader}"
|
||||
.tableViewManager="${this.view}"
|
||||
></mobile-table-header>
|
||||
<div class="mobile-affine-table-body">
|
||||
${repeat(
|
||||
ids,
|
||||
id => id,
|
||||
(id, idx) => {
|
||||
return html` <mobile-table-row
|
||||
data-row-index="${idx}"
|
||||
data-row-id="${id}"
|
||||
.dataViewEle="${this.dataViewEle}"
|
||||
.view="${this.view}"
|
||||
.rowId="${id}"
|
||||
.rowIndex="${idx}"
|
||||
></mobile-table-row>`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
${this.view.readonly$.value
|
||||
? null
|
||||
: html` <div
|
||||
class="data-view-table-group-add-row dv-hover"
|
||||
@click="${this.clickAddRow}"
|
||||
>
|
||||
<div
|
||||
class="data-view-table-group-add-row-button dv-icon-16"
|
||||
data-test-id="affine-database-add-row-button"
|
||||
role="button"
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this.renderRows(this.rows);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group: GroupData | undefined = undefined;
|
||||
|
||||
@query('.affine-database-block-rows')
|
||||
accessor rowsContainer: HTMLElement | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor viewEle!: DataViewTable;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-table-group': MobileTableGroup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { PlusIcon } from '@blocksuite/icons/lit';
|
||||
import { css, type TemplateResult } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
|
||||
export class MobileTableHeader extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.mobile-table-add-column {
|
||||
font-size: 18px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
private _onAddColumn = () => {
|
||||
if (this.readonly) return;
|
||||
this.tableViewManager.propertyAdd('end');
|
||||
this.editLastColumnTitle();
|
||||
};
|
||||
|
||||
editLastColumnTitle = () => {
|
||||
const columns = this.querySelectorAll('mobile-table-column-header');
|
||||
const column = columns.item(columns.length - 1);
|
||||
column.editTitle();
|
||||
};
|
||||
|
||||
private get readonly() {
|
||||
return this.tableViewManager.readonly$.value;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${this.renderGroupHeader?.()}
|
||||
<div class="mobile-table-header mobile-table-row">
|
||||
${repeat(
|
||||
this.tableViewManager.properties$.value,
|
||||
column => column.id,
|
||||
(column, index) => {
|
||||
const style = styleMap({
|
||||
width: `${column.width$.value}px`,
|
||||
border: index === 0 ? 'none' : undefined,
|
||||
});
|
||||
return html`
|
||||
<mobile-table-column-header
|
||||
style="${style}"
|
||||
data-column-id="${column.id}"
|
||||
data-column-index="${index}"
|
||||
class="mobile-table-cell"
|
||||
.column="${column}"
|
||||
.tableViewManager="${this.tableViewManager}"
|
||||
></mobile-table-column-header>
|
||||
<div class="cell-divider" style="height: auto;"></div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<div @click="${this._onAddColumn}" class="mobile-table-add-column">
|
||||
${PlusIcon()}
|
||||
</div>
|
||||
<div class="scale-div" style="width: 1px;height: 1px;"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor renderGroupHeader: (() => TemplateResult) | undefined;
|
||||
|
||||
@query('.scale-div')
|
||||
accessor scaleDiv!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tableViewManager!: TableSingleView;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-table-header': MobileTableHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
menu,
|
||||
popFilterableSimpleMenu,
|
||||
type PopupTarget,
|
||||
} 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';
|
||||
|
||||
export const popMobileRowMenu = (
|
||||
target: PopupTarget,
|
||||
rowId: string,
|
||||
dataViewEle: DataViewRenderer,
|
||||
view: SingleView
|
||||
) => {
|
||||
popFilterableSimpleMenu(target, [
|
||||
menu.group({
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Expand Row',
|
||||
prefix: ExpandFullIcon(),
|
||||
select: () => {
|
||||
dataViewEle.openDetailPanel({
|
||||
view: view,
|
||||
rowId: rowId,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
menu.group({
|
||||
name: '',
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Delete Row',
|
||||
class: { 'delete-item': true },
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
view.rowDelete([rowId]);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { CenterPeekIcon, MoreHorizontalIcon } from '@blocksuite/icons/lit';
|
||||
import { css, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { DataViewRenderer } from '../../../core/data-view.js';
|
||||
import type { TableSingleView } from '../table-view-manager.js';
|
||||
import { popMobileRowMenu } from './menu.js';
|
||||
|
||||
export class MobileTableRow extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.mobile-table-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid var(--affine-border-color);
|
||||
position: relative;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.mobile-row-ops {
|
||||
position: relative;
|
||||
width: 0;
|
||||
margin-top: 5px;
|
||||
height: max-content;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
justify-content: end;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.affine-database-block-row:has([data-editing='true']) .mobile-row-ops {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mobile-row-op {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 0px 4px 0px rgba(66, 65, 73, 0.14);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
}
|
||||
`;
|
||||
|
||||
get groupKey() {
|
||||
return this.closest('affine-data-view-table-group')?.group?.key;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.add('mobile-table-row');
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
const view = this.view;
|
||||
return html`
|
||||
${repeat(
|
||||
view.properties$.value,
|
||||
v => v.id,
|
||||
(column, i) => {
|
||||
const clickDetail = () => {
|
||||
this.dataViewEle.openDetailPanel({
|
||||
view: this.view,
|
||||
rowId: this.rowId,
|
||||
});
|
||||
};
|
||||
const openMenu = (e: MouseEvent) => {
|
||||
const ele = e.currentTarget as HTMLElement;
|
||||
popMobileRowMenu(
|
||||
popupTargetFromElement(ele),
|
||||
this.rowId,
|
||||
this.dataViewEle,
|
||||
this.view
|
||||
);
|
||||
};
|
||||
return html`
|
||||
<div style="display: flex;">
|
||||
<mobile-table-cell
|
||||
class="mobile-table-cell"
|
||||
style=${styleMap({
|
||||
width: `${column.width$.value}px`,
|
||||
border: i === 0 ? 'none' : undefined,
|
||||
})}
|
||||
.view="${view}"
|
||||
.column="${column}"
|
||||
.rowId="${this.rowId}"
|
||||
data-row-id="${this.rowId}"
|
||||
.rowIndex="${this.rowIndex}"
|
||||
data-row-index="${this.rowIndex}"
|
||||
.columnId="${column.id}"
|
||||
data-column-id="${column.id}"
|
||||
.columnIndex="${i}"
|
||||
data-column-index="${i}"
|
||||
>
|
||||
</mobile-table-cell>
|
||||
<div class="cell-divider"></div>
|
||||
</div>
|
||||
${!column.readonly$.value &&
|
||||
column.view.mainProperties$.value.titleColumn === column.id
|
||||
? html` <div class="mobile-row-ops">
|
||||
<div class="mobile-row-op" @click="${clickDetail}">
|
||||
${CenterPeekIcon()}
|
||||
</div>
|
||||
${!view.readonly$.value
|
||||
? html` <div class="mobile-row-op" @click="${openMenu}">
|
||||
${MoreHorizontalIcon()}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<div class="database-cell add-column-button"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewEle!: DataViewRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowId!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowIndex!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: TableSingleView;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mobile-table-row': MobileTableRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
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 { css } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type { 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 { TableSingleView } from '../table-view-manager.js';
|
||||
import type { TableViewSelectionWithType } from '../types.js';
|
||||
|
||||
export class MobileDataViewTable extends DataViewBase<
|
||||
TableSingleView,
|
||||
TableViewSelectionWithType
|
||||
> {
|
||||
static override styles = css`
|
||||
.mobile-affine-database-table-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 4px;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.mobile-affine-database-table-container {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.cell-divider {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--affine-border-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user