mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +08:00
refactor(editor): extract note block (#9310)
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
asyncSetInlineRange,
|
||||
focusTextModel,
|
||||
onModelTextUpdated,
|
||||
} from '@blocksuite/affine-components/rich-text';
|
||||
import {
|
||||
matchFlavours,
|
||||
mergeToCodeModel,
|
||||
transformModel,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
type UpdateBlockConfig = {
|
||||
flavour: BlockSuite.Flavour;
|
||||
props?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const updateBlockType: Command<
|
||||
'selectedBlocks',
|
||||
'updatedBlocks',
|
||||
UpdateBlockConfig
|
||||
> = (ctx, next) => {
|
||||
const { std, flavour, props } = ctx;
|
||||
const host = std.host;
|
||||
const doc = std.doc;
|
||||
|
||||
const getSelectedBlocks = () => {
|
||||
let { selectedBlocks } = ctx;
|
||||
|
||||
if (selectedBlocks == null) {
|
||||
const [result, ctx] = std.command
|
||||
.chain()
|
||||
.tryAll(chain => [chain.getTextSelection(), chain.getBlockSelections()])
|
||||
.getSelectedBlocks({ types: ['text', 'block'] })
|
||||
.run();
|
||||
if (result) {
|
||||
selectedBlocks = ctx.selectedBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedBlocks;
|
||||
};
|
||||
|
||||
const selectedBlocks = getSelectedBlocks();
|
||||
if (!selectedBlocks || selectedBlocks.length === 0) return false;
|
||||
|
||||
const blockModels = selectedBlocks.map(ele => ele.model);
|
||||
|
||||
const hasSameDoc = selectedBlocks.every(block => block.doc === doc);
|
||||
if (!hasSameDoc) {
|
||||
// doc check
|
||||
console.error(
|
||||
'Not all models have the same doc instance, the result for update text type may not be correct',
|
||||
selectedBlocks
|
||||
);
|
||||
}
|
||||
|
||||
const mergeToCode: Command<never, 'updatedBlocks'> = (_, next) => {
|
||||
if (flavour !== 'affine:code') return;
|
||||
const id = mergeToCodeModel(blockModels);
|
||||
if (!id) return;
|
||||
const model = doc.getBlockById(id);
|
||||
if (!model) return;
|
||||
asyncSetInlineRange(host, model, {
|
||||
index: model.text?.length ?? 0,
|
||||
length: 0,
|
||||
}).catch(console.error);
|
||||
return next({ updatedBlocks: [model] });
|
||||
};
|
||||
const appendDivider: Command<never, 'updatedBlocks'> = (_, next) => {
|
||||
if (flavour !== 'affine:divider') {
|
||||
return false;
|
||||
}
|
||||
const model = blockModels.at(-1);
|
||||
if (!model) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
const index = parent.children.indexOf(model);
|
||||
const nextSibling = doc.getNext(model);
|
||||
let nextSiblingId = nextSibling?.id as string;
|
||||
const id = doc.addBlock('affine:divider', {}, parent, index + 1);
|
||||
if (!nextSibling) {
|
||||
nextSiblingId = doc.addBlock('affine:paragraph', {}, parent);
|
||||
}
|
||||
focusTextModel(host.std, nextSiblingId);
|
||||
const newModel = doc.getBlockById(id);
|
||||
if (!newModel) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
return next({ updatedBlocks: [newModel] });
|
||||
};
|
||||
|
||||
const focusText: Command<'updatedBlocks'> = (ctx, next) => {
|
||||
const { updatedBlocks } = ctx;
|
||||
if (!updatedBlocks || updatedBlocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstNewModel = updatedBlocks[0];
|
||||
const lastNewModel = updatedBlocks[updatedBlocks.length - 1];
|
||||
|
||||
const allTextUpdated = updatedBlocks.map(model =>
|
||||
onModelTextUpdated(host, model)
|
||||
);
|
||||
const selectionManager = host.selection;
|
||||
const textSelection = selectionManager.find('text');
|
||||
if (!textSelection) {
|
||||
return false;
|
||||
}
|
||||
const newTextSelection = selectionManager.create('text', {
|
||||
from: {
|
||||
blockId: firstNewModel.id,
|
||||
index: textSelection.from.index,
|
||||
length: textSelection.from.length,
|
||||
},
|
||||
to: textSelection.to
|
||||
? {
|
||||
blockId: lastNewModel.id,
|
||||
index: textSelection.to.index,
|
||||
length: textSelection.to.length,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
Promise.all(allTextUpdated)
|
||||
.then(() => {
|
||||
selectionManager.setGroup('note', [newTextSelection]);
|
||||
})
|
||||
.catch(console.error);
|
||||
return next();
|
||||
};
|
||||
|
||||
const focusBlock: Command<'updatedBlocks'> = (ctx, next) => {
|
||||
const { updatedBlocks } = ctx;
|
||||
if (!updatedBlocks || updatedBlocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectionManager = host.selection;
|
||||
|
||||
const blockSelections = selectionManager.filter('block');
|
||||
if (blockSelections.length === 0) {
|
||||
return false;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const selections = updatedBlocks.map(model => {
|
||||
return selectionManager.create('block', {
|
||||
blockId: model.id,
|
||||
});
|
||||
});
|
||||
|
||||
selectionManager.setGroup('note', selections);
|
||||
});
|
||||
return next();
|
||||
};
|
||||
|
||||
const [result, resultCtx] = std.command
|
||||
.chain()
|
||||
.inline((_, next) => {
|
||||
doc.captureSync();
|
||||
return next();
|
||||
})
|
||||
// update block type
|
||||
.try<'updatedBlocks'>(chain => [
|
||||
chain.inline<'updatedBlocks'>(mergeToCode),
|
||||
chain.inline<'updatedBlocks'>(appendDivider),
|
||||
chain.inline<'updatedBlocks'>((_, next) => {
|
||||
const newModels: BlockModel[] = [];
|
||||
blockModels.forEach(model => {
|
||||
if (
|
||||
!matchFlavours(model, [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (model.flavour === flavour) {
|
||||
doc.updateBlock(model, props ?? {});
|
||||
newModels.push(model);
|
||||
return;
|
||||
}
|
||||
const newId = transformModel(model, flavour, props);
|
||||
if (!newId) {
|
||||
return;
|
||||
}
|
||||
const newModel = doc.getBlockById(newId);
|
||||
if (newModel) {
|
||||
newModels.push(newModel);
|
||||
}
|
||||
});
|
||||
return next({ updatedBlocks: newModels });
|
||||
}),
|
||||
])
|
||||
// focus
|
||||
.try(chain => [
|
||||
chain.inline((_, next) => {
|
||||
if (['affine:code', 'affine:divider'].includes(flavour)) {
|
||||
return next();
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
chain.inline(focusText),
|
||||
chain.inline(focusBlock),
|
||||
chain.inline((_, next) => next()),
|
||||
])
|
||||
.run();
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return next({ updatedBlocks: resultCtx.updatedBlocks });
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const dedentBlockToRoot: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.doc.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
let parent = doc.getParent(model);
|
||||
let changed = false;
|
||||
while (parent && !matchFlavours(parent, ['affine:note'])) {
|
||||
if (!changed) {
|
||||
if (stopCapture) doc.captureSync();
|
||||
changed = true;
|
||||
}
|
||||
std.command.exec('dedentBlock', { blockId: model.id, stopCapture: true });
|
||||
parent = doc.getParent(model);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const dedentBlock: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.doc.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
const grandParent = parent && doc.getParent(parent);
|
||||
if (doc.readonly || !parent || parent.role !== 'content' || !grandParent) {
|
||||
// Top most, can not unindent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
doc.moveBlocks([model, ...collapsedSiblings], grandParent, parent, false);
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const nextSiblings = doc.getNexts(model);
|
||||
doc.moveBlocks(nextSiblings, model);
|
||||
doc.moveBlocks([model], grandParent, parent, false);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const dedentBlocksToRoot: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const text = std.selection.find('text');
|
||||
if (text) {
|
||||
// If the text selection is not at the beginning of the block, use default behavior
|
||||
if (text.from.index !== 0) return;
|
||||
|
||||
blockIds = [text.from.blockId, text.to?.blockId].filter(
|
||||
(x): x is string => !!x
|
||||
);
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || doc.readonly) return;
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
for (let i = blockIds.length - 1; i >= 0; i--) {
|
||||
const model = blockIds[i];
|
||||
const parent = doc.getParent(model);
|
||||
if (parent && !matchFlavours(parent, ['affine:note'])) {
|
||||
std.command.exec('dedentBlockToRoot', {
|
||||
blockId: model,
|
||||
stopCapture: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const dedentBlocks: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc, selection, range, host } = std;
|
||||
const { schema } = doc;
|
||||
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const nativeRange = range.value;
|
||||
if (nativeRange) {
|
||||
const topBlocks = range.getSelectedBlockComponentsByRange(nativeRange, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
if (topBlocks.length > 0) {
|
||||
blockIds = topBlocks.map(block => block.blockId);
|
||||
}
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || doc.readonly) return;
|
||||
|
||||
// Find the first model that can be unindented
|
||||
let firstDedentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const model = doc.getBlock(blockIds[i])?.model;
|
||||
if (!model) continue;
|
||||
const parent = doc.getParent(blockIds[i]);
|
||||
if (!parent) continue;
|
||||
const grandParent = doc.getParent(parent);
|
||||
if (!grandParent) continue;
|
||||
|
||||
if (schema.isValid(model.flavour, grandParent.flavour)) {
|
||||
firstDedentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstDedentIndex === -1) return;
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstDedentIndex).forEach(id => {
|
||||
const model = doc.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
collapsedIds.push(...collapsedSiblings.map(sibling => sibling.id));
|
||||
}
|
||||
});
|
||||
// Models waiting to be dedented
|
||||
const dedentIds = blockIds
|
||||
.slice(firstDedentIndex)
|
||||
.filter(id => !collapsedIds.includes(id));
|
||||
dedentIds.reverse().forEach(id => {
|
||||
std.command.exec('dedentBlock', { blockId: id, stopCapture: false });
|
||||
});
|
||||
|
||||
const textSelection = selection.find('text');
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const focusBlockEnd: Command<'focusBlock'> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock || !focusBlock.model.text) return;
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create('text', {
|
||||
from: {
|
||||
blockId: focusBlock.blockId,
|
||||
index: focusBlock.model.text.length,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const focusBlockStart: Command<'focusBlock'> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock || !focusBlock.model.text) return;
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create('text', {
|
||||
from: { blockId: focusBlock.blockId, index: 0, length: 0 },
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ListBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const indentBlock: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc } = std;
|
||||
const { schema } = doc;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.doc.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const previousSibling = doc.getPrev(model);
|
||||
if (
|
||||
doc.readonly ||
|
||||
!previousSibling ||
|
||||
!schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
// can not indent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
doc.moveBlocks([model, ...collapsedSiblings], previousSibling);
|
||||
} else {
|
||||
doc.moveBlocks([model], previousSibling);
|
||||
}
|
||||
|
||||
// update collapsed state of affine list
|
||||
if (
|
||||
matchFlavours(previousSibling, ['affine:list']) &&
|
||||
previousSibling.collapsed
|
||||
) {
|
||||
doc.updateBlock(previousSibling, {
|
||||
collapsed: false,
|
||||
} as Partial<ListBlockModel>);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
getNearestHeadingBefore,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const indentBlocks: Command<
|
||||
never,
|
||||
never,
|
||||
{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { doc, selection, range, host } = std;
|
||||
const { schema } = doc;
|
||||
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const nativeRange = range.value;
|
||||
if (nativeRange) {
|
||||
const topBlocks = range.getSelectedBlockComponentsByRange(nativeRange, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
if (topBlocks.length > 0) {
|
||||
blockIds = topBlocks.map(block => block.blockId);
|
||||
}
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || doc.readonly) return;
|
||||
|
||||
// Find the first model that can be indented
|
||||
let firstIndentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const previousSibling = doc.getPrev(blockIds[i]);
|
||||
const model = doc.getBlock(blockIds[i])?.model;
|
||||
if (
|
||||
model &&
|
||||
previousSibling &&
|
||||
schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
firstIndentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No model can be indented
|
||||
if (firstIndentIndex === -1) return;
|
||||
|
||||
if (stopCapture) doc.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstIndentIndex).forEach(id => {
|
||||
const model = doc.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchFlavours(model, ['affine:paragraph']) &&
|
||||
model.type.startsWith('h') &&
|
||||
model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
collapsedIds.push(...collapsedSiblings.map(sibling => sibling.id));
|
||||
}
|
||||
});
|
||||
// Models waiting to be indented
|
||||
const indentIds = blockIds
|
||||
.slice(firstIndentIndex)
|
||||
.filter(id => !collapsedIds.includes(id));
|
||||
const firstModel = doc.getBlock(indentIds[0])?.model;
|
||||
if (!firstModel) return;
|
||||
|
||||
{
|
||||
// > # 123
|
||||
// > # 456
|
||||
// > # 789
|
||||
//
|
||||
// we need to update 123 collapsed state to false when indent 456 and 789
|
||||
|
||||
const nearestHeading = getNearestHeadingBefore(firstModel);
|
||||
if (
|
||||
nearestHeading &&
|
||||
matchFlavours(nearestHeading, ['affine:paragraph']) &&
|
||||
nearestHeading.collapsed
|
||||
) {
|
||||
doc.updateBlock(nearestHeading, {
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
indentIds.forEach(id => {
|
||||
std.command.exec('indentBlock', { blockId: id, stopCapture: false });
|
||||
});
|
||||
|
||||
{
|
||||
// 123
|
||||
// > # 456
|
||||
// 789
|
||||
// 012
|
||||
//
|
||||
// we need to update 456 collapsed state to false when indent 789 and 012
|
||||
const nearestHeading = getNearestHeadingBefore(firstModel);
|
||||
if (
|
||||
nearestHeading &&
|
||||
matchFlavours(nearestHeading, ['affine:paragraph']) &&
|
||||
nearestHeading.collapsed
|
||||
) {
|
||||
doc.updateBlock(nearestHeading, {
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const textSelection = selection.find('text');
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
getBlockIndexCommand,
|
||||
getBlockSelectionsCommand,
|
||||
getNextBlockCommand,
|
||||
getPrevBlockCommand,
|
||||
getSelectedBlocksCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type { BlockCommands } from '@blocksuite/block-std';
|
||||
|
||||
import { updateBlockType } from './block-type.js';
|
||||
import { dedentBlock } from './dedent-block.js';
|
||||
import { dedentBlockToRoot } from './dedent-block-to-root.js';
|
||||
import { dedentBlocks } from './dedent-blocks.js';
|
||||
import { dedentBlocksToRoot } from './dedent-blocks-to-root.js';
|
||||
import { focusBlockEnd } from './focus-block-end.js';
|
||||
import { focusBlockStart } from './focus-block-start.js';
|
||||
import { indentBlock } from './indent-block.js';
|
||||
import { indentBlocks } from './indent-blocks.js';
|
||||
import { selectBlock } from './select-block.js';
|
||||
import { selectBlocksBetween } from './select-blocks-between.js';
|
||||
|
||||
export const commands: BlockCommands = {
|
||||
// block
|
||||
getBlockIndex: getBlockIndexCommand,
|
||||
getPrevBlock: getPrevBlockCommand,
|
||||
getNextBlock: getNextBlockCommand,
|
||||
getSelectedBlocks: getSelectedBlocksCommand,
|
||||
getBlockSelections: getBlockSelectionsCommand,
|
||||
selectBlock,
|
||||
selectBlocksBetween,
|
||||
focusBlockStart,
|
||||
focusBlockEnd,
|
||||
updateBlockType,
|
||||
indentBlock,
|
||||
dedentBlock,
|
||||
indentBlocks,
|
||||
dedentBlocks,
|
||||
dedentBlockToRoot,
|
||||
dedentBlocksToRoot,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const selectBlock: Command<'focusBlock'> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create('block', { blockId: focusBlock.blockId }),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Command } from '@blocksuite/block-std';
|
||||
|
||||
export const selectBlocksBetween: Command<
|
||||
'focusBlock' | 'anchorBlock',
|
||||
never,
|
||||
{ tail: boolean }
|
||||
> = (ctx, next) => {
|
||||
const { focusBlock, anchorBlock, tail } = ctx;
|
||||
if (!focusBlock || !anchorBlock) {
|
||||
return;
|
||||
}
|
||||
const selection = ctx.std.selection;
|
||||
|
||||
// In same block
|
||||
if (anchorBlock.blockId === focusBlock.blockId) {
|
||||
const blockId = focusBlock.blockId;
|
||||
selection.setGroup('note', [selection.create('block', { blockId })]);
|
||||
return next();
|
||||
}
|
||||
|
||||
// In different blocks
|
||||
const selections = [...selection.value];
|
||||
if (selections.every(sel => sel.blockId !== focusBlock.blockId)) {
|
||||
if (tail) {
|
||||
selections.push(
|
||||
selection.create('block', { blockId: focusBlock.blockId })
|
||||
);
|
||||
} else {
|
||||
selections.unshift(
|
||||
selection.create('block', { blockId: focusBlock.blockId })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let start = false;
|
||||
const sel = selections.filter(sel => {
|
||||
if (
|
||||
sel.blockId === anchorBlock.blockId ||
|
||||
sel.blockId === focusBlock.blockId
|
||||
) {
|
||||
start = !start;
|
||||
return true;
|
||||
}
|
||||
return start;
|
||||
});
|
||||
|
||||
selection.setGroup('note', sel);
|
||||
return next();
|
||||
};
|
||||
Reference in New Issue
Block a user