mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +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:
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user