mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-05 00:16:47 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import { INLINE_ROOT_ATTR, type InlineRootElement } from '@blocksuite/inline';
|
||||
|
||||
import { LifeCycleWatcher } from '../extension/index.js';
|
||||
import type { TextSelection } from '../selection/index.js';
|
||||
import type { BlockComponent } from '../view/element/block-component.js';
|
||||
import { BLOCK_ID_ATTR } from '../view/index.js';
|
||||
import { RANGE_QUERY_EXCLUDE_ATTR, RANGE_SYNC_EXCLUDE_ATTR } from './consts.js';
|
||||
import { RangeBinding } from './range-binding.js';
|
||||
|
||||
/**
|
||||
* CRUD for Range and TextSelection
|
||||
*/
|
||||
export class RangeManager extends LifeCycleWatcher {
|
||||
static override readonly key = 'rangeManager';
|
||||
|
||||
binding: RangeBinding | null = null;
|
||||
|
||||
get value() {
|
||||
const selection = document.getSelection();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
if (selection.rangeCount === 0) return null;
|
||||
return selection.getRangeAt(0);
|
||||
}
|
||||
|
||||
private _isRangeSyncExcluded(el: Element) {
|
||||
return !!el.closest(`[${RANGE_SYNC_EXCLUDE_ATTR}="true"]`);
|
||||
}
|
||||
|
||||
clear() {
|
||||
const selection = document.getSelection();
|
||||
if (!selection) return;
|
||||
selection.removeAllRanges();
|
||||
|
||||
const topContenteditableElement = this.std.host.querySelector(
|
||||
'[contenteditable="true"]'
|
||||
);
|
||||
if (topContenteditableElement instanceof HTMLElement) {
|
||||
topContenteditableElement.blur();
|
||||
}
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
getClosestBlock(node: Node) {
|
||||
const el = node instanceof Element ? node : node.parentElement;
|
||||
if (!el) return null;
|
||||
const block = el.closest<BlockComponent>(`[${BLOCK_ID_ATTR}]`);
|
||||
if (!block) return null;
|
||||
if (this._isRangeSyncExcluded(block)) return null;
|
||||
return block;
|
||||
}
|
||||
|
||||
getClosestInlineEditor(node: Node) {
|
||||
const el = node instanceof Element ? node : node.parentElement;
|
||||
if (!el) return null;
|
||||
const inlineRoot = el.closest<InlineRootElement>(`[${INLINE_ROOT_ATTR}]`);
|
||||
if (!inlineRoot) return null;
|
||||
|
||||
if (this._isRangeSyncExcluded(inlineRoot)) return null;
|
||||
|
||||
return inlineRoot.inlineEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* aaa
|
||||
* b[bb
|
||||
* ccc
|
||||
* ddd
|
||||
* ee]e
|
||||
*
|
||||
* all mode: [aaa, bbb, ccc, ddd, eee]
|
||||
* flat mode: [bbb, ccc, ddd, eee]
|
||||
* highest mode: [bbb, ddd]
|
||||
*
|
||||
* match function will be evaluated before filtering using mode
|
||||
*/
|
||||
getSelectedBlockComponentsByRange(
|
||||
range: Range,
|
||||
options: {
|
||||
match?: (el: BlockComponent) => boolean;
|
||||
mode?: 'all' | 'flat' | 'highest';
|
||||
} = {}
|
||||
): BlockComponent[] {
|
||||
const { mode = 'all', match = () => true } = options;
|
||||
|
||||
let result = Array.from<BlockComponent>(
|
||||
this.std.host.querySelectorAll(
|
||||
`[${BLOCK_ID_ATTR}]:not([${RANGE_QUERY_EXCLUDE_ATTR}="true"])`
|
||||
)
|
||||
).filter(el => range.intersectsNode(el) && match(el));
|
||||
|
||||
if (result.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const firstElement = this.getClosestBlock(range.startContainer);
|
||||
if (!firstElement) return [];
|
||||
const firstElementIndex = result.indexOf(firstElement);
|
||||
if (firstElementIndex === -1) return [];
|
||||
|
||||
if (mode === 'flat') {
|
||||
result = result.slice(firstElementIndex);
|
||||
} else if (mode === 'highest') {
|
||||
result = result.slice(firstElementIndex);
|
||||
let parent = result[0];
|
||||
result = result.filter((node, index) => {
|
||||
if (index === 0) return true;
|
||||
if (
|
||||
parent.compareDocumentPosition(node) &
|
||||
Node.DOCUMENT_POSITION_CONTAINED_BY
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
parent = node;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
this.binding = new RangeBinding(this);
|
||||
}
|
||||
|
||||
queryInlineEditorByPath(path: string) {
|
||||
const block = this.std.host.view.getBlock(path);
|
||||
if (!block) return null;
|
||||
|
||||
const inlineRoot = block.querySelector<InlineRootElement>(
|
||||
`[${INLINE_ROOT_ATTR}]`
|
||||
);
|
||||
if (!inlineRoot) return null;
|
||||
|
||||
if (this._isRangeSyncExcluded(inlineRoot)) return null;
|
||||
|
||||
return inlineRoot.inlineEditor;
|
||||
}
|
||||
|
||||
rangeToTextSelection(range: Range, reverse = false): TextSelection | null {
|
||||
const { startContainer, endContainer } = range;
|
||||
|
||||
const startBlock = this.getClosestBlock(startContainer);
|
||||
const endBlock = this.getClosestBlock(endContainer);
|
||||
if (!startBlock || !endBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startInlineEditor = this.getClosestInlineEditor(startContainer);
|
||||
const endInlineEditor = this.getClosestInlineEditor(endContainer);
|
||||
if (!startInlineEditor || !endInlineEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startInlineRange = startInlineEditor.toInlineRange(range);
|
||||
const endInlineRange = endInlineEditor.toInlineRange(range);
|
||||
if (!startInlineRange || !endInlineRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.std.host.selection.create('text', {
|
||||
from: {
|
||||
blockId: startBlock.blockId,
|
||||
index: startInlineRange.index,
|
||||
length: startInlineRange.length,
|
||||
},
|
||||
to:
|
||||
startBlock === endBlock
|
||||
? null
|
||||
: {
|
||||
blockId: endBlock.blockId,
|
||||
index: endInlineRange.index,
|
||||
length: endInlineRange.length,
|
||||
},
|
||||
reverse,
|
||||
});
|
||||
}
|
||||
|
||||
set(range: Range) {
|
||||
const selection = document.getSelection();
|
||||
if (!selection) return;
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
syncRangeToTextSelection(range: Range, isRangeReversed: boolean) {
|
||||
const selectionManager = this.std.host.selection;
|
||||
|
||||
if (!range) {
|
||||
selectionManager.clear(['text']);
|
||||
return;
|
||||
}
|
||||
|
||||
const textSelection = this.rangeToTextSelection(range, isRangeReversed);
|
||||
if (textSelection) {
|
||||
selectionManager.setGroup('note', [textSelection]);
|
||||
} else {
|
||||
selectionManager.clear(['text']);
|
||||
}
|
||||
}
|
||||
|
||||
syncTextSelectionToRange(selection: TextSelection) {
|
||||
const range = this.textSelectionToRange(selection);
|
||||
if (range) {
|
||||
this.set(range);
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
textSelectionToRange(selection: TextSelection): Range | null {
|
||||
const { from, to } = selection;
|
||||
|
||||
const fromInlineEditor = this.queryInlineEditorByPath(from.blockId);
|
||||
if (!fromInlineEditor) return null;
|
||||
|
||||
if (selection.isInSameBlock()) {
|
||||
return fromInlineEditor.toDomRange({
|
||||
index: from.index,
|
||||
length: from.length,
|
||||
});
|
||||
}
|
||||
|
||||
if (!to) return null;
|
||||
const toInlineEditor = this.queryInlineEditorByPath(to.blockId);
|
||||
if (!toInlineEditor) return null;
|
||||
|
||||
const fromRange = fromInlineEditor.toDomRange({
|
||||
index: from.index,
|
||||
length: from.length,
|
||||
});
|
||||
const toRange = toInlineEditor.toDomRange({
|
||||
index: to.index,
|
||||
length: to.length,
|
||||
});
|
||||
|
||||
if (!fromRange || !toRange) return null;
|
||||
|
||||
const range = document.createRange();
|
||||
const startContainer = fromRange.startContainer;
|
||||
const startOffset = fromRange.startOffset;
|
||||
const endContainer = toRange.endContainer;
|
||||
const endOffset = toRange.endOffset;
|
||||
range.setStart(startContainer, startOffset);
|
||||
range.setEnd(endContainer, endOffset);
|
||||
|
||||
return range;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user