mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +08:00
feat(editor): simple table block (#9740)
close: BS-2122, BS-2125, BS-2124, BS-2420, PD-2073, BS-2126, BS-2469, BS-2470, BS-2478, BS-2471
This commit is contained in:
@@ -6,162 +6,17 @@ import {
|
||||
import {
|
||||
BlockHtmlAdapterExtension,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
HastUtils,
|
||||
type InlineHtmlAST,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import type { Element } from 'hast';
|
||||
|
||||
import { processTable } from './utils';
|
||||
|
||||
const DATABASE_NODE_TYPES = new Set(['table', 'thead', 'tbody', 'th', 'tr']);
|
||||
|
||||
export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
flavour: DatabaseBlockSchema.model.flavour,
|
||||
toMatch: o =>
|
||||
HastUtils.isElement(o.node) && DATABASE_NODE_TYPES.has(o.node.tagName),
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
if (o.node.tagName === 'table') {
|
||||
const tableHeader = HastUtils.querySelector(o.node, 'thead');
|
||||
if (!tableHeader) {
|
||||
return;
|
||||
}
|
||||
const tableHeaderRow = HastUtils.querySelector(tableHeader, 'tr');
|
||||
if (!tableHeaderRow) {
|
||||
return;
|
||||
}
|
||||
// Table header row as database header row
|
||||
const viewsColumns = tableHeaderRow.children.map(() => {
|
||||
return {
|
||||
id: nanoid(),
|
||||
hide: false,
|
||||
width: 180,
|
||||
};
|
||||
});
|
||||
|
||||
// Build database cells from table body rows
|
||||
const cells = Object.create(null);
|
||||
const tableBody = HastUtils.querySelector(o.node, 'tbody');
|
||||
tableBody?.children.forEach(row => {
|
||||
const rowId = nanoid();
|
||||
cells[rowId] = Object.create(null);
|
||||
(row as Element).children.forEach((cell, index) => {
|
||||
const column = viewsColumns[index];
|
||||
if (!column) {
|
||||
return;
|
||||
}
|
||||
cells[rowId][column.id] = {
|
||||
columnId: column.id,
|
||||
value: TextUtils.createText(
|
||||
(cell as Element).children
|
||||
.map(child => ('value' in child ? child.value : ''))
|
||||
.join('')
|
||||
),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Build database columns from table header row
|
||||
const columns = tableHeaderRow.children.flatMap((_child, index) => {
|
||||
const column = viewsColumns[index];
|
||||
if (!column) {
|
||||
return [];
|
||||
}
|
||||
return {
|
||||
type: index === 0 ? 'title' : 'rich-text',
|
||||
name: (_child as Element).children
|
||||
.map(child => ('value' in child ? child.value : ''))
|
||||
.join(''),
|
||||
data: {},
|
||||
id: column.id,
|
||||
};
|
||||
});
|
||||
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:database',
|
||||
props: {
|
||||
views: [
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Table View',
|
||||
mode: 'table',
|
||||
columns: [],
|
||||
filter: {
|
||||
type: 'group',
|
||||
op: 'and',
|
||||
conditions: [],
|
||||
},
|
||||
header: {
|
||||
titleColumn: viewsColumns[0]?.id,
|
||||
iconColumn: 'type',
|
||||
},
|
||||
},
|
||||
],
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [],
|
||||
},
|
||||
cells,
|
||||
columns,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
);
|
||||
walkerContext.setNodeContext('affine:table:rowid', Object.keys(cells));
|
||||
walkerContext.skipChildren(1);
|
||||
}
|
||||
|
||||
// The first child of each table body row is the database title cell
|
||||
if (o.node.tagName === 'tr') {
|
||||
const { deltaConverter } = context;
|
||||
const firstChild = o.node.children[0];
|
||||
if (!firstChild) {
|
||||
return;
|
||||
}
|
||||
walkerContext
|
||||
.openNode({
|
||||
type: 'block',
|
||||
id:
|
||||
(
|
||||
walkerContext.getNodeContext(
|
||||
'affine:table:rowid'
|
||||
) as Array<string>
|
||||
).shift() ?? nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltaConverter.astToDelta(firstChild),
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
children: [],
|
||||
})
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
leave: (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
if (o.node.tagName === 'table') {
|
||||
walkerContext.closeNode();
|
||||
}
|
||||
},
|
||||
},
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
|
||||
@@ -7,9 +7,7 @@ import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
type MarkdownAST,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import type { TableRow } from 'mdast';
|
||||
|
||||
import { processTable } from './utils';
|
||||
@@ -24,123 +22,7 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
||||
flavour: DatabaseBlockSchema.model.flavour,
|
||||
toMatch: o => isDatabaseNode(o.node),
|
||||
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
if (o.node.type === 'table') {
|
||||
const viewsColumns = o.node.children[0]?.children.map(() => {
|
||||
return {
|
||||
id: nanoid(),
|
||||
hide: false,
|
||||
width: 180,
|
||||
};
|
||||
});
|
||||
const cells = Object.create(null);
|
||||
o.node.children.slice(1).forEach(row => {
|
||||
const rowId = nanoid();
|
||||
cells[rowId] = Object.create(null);
|
||||
row.children.slice(1).forEach((cell, index) => {
|
||||
const column = viewsColumns?.[index + 1];
|
||||
if (!column) {
|
||||
return;
|
||||
}
|
||||
cells[rowId][column.id] = {
|
||||
columnId: column.id,
|
||||
value: TextUtils.createText(
|
||||
cell.children
|
||||
.map(child => ('value' in child ? child.value : ''))
|
||||
.join('')
|
||||
),
|
||||
};
|
||||
});
|
||||
});
|
||||
const columns = o.node.children[0]?.children.map((_child, index) => {
|
||||
return {
|
||||
type: index === 0 ? 'title' : 'rich-text',
|
||||
name: _child.children
|
||||
.map(child => ('value' in child ? child.value : ''))
|
||||
.join(''),
|
||||
data: {},
|
||||
id: viewsColumns?.[index]?.id,
|
||||
};
|
||||
});
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:database',
|
||||
props: {
|
||||
views: [
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Table View',
|
||||
mode: 'table',
|
||||
columns: [],
|
||||
filter: {
|
||||
type: 'group',
|
||||
op: 'and',
|
||||
conditions: [],
|
||||
},
|
||||
header: {
|
||||
titleColumn: viewsColumns?.[0]?.id,
|
||||
iconColumn: 'type',
|
||||
},
|
||||
},
|
||||
],
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [],
|
||||
},
|
||||
cells,
|
||||
columns,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
);
|
||||
walkerContext.setNodeContext(
|
||||
'affine:table:rowid',
|
||||
Object.keys(cells)
|
||||
);
|
||||
walkerContext.skipChildren(1);
|
||||
}
|
||||
|
||||
if (o.node.type === 'tableRow') {
|
||||
const { deltaConverter } = context;
|
||||
const firstChild = o.node.children[0];
|
||||
if (!firstChild) {
|
||||
return;
|
||||
}
|
||||
walkerContext
|
||||
.openNode({
|
||||
type: 'block',
|
||||
id:
|
||||
(
|
||||
walkerContext.getNodeContext(
|
||||
'affine:table:rowid'
|
||||
) as Array<string>
|
||||
).shift() ?? nanoid(),
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: deltaConverter.astToDelta(firstChild),
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
children: [],
|
||||
})
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
leave: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
if (o.node.type === 'table') {
|
||||
walkerContext.closeNode();
|
||||
}
|
||||
},
|
||||
},
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext, deltaConverter } = context;
|
||||
|
||||
@@ -334,11 +334,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
|
||||
this._dataSource.contextSet(HostContextKey, this.host);
|
||||
const id = currentViewStorage.getCurrentView(this.model.id);
|
||||
if (id && this.dataSource.viewManager.viewGet(id)) {
|
||||
console.log(
|
||||
'set current view',
|
||||
id,
|
||||
this._dataSource.viewManager.viewGet(id)
|
||||
);
|
||||
this.dataSource.viewManager.setCurrentView(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +518,8 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
|
||||
override render() {
|
||||
return html`<rich-text
|
||||
data-disable-ask-ai
|
||||
data-not-block-text
|
||||
.yText=${this.value}
|
||||
.inlineEventSource=${this.topContenteditableElement}
|
||||
.attributesSchema=${this.attributesSchema}
|
||||
|
||||
@@ -388,6 +388,8 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
|
||||
|
||||
override renderBlockText() {
|
||||
return html` <rich-text
|
||||
data-disable-ask-ai
|
||||
data-not-block-text
|
||||
.yText="${this.value}"
|
||||
.inlineEventSource="${this.topContenteditableElement}"
|
||||
.attributesSchema="${this.attributesSchema}"
|
||||
@@ -407,6 +409,8 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
|
||||
|
||||
override renderLinkedDoc(): TemplateResult {
|
||||
return html` <rich-text
|
||||
data-disable-ask-ai
|
||||
data-not-block-text
|
||||
.yText="${this.linkedDocTitle$.value}"
|
||||
.inlineEventSource="${this.topContenteditableElement}"
|
||||
.readonly="${this.readonly}"
|
||||
|
||||
Reference in New Issue
Block a user