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:
zzj3720
2025-01-24 10:07:57 +00:00
parent 3f4311ff1c
commit 5a5779c05a
61 changed files with 3577 additions and 381 deletions

View File

@@ -25,6 +25,7 @@
"@toeverything/theme": "^1.1.7",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"fractional-indexing": "^3.2.0",
"lit": "^3.2.0",
"lodash.clonedeep": "^4.5.0",
"lodash.mergewith": "^4.6.2",

View File

@@ -14,7 +14,7 @@ import {
combinedLightCssVariables,
} from '@toeverything/theme';
import { isInsideEdgelessEditor } from '../utils/index.js';
import { isInsideEdgelessEditor } from '../utils/dom';
export const ThemeExtensionIdentifier = createIdentifier<ThemeExtension>(
'AffineThemeExtension'

View File

@@ -6,9 +6,10 @@ import {
type AffineTheme,
cssVar,
} from '@toeverything/theme';
export { cssVar } from '@toeverything/theme';
import { type AffineThemeKeyV2, cssVarV2 } from '@toeverything/theme/v2';
import { unsafeCSS } from 'lit';
export { cssVarV2 } from '@toeverything/theme/v2';
export const ColorVariables = [
'--affine-brand-color',
'--affine-primary-color',

View File

@@ -0,0 +1,82 @@
type OffsetList = number[];
type CellOffsets = {
rows: OffsetList;
columns: OffsetList;
};
export const domToOffsets = (
element: HTMLElement,
rowSelector: string,
cellSelector: string
): CellOffsets | undefined => {
const rowDoms = Array.from(element.querySelectorAll(rowSelector));
const firstRowDom = rowDoms[0];
if (!firstRowDom) return;
const columnDoms = Array.from(firstRowDom.querySelectorAll(cellSelector));
const rows: OffsetList = [];
const columns: OffsetList = [];
for (let i = 0; i < rowDoms.length; i++) {
const rect = rowDoms[i].getBoundingClientRect();
if (!rect) continue;
if (i === 0) {
rows.push(rect.top);
}
rows.push(rect.bottom);
}
for (let i = 0; i < columnDoms.length; i++) {
const rect = columnDoms[i].getBoundingClientRect();
if (!rect) continue;
if (i === 0) {
columns.push(rect.left);
}
columns.push(rect.right);
}
return {
rows,
columns,
};
};
export const getIndexByPosition = (
positions: OffsetList,
offset: number,
reverse = false
) => {
if (reverse) {
return positions.slice(1).findIndex(p => offset <= p);
}
return positions.slice(0, -1).findLastIndex(p => offset >= p);
};
export const getRangeByPositions = (
positions: OffsetList,
start: number,
end: number
) => {
const startIndex = getIndexByPosition(positions, start, true);
const endIndex = getIndexByPosition(positions, end);
return {
start: startIndex,
end: endIndex,
};
};
export const getAreaByOffsets = (
offsets: CellOffsets,
top: number,
bottom: number,
left: number,
right: number
) => {
const { rows, columns } = offsets;
const startRow = getIndexByPosition(rows, top, true);
const endRow = getIndexByPosition(rows, bottom);
const startColumn = getIndexByPosition(columns, left, true);
const endColumn = getIndexByPosition(columns, right);
return {
top: startRow,
bottom: endRow,
left: startColumn,
right: endColumn,
};
};

View File

@@ -0,0 +1,70 @@
import { generateKeyBetween } from 'fractional-indexing';
/**
* generate a key between a and b, the result key is always satisfied with a < result < b.
* the key always has a random suffix, so there is no need to worry about collision.
*
* make sure a and b are generated by this function.
*
* @param customPostfix custom postfix for the key, only letters and numbers are allowed
*/
export function generateFractionalIndexingKeyBetween(
a: string | null,
b: string | null
) {
const randomSize = 32;
function postfix(length: number = randomSize) {
const chars =
'123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const values = new Uint8Array(length);
crypto.getRandomValues(values);
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(values[i] % chars.length);
}
return result;
}
if (a !== null && b !== null && a >= b) {
throw new Error('a should be smaller than b');
}
// get the subkey in full key
// e.g.
// a0xxxx -> a
// a0x0xxxx -> a0x
function subkey(key: string | null) {
if (key === null) {
return null;
}
if (key.length <= randomSize + 1) {
// no subkey
return key;
}
const splitAt = key.substring(0, key.length - randomSize - 1);
return splitAt;
}
const aSubkey = subkey(a);
const bSubkey = subkey(b);
if (aSubkey === null && bSubkey === null) {
// generate a new key
return generateKeyBetween(null, null) + '0' + postfix();
} else if (aSubkey === null && bSubkey !== null) {
// generate a key before b
return generateKeyBetween(null, bSubkey) + '0' + postfix();
} else if (bSubkey === null && aSubkey !== null) {
// generate a key after a
return generateKeyBetween(aSubkey, null) + '0' + postfix();
} else if (aSubkey !== null && bSubkey !== null) {
// generate a key between a and b
if (aSubkey === bSubkey && a !== null && b !== null) {
// conflict, if the subkeys are the same, generate a key between fullkeys
return generateKeyBetween(a, b) + '0' + postfix();
} else {
return generateKeyBetween(aSubkey, bSubkey) + '0' + postfix();
}
}
throw new Error('Never reach here');
}

View File

@@ -1,11 +1,13 @@
export * from './auto-scroll';
export * from './button-popper';
export * from './cell-select';
export * from './collapsed';
export * from './dnd';
export * from './dom';
export * from './edgeless';
export * from './event';
export * from './file';
export * from './fractional-indexing';
export * from './insert';
export * from './is-abort-error';
export * from './math';
@@ -18,4 +20,5 @@ export * from './spec';
export * from './string';
export * from './title';
export * from './url';
export * from './virtual-padding';
export * from './zod-schema';

View File

@@ -0,0 +1,35 @@
import type { BlockComponent } from '@blocksuite/block-std';
import { autoUpdate } from '@floating-ui/dom';
import { signal } from '@preact/signals-core';
import type { ReactiveController } from 'lit';
import { DocModeProvider } from '../services/doc-mode-service';
export class VirtualPaddingController implements ReactiveController {
public readonly virtualPadding$ = signal(0);
constructor(private readonly block: BlockComponent) {
block.addController(this);
}
get std() {
return this.host.std;
}
get host() {
return this.block.host;
}
hostConnected(): void {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return;
}
this.block.disposables.add(
autoUpdate(this.host, this.block, () => {
const padding =
this.block.getBoundingClientRect().left -
this.host.getBoundingClientRect().left;
this.virtualPadding$.value = Math.max(0, padding - 72);
})
);
}
}