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:
Saul-Mirone
2025-04-07 12:34:40 +00:00
parent e1bd2047c4
commit 1f45cc5dec
893 changed files with 439 additions and 460 deletions
@@ -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();
};