mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +08:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
import {
|
||||
CodeBlockModel,
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
asyncSetInlineRange,
|
||||
focusTextModel,
|
||||
onModelTextUpdated,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
getBlockSelectionsCommand,
|
||||
getSelectedBlocksCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
matchModels,
|
||||
mergeToCodeModel,
|
||||
transformModel,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type Command,
|
||||
TextSelection,
|
||||
} from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
type UpdateBlockConfig = {
|
||||
flavour: string;
|
||||
props?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const updateBlockType: Command<
|
||||
UpdateBlockConfig & {
|
||||
selectedBlocks?: BlockComponent[];
|
||||
},
|
||||
{
|
||||
updatedBlocks: BlockModel[];
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
const { std, flavour, props } = ctx;
|
||||
const host = std.host;
|
||||
const doc = std.store;
|
||||
|
||||
const getSelectedBlocks = () => {
|
||||
let { selectedBlocks } = ctx;
|
||||
|
||||
if (selectedBlocks == null) {
|
||||
const [result, ctx] = std.command
|
||||
.chain()
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getTextSelectionCommand),
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedBlocksCommand, { 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<{}, { updatedBlocks: BlockModel[] }> = (
|
||||
_,
|
||||
next
|
||||
) => {
|
||||
if (flavour !== 'affine:code') return;
|
||||
const id = mergeToCodeModel(blockModels);
|
||||
if (!id) return;
|
||||
const model = doc.getModelById(id);
|
||||
if (!model) return;
|
||||
asyncSetInlineRange(std, model, {
|
||||
index: model.text?.length ?? 0,
|
||||
length: 0,
|
||||
}).catch(console.error);
|
||||
return next({ updatedBlocks: [model] });
|
||||
};
|
||||
const appendDivider: Command<{}, { updatedBlocks: BlockModel[] }> = (
|
||||
_,
|
||||
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.getModelById(id);
|
||||
if (!newModel) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
return next({ updatedBlocks: [newModel] });
|
||||
};
|
||||
|
||||
const focusText: Command<{ updatedBlocks: BlockModel[] }> = (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(std, model)
|
||||
);
|
||||
const selectionManager = host.selection;
|
||||
const textSelection = selectionManager.find(TextSelection);
|
||||
if (!textSelection) {
|
||||
return false;
|
||||
}
|
||||
const newTextSelection = selectionManager.create(TextSelection, {
|
||||
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: BlockModel[] }> = (ctx, next) => {
|
||||
const { updatedBlocks } = ctx;
|
||||
if (!updatedBlocks || updatedBlocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectionManager = host.selection;
|
||||
|
||||
const blockSelections = selectionManager.filter(BlockSelection);
|
||||
if (blockSelections.length === 0) {
|
||||
return false;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const selections = updatedBlocks.map(model => {
|
||||
return selectionManager.create(BlockSelection, {
|
||||
blockId: model.id,
|
||||
});
|
||||
});
|
||||
|
||||
selectionManager.setGroup('note', selections);
|
||||
});
|
||||
return next();
|
||||
};
|
||||
|
||||
const [result, resultCtx] = std.command
|
||||
.chain()
|
||||
.pipe((_, next) => {
|
||||
doc.captureSync();
|
||||
return next();
|
||||
})
|
||||
// update block type
|
||||
.try<{ updatedBlocks: BlockModel[] }>(chain => [
|
||||
chain.pipe(mergeToCode),
|
||||
chain.pipe(appendDivider),
|
||||
chain.pipe((_, next) => {
|
||||
const newModels: BlockModel[] = [];
|
||||
blockModels.forEach(model => {
|
||||
if (
|
||||
!matchModels(model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
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.getModelById(newId);
|
||||
if (newModel) {
|
||||
newModels.push(newModel);
|
||||
}
|
||||
});
|
||||
return next({ updatedBlocks: newModels });
|
||||
}),
|
||||
])
|
||||
// focus
|
||||
.try(chain => [
|
||||
chain.pipe((_, next) => {
|
||||
if (['affine:code', 'affine:divider'].includes(flavour)) {
|
||||
return next();
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
chain.pipe(focusText),
|
||||
chain.pipe(focusBlock),
|
||||
chain.pipe((_, next) => next()),
|
||||
])
|
||||
.run();
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return next({ updatedBlocks: resultCtx.updatedBlocks });
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
export const changeNoteDisplayMode: Command<{
|
||||
noteId: string;
|
||||
mode: NoteDisplayMode;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
const { std, noteId, mode, stopCapture } = ctx;
|
||||
|
||||
const noteBlockModel = std.store.getBlock(noteId)?.model;
|
||||
if (!noteBlockModel || !matchModels(noteBlockModel, [NoteBlockModel])) return;
|
||||
|
||||
const currentMode = noteBlockModel.props.displayMode;
|
||||
if (currentMode === mode) return;
|
||||
|
||||
if (stopCapture) std.store.captureSync();
|
||||
|
||||
// Update the order in the note list in its parent, for example
|
||||
// 1. Both mode note | 1. Both mode note
|
||||
// 2. Page mode note | 2. Page mode note
|
||||
// ---------------------------- |-> 3. the changed note (Both or Page)
|
||||
// 3. Edgeless mode note | ---------------------------
|
||||
// 4. the changing edgeless note -| 4. Edgeless mode note
|
||||
const parent = std.store.getParent(noteBlockModel);
|
||||
if (parent) {
|
||||
const notes = parent.children.filter(child =>
|
||||
matchModels(child, [NoteBlockModel])
|
||||
);
|
||||
const firstEdgelessOnlyNote = notes.find(
|
||||
note => note.props.displayMode === NoteDisplayMode.EdgelessOnly
|
||||
);
|
||||
const lastPageVisibleNote = notes.findLast(
|
||||
note => note.props.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
);
|
||||
|
||||
if (currentMode === NoteDisplayMode.EdgelessOnly) {
|
||||
std.store.moveBlocks(
|
||||
[noteBlockModel],
|
||||
parent,
|
||||
lastPageVisibleNote ?? firstEdgelessOnlyNote,
|
||||
lastPageVisibleNote ? false : true
|
||||
);
|
||||
} else if (mode === NoteDisplayMode.EdgelessOnly) {
|
||||
std.store.moveBlocks(
|
||||
[noteBlockModel],
|
||||
parent,
|
||||
firstEdgelessOnlyNote ?? lastPageVisibleNote,
|
||||
firstEdgelessOnlyNote ? true : false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std.store.updateBlock(noteBlockModel, {
|
||||
displayMode: mode,
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
import { dedentBlock } from './dedent-block';
|
||||
|
||||
export const dedentBlockToRoot: Command<{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
let parent = store.getParent(model);
|
||||
let changed = false;
|
||||
while (parent && !matchModels(parent, [NoteBlockModel])) {
|
||||
if (!changed) {
|
||||
if (stopCapture) store.captureSync();
|
||||
changed = true;
|
||||
}
|
||||
std.command.exec(dedentBlock, { blockId: model.id, stopCapture: true });
|
||||
parent = store.getParent(model);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const dedentBlock: Command<{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const parent = store.getParent(model);
|
||||
const grandParent = parent && store.getParent(parent);
|
||||
if (store.readonly || !parent || parent.role !== 'content' || !grandParent) {
|
||||
// Top most, can not unindent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
store.moveBlocks([model, ...collapsedSiblings], grandParent, parent, false);
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const nextSiblings = store.getNexts(model);
|
||||
store.moveBlocks(nextSiblings, model);
|
||||
store.moveBlocks([model], grandParent, parent, false);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { dedentBlockToRoot } from './dedent-block-to-root';
|
||||
|
||||
export const dedentBlocksToRoot: Command<{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const text = std.selection.find(TextSelection);
|
||||
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 || store.readonly) return;
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
for (let i = blockIds.length - 1; i >= 0; i--) {
|
||||
const model = blockIds[i];
|
||||
const parent = store.getParent(model);
|
||||
if (parent && !matchModels(parent, [NoteBlockModel])) {
|
||||
std.command.exec(dedentBlockToRoot, {
|
||||
blockId: model,
|
||||
stopCapture: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { dedentBlock } from './dedent-block';
|
||||
|
||||
export const dedentBlocks: Command<{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store, selection, range, host } = std;
|
||||
const { schema } = store;
|
||||
|
||||
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 || store.readonly) return;
|
||||
|
||||
// Find the first model that can be unindented
|
||||
let firstDedentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const model = store.getBlock(blockIds[i])?.model;
|
||||
if (!model) continue;
|
||||
const parent = store.getParent(blockIds[i]);
|
||||
if (!parent) continue;
|
||||
const grandParent = store.getParent(parent);
|
||||
if (!grandParent) continue;
|
||||
|
||||
if (schema.isValid(model.flavour, grandParent.flavour)) {
|
||||
firstDedentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstDedentIndex === -1) return;
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstDedentIndex).forEach(id => {
|
||||
const model = store.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.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(TextSelection);
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ListBlockModel, ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const indentBlock: Command<{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
const { schema } = store;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const previousSibling = store.getPrev(model);
|
||||
if (
|
||||
store.readonly ||
|
||||
!previousSibling ||
|
||||
!schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
// can not indent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
store.moveBlocks([model, ...collapsedSiblings], previousSibling);
|
||||
} else {
|
||||
store.moveBlocks([model], previousSibling);
|
||||
}
|
||||
|
||||
// update collapsed state of affine list
|
||||
if (
|
||||
matchModels(previousSibling, [ListBlockModel]) &&
|
||||
previousSibling.props.collapsed
|
||||
) {
|
||||
store.updateBlock(previousSibling, {
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
getNearestHeadingBefore,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { indentBlock } from './indent-block';
|
||||
|
||||
export const indentBlocks: Command<{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store, selection, range, host } = std;
|
||||
const { schema } = store;
|
||||
|
||||
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 || store.readonly) return;
|
||||
|
||||
// Find the first model that can be indented
|
||||
let firstIndentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const previousSibling = store.getPrev(blockIds[i]);
|
||||
const model = store.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) store.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstIndentIndex).forEach(id => {
|
||||
const model = store.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.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 = store.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 &&
|
||||
matchModels(nearestHeading, [ParagraphBlockModel]) &&
|
||||
nearestHeading.props.collapsed
|
||||
) {
|
||||
store.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 &&
|
||||
matchModels(nearestHeading, [ParagraphBlockModel]) &&
|
||||
nearestHeading.props.collapsed
|
||||
) {
|
||||
store.updateBlock(nearestHeading, { collapsed: false });
|
||||
}
|
||||
}
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export { updateBlockType } from './block-type.js';
|
||||
export { changeNoteDisplayMode } from './change-note-display-mode.js';
|
||||
export { dedentBlock } from './dedent-block.js';
|
||||
export { dedentBlockToRoot } from './dedent-block-to-root.js';
|
||||
export { dedentBlocks } from './dedent-blocks.js';
|
||||
export { dedentBlocksToRoot } from './dedent-blocks-to-root.js';
|
||||
export { indentBlock } from './indent-block.js';
|
||||
export { indentBlocks } from './indent-blocks.js';
|
||||
export { selectBlock } from './select-block.js';
|
||||
export { selectBlocksBetween } from './select-blocks-between.js';
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type Command,
|
||||
} from '@blocksuite/std';
|
||||
|
||||
export const selectBlock: Command<{
|
||||
focusBlock?: BlockComponent;
|
||||
}> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create(BlockSelection, { blockId: focusBlock.blockId }),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type Command,
|
||||
} from '@blocksuite/std';
|
||||
|
||||
export const selectBlocksBetween: Command<{
|
||||
focusBlock?: BlockComponent;
|
||||
anchorBlock?: BlockComponent;
|
||||
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(BlockSelection, { blockId })]);
|
||||
return next();
|
||||
}
|
||||
|
||||
// In different blocks
|
||||
const selections = [...selection.value];
|
||||
if (selections.every(sel => sel.blockId !== focusBlock.blockId)) {
|
||||
if (tail) {
|
||||
selections.push(
|
||||
selection.create(BlockSelection, { blockId: focusBlock.blockId })
|
||||
);
|
||||
} else {
|
||||
selections.unshift(
|
||||
selection.create(BlockSelection, { 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