mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 13:58:50 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
|
||||
export function isInsidePageEditor(host: EditorHost) {
|
||||
return Array.from(host.children).some(
|
||||
v => v.tagName.toLowerCase() === 'affine-page-root'
|
||||
);
|
||||
}
|
||||
|
||||
export function isInsideEdgelessEditor(host: EditorHost) {
|
||||
return Array.from(host.children).some(
|
||||
v =>
|
||||
v.tagName.toLowerCase() === 'affine-edgeless-root' ||
|
||||
v.tagName.toLowerCase() === 'affine-edgeless-root-preview'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './checker.js';
|
||||
export * from './point-to-block.js';
|
||||
export * from './point-to-range.js';
|
||||
export * from './query.js';
|
||||
export * from './scroll-container.js';
|
||||
export * from './viewport.js';
|
||||
@@ -0,0 +1,320 @@
|
||||
import { BLOCK_ID_ATTR, type BlockComponent } from '@blocksuite/block-std';
|
||||
import type { Point, Rect } from '@blocksuite/global/utils';
|
||||
|
||||
import { BLOCK_CHILDREN_CONTAINER_PADDING_LEFT } from '../../consts/index.js';
|
||||
import { clamp } from '../math.js';
|
||||
import { matchFlavours } from '../model/checker.js';
|
||||
|
||||
const ATTR_SELECTOR = `[${BLOCK_ID_ATTR}]`;
|
||||
|
||||
// margin-top: calc(var(--affine-paragraph-space) + 24px);
|
||||
// h1.margin-top = 8px + 24px = 32px;
|
||||
const MAX_SPACE = 32;
|
||||
const STEPS = MAX_SPACE / 2 / 2;
|
||||
|
||||
/**
|
||||
* Returns `16` if node is contained in the parent.
|
||||
* Otherwise return `0`.
|
||||
*/
|
||||
function contains(parent: Element, node: Element) {
|
||||
return (
|
||||
parent.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINED_BY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if element has `data-block-id` attribute.
|
||||
*/
|
||||
function hasBlockId(element: Element): element is BlockComponent {
|
||||
return element.hasAttribute(BLOCK_ID_ATTR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if element is default/edgeless page or note.
|
||||
*/
|
||||
function isRootOrNoteOrSurface(element: BlockComponent) {
|
||||
return matchFlavours(element.model, [
|
||||
'affine:page',
|
||||
'affine:note',
|
||||
// @ts-expect-error TODO: migrate surface model to @blocksuite/affine-model
|
||||
'affine:surface',
|
||||
]);
|
||||
}
|
||||
|
||||
function isBlock(element: BlockComponent) {
|
||||
return !isRootOrNoteOrSurface(element);
|
||||
}
|
||||
|
||||
function isImage({ tagName }: Element) {
|
||||
return tagName === 'AFFINE-IMAGE';
|
||||
}
|
||||
|
||||
function isDatabase({ tagName }: Element) {
|
||||
return tagName === 'AFFINE-DATABASE-TABLE' || tagName === 'AFFINE-DATABASE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the closest block element by a point in the rect.
|
||||
*
|
||||
* ```
|
||||
* ############### block
|
||||
* ||############# block
|
||||
* ||||########### block
|
||||
* |||| ...
|
||||
* |||| y - 2 * n
|
||||
* |||| ...
|
||||
* ||||----------- cursor
|
||||
* |||| ...
|
||||
* |||| y + 2 * n
|
||||
* |||| ...
|
||||
* ||||########### block
|
||||
* ||############# block
|
||||
* ############### block
|
||||
* ```
|
||||
*/
|
||||
export function getClosestBlockComponentByPoint(
|
||||
point: Point,
|
||||
state: {
|
||||
rect?: Rect;
|
||||
container?: Element;
|
||||
snapToEdge?: {
|
||||
x: boolean;
|
||||
y: boolean;
|
||||
};
|
||||
} | null = null,
|
||||
scale = 1
|
||||
): BlockComponent | null {
|
||||
const { y } = point;
|
||||
|
||||
let container;
|
||||
let element = null;
|
||||
let bounds = null;
|
||||
let childBounds = null;
|
||||
let diff = 0;
|
||||
let n = 1;
|
||||
|
||||
if (state) {
|
||||
const {
|
||||
snapToEdge = {
|
||||
x: true,
|
||||
y: false,
|
||||
},
|
||||
} = state;
|
||||
container = state.container;
|
||||
const rect = state.rect || container?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
if (snapToEdge.x) {
|
||||
point.x = Math.min(
|
||||
Math.max(point.x, rect.left) +
|
||||
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT * scale -
|
||||
1,
|
||||
rect.right - BLOCK_CHILDREN_CONTAINER_PADDING_LEFT * scale - 1
|
||||
);
|
||||
}
|
||||
if (snapToEdge.y) {
|
||||
// TODO handle scale
|
||||
if (scale !== 1) {
|
||||
console.warn('scale is not supported yet');
|
||||
}
|
||||
point.y = clamp(point.y, rect.top + 1, rect.bottom - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find block element
|
||||
element = findBlockComponent(
|
||||
document.elementsFromPoint(point.x, point.y),
|
||||
container
|
||||
);
|
||||
|
||||
// Horizontal direction: for nested structures
|
||||
if (element) {
|
||||
// Database
|
||||
if (isDatabase(element)) {
|
||||
bounds = element.getBoundingClientRect();
|
||||
const rows = getDatabaseBlockRowsElement(element);
|
||||
if (rows) {
|
||||
childBounds = rows.getBoundingClientRect();
|
||||
|
||||
if (childBounds.height) {
|
||||
if (point.y < childBounds.top || point.y > childBounds.bottom) {
|
||||
return element as BlockComponent;
|
||||
}
|
||||
childBounds = null;
|
||||
} else {
|
||||
return element as BlockComponent;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Indented paragraphs or list
|
||||
bounds = getRectByBlockComponent(element);
|
||||
childBounds = element
|
||||
.querySelector('.affine-block-children-container')
|
||||
?.firstElementChild?.getBoundingClientRect();
|
||||
|
||||
if (childBounds && childBounds.height) {
|
||||
if (bounds.x < point.x && point.x <= childBounds.x) {
|
||||
return element as BlockComponent;
|
||||
}
|
||||
childBounds = null;
|
||||
} else {
|
||||
return element as BlockComponent;
|
||||
}
|
||||
}
|
||||
|
||||
bounds = null;
|
||||
element = null;
|
||||
}
|
||||
|
||||
// Vertical direction
|
||||
do {
|
||||
point.y = y - n * 2;
|
||||
|
||||
if (n < 0) n--;
|
||||
n *= -1;
|
||||
|
||||
// find block element
|
||||
element = findBlockComponent(
|
||||
document.elementsFromPoint(point.x, point.y),
|
||||
container
|
||||
);
|
||||
|
||||
if (element) {
|
||||
bounds = getRectByBlockComponent(element);
|
||||
diff = bounds.bottom - point.y;
|
||||
if (diff >= 0 && diff <= STEPS * 2) {
|
||||
return element as BlockComponent;
|
||||
}
|
||||
diff = point.y - bounds.top;
|
||||
if (diff >= 0 && diff <= STEPS * 2) {
|
||||
return element as BlockComponent;
|
||||
}
|
||||
bounds = null;
|
||||
element = null;
|
||||
}
|
||||
} while (n <= STEPS);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most close block on the given position
|
||||
* @param container container which the blocks can be found inside
|
||||
* @param point position
|
||||
* @param selector selector to find the block
|
||||
*/
|
||||
export function findClosestBlockComponent(
|
||||
container: BlockComponent,
|
||||
point: Point,
|
||||
selector: string
|
||||
): BlockComponent | null {
|
||||
const children = (
|
||||
Array.from(container.querySelectorAll(selector)) as BlockComponent[]
|
||||
)
|
||||
.filter(child => child.host === container.host)
|
||||
.filter(child => child !== container);
|
||||
|
||||
let lastDistance = Number.POSITIVE_INFINITY;
|
||||
let lastChild = null;
|
||||
|
||||
if (!children.length) return null;
|
||||
|
||||
for (const child of children) {
|
||||
const rect = child.getBoundingClientRect();
|
||||
if (rect.height === 0 || point.y > rect.bottom || point.y < rect.top)
|
||||
continue;
|
||||
const distance =
|
||||
Math.pow(point.y - (rect.y + rect.height / 2), 2) +
|
||||
Math.pow(point.x - rect.x, 2);
|
||||
|
||||
if (distance <= lastDistance) {
|
||||
lastDistance = distance;
|
||||
lastChild = child;
|
||||
} else {
|
||||
return lastChild;
|
||||
}
|
||||
}
|
||||
|
||||
return lastChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the closest block element by element that does not contain the page element and note element.
|
||||
*/
|
||||
export function getClosestBlockComponentByElement(
|
||||
element: Element | null
|
||||
): BlockComponent | null {
|
||||
if (!element) return null;
|
||||
if (hasBlockId(element) && isBlock(element)) {
|
||||
return element;
|
||||
}
|
||||
const blockComponent = element.closest<BlockComponent>(ATTR_SELECTOR);
|
||||
if (blockComponent && isBlock(blockComponent)) {
|
||||
return blockComponent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rect of the block element.
|
||||
*
|
||||
* Compatible with Safari!
|
||||
* https://github.com/toeverything/blocksuite/issues/902
|
||||
* https://github.com/toeverything/blocksuite/pull/1121
|
||||
*/
|
||||
export function getRectByBlockComponent(element: Element | BlockComponent) {
|
||||
if (isDatabase(element)) return element.getBoundingClientRect();
|
||||
return (element.firstElementChild ?? element).getBoundingClientRect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns block elements excluding their subtrees.
|
||||
* Only keep block elements of same level.
|
||||
*/
|
||||
export function getBlockComponentsExcludeSubtrees(
|
||||
elements: Element[] | BlockComponent[]
|
||||
): BlockComponent[] {
|
||||
if (elements.length <= 1) return elements as BlockComponent[];
|
||||
let parent = elements[0];
|
||||
return elements.filter((node, index) => {
|
||||
if (index === 0) return true;
|
||||
if (contains(parent, node)) {
|
||||
return false;
|
||||
} else {
|
||||
parent = node;
|
||||
return true;
|
||||
}
|
||||
}) as BlockComponent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find block element from an `Element[]`.
|
||||
* In Chrome/Safari, `document.elementsFromPoint` does not include `affine-image`.
|
||||
*/
|
||||
function findBlockComponent(elements: Element[], parent?: Element) {
|
||||
const len = elements.length;
|
||||
let element = null;
|
||||
let i = 0;
|
||||
while (i < len) {
|
||||
element = elements[i];
|
||||
i++;
|
||||
// if parent does not contain element, it's ignored
|
||||
if (parent && !contains(parent, element)) continue;
|
||||
if (hasBlockId(element) && isBlock(element)) return element;
|
||||
if (isImage(element)) {
|
||||
const element = elements[i];
|
||||
if (i < len && hasBlockId(element) && isBlock(element)) {
|
||||
return elements[i];
|
||||
}
|
||||
return getClosestBlockComponentByElement(element);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rows of the database.
|
||||
*/
|
||||
function getDatabaseBlockRowsElement(element: Element) {
|
||||
return element.querySelector('.affine-database-block-rows');
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { IS_FIREFOX } from '@blocksuite/global/env';
|
||||
|
||||
declare global {
|
||||
interface Document {
|
||||
// firefox API
|
||||
caretPositionFromPoint(
|
||||
x: number,
|
||||
y: number
|
||||
): {
|
||||
offsetNode: Node;
|
||||
offset: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for the browser's `caretPositionFromPoint` and `caretRangeFromPoint`,
|
||||
* but adapted for different browsers.
|
||||
*/
|
||||
export function caretRangeFromPoint(
|
||||
clientX: number,
|
||||
clientY: number
|
||||
): Range | null {
|
||||
if (IS_FIREFOX) {
|
||||
const caret = document.caretPositionFromPoint(clientX, clientY);
|
||||
// TODO handle caret is covered by popup
|
||||
const range = document.createRange();
|
||||
range.setStart(caret.offsetNode, caret.offset);
|
||||
return range;
|
||||
}
|
||||
|
||||
const range = document.caretRangeFromPoint(clientX, clientY);
|
||||
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// See https://github.com/toeverything/blocksuite/issues/1382
|
||||
const rangeRects = range?.getClientRects();
|
||||
if (
|
||||
rangeRects &&
|
||||
rangeRects.length === 2 &&
|
||||
range.startOffset === range.endOffset &&
|
||||
clientY < rangeRects[0].y + rangeRects[0].height
|
||||
) {
|
||||
const deltaX = (rangeRects[0].x | 0) - (rangeRects[1].x | 0);
|
||||
|
||||
if (deltaX > 0) {
|
||||
range.setStart(range.startContainer, range.startOffset - 1);
|
||||
range.setEnd(range.endContainer, range.endOffset - 1);
|
||||
}
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
export function resetNativeSelection(range: Range | null) {
|
||||
const selection = window.getSelection();
|
||||
if (!selection) return;
|
||||
selection.removeAllRanges();
|
||||
range && selection.addRange(range);
|
||||
}
|
||||
|
||||
export function getCurrentNativeRange(selection = window.getSelection()) {
|
||||
// When called on an <iframe> that is not displayed (e.g., where display: none is set) Firefox will return null
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection for more details
|
||||
if (!selection) {
|
||||
console.error('Failed to get current range, selection is null');
|
||||
return null;
|
||||
}
|
||||
if (selection.rangeCount === 0) {
|
||||
return null;
|
||||
}
|
||||
if (selection.rangeCount > 1) {
|
||||
console.warn('getCurrentNativeRange may be wrong, rangeCount > 1');
|
||||
}
|
||||
return selection.getRangeAt(0);
|
||||
}
|
||||
|
||||
export function handleNativeRangeAtPoint(x: number, y: number) {
|
||||
const range = caretRangeFromPoint(x, y);
|
||||
const startContainer = range?.startContainer;
|
||||
// click on rich text
|
||||
if (startContainer instanceof Node) {
|
||||
resetNativeSelection(range);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { BLOCK_ID_ATTR, type BlockComponent } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
const ATTR_SELECTOR = `[${BLOCK_ID_ATTR}]`;
|
||||
|
||||
export function getModelByElement<Model extends BlockModel>(
|
||||
element: Element
|
||||
): Model | null {
|
||||
const closestBlock = element.closest<BlockComponent>(ATTR_SELECTOR);
|
||||
if (!closestBlock) {
|
||||
return null;
|
||||
}
|
||||
return closestBlock.model as Model;
|
||||
}
|
||||
|
||||
export function getRootByElement(
|
||||
element: Element
|
||||
): BlockComponent<RootBlockModel> | null {
|
||||
const pageRoot = getPageRootByElement(element);
|
||||
if (pageRoot) return pageRoot;
|
||||
|
||||
const edgelessRoot = getEdgelessRootByElement(element);
|
||||
if (edgelessRoot) return edgelessRoot;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getPageRootByElement(
|
||||
element: Element
|
||||
): BlockComponent<RootBlockModel> | null {
|
||||
return element.closest('affine-page-root');
|
||||
}
|
||||
|
||||
export function getEdgelessRootByElement(
|
||||
element: Element
|
||||
): BlockComponent<RootBlockModel> | null {
|
||||
return element.closest('affine-edgeless-root');
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const getScrollContainer = (ele: HTMLElement) => {
|
||||
let container: HTMLElement | null = ele;
|
||||
while (container && !isScrollable(container)) {
|
||||
container = container.parentElement;
|
||||
}
|
||||
return container ?? document.body;
|
||||
};
|
||||
|
||||
export const isScrollable = (ele: HTMLElement) => {
|
||||
const value = window.getComputedStyle(ele).overflowY;
|
||||
return value === 'scroll' || value === 'auto';
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { BlockComponent, EditorHost } from '@blocksuite/block-std';
|
||||
|
||||
import { isInsidePageEditor } from './checker.js';
|
||||
|
||||
/**
|
||||
* Get editor viewport element.
|
||||
* @example
|
||||
* ```ts
|
||||
* const viewportElement = getViewportElement(this.model.doc);
|
||||
* if (!viewportElement) return;
|
||||
* this._disposables.addFromEvent(viewportElement, 'scroll', () => {
|
||||
* updatePosition();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function getViewportElement(editorHost: EditorHost): HTMLElement | null {
|
||||
if (!isInsidePageEditor(editorHost)) return null;
|
||||
const doc = editorHost.doc;
|
||||
if (!doc.root) {
|
||||
console.error('Failed to get root doc');
|
||||
return null;
|
||||
}
|
||||
const rootComponent = editorHost.view.getBlock(doc.root.id);
|
||||
|
||||
if (
|
||||
!rootComponent ||
|
||||
rootComponent.closest('affine-page-root') !== rootComponent
|
||||
) {
|
||||
console.error('Failed to get viewport element!');
|
||||
return null;
|
||||
}
|
||||
return (rootComponent as BlockComponent & { viewportElement: HTMLElement })
|
||||
.viewportElement;
|
||||
}
|
||||
Reference in New Issue
Block a user