mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-24 05:07:06 +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,28 @@
|
||||
import { toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command, EditorHost } from '@blocksuite/std';
|
||||
|
||||
export const convertToNumberedListCommand: Command<
|
||||
{
|
||||
id: string;
|
||||
order: number; // This parameter may not correspond to the final order.
|
||||
stopCapturing?: boolean;
|
||||
},
|
||||
{
|
||||
listConvertedId: string;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
const { std, id, order, stopCapturing = true } = ctx;
|
||||
const host = std.host as EditorHost;
|
||||
const doc = host.doc;
|
||||
|
||||
const model = doc.getBlock(id)?.model;
|
||||
if (!model || !model.text) return;
|
||||
|
||||
if (stopCapturing) host.doc.captureSync();
|
||||
|
||||
const listConvertedId = toNumberedList(std, model, order);
|
||||
|
||||
if (!listConvertedId) return;
|
||||
|
||||
return next({ listConvertedId });
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
import { ListBlockModel } from '@blocksuite/affine-model';
|
||||
import type { IndentContext } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { correctNumberedListsOrderToPrev } from './utils.js';
|
||||
|
||||
export const canDedentListCommand: Command<
|
||||
Partial<Omit<IndentContext, 'flavour' | 'type'>>,
|
||||
{
|
||||
indentContext: IndentContext;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId, inlineIndex } = ctx;
|
||||
const { std } = ctx;
|
||||
const { selection, store } = std;
|
||||
if (!blockId) {
|
||||
const text = selection.find(TextSelection);
|
||||
/**
|
||||
* Do nothing if the selection:
|
||||
* - is not a text selection
|
||||
* - or spans multiple blocks
|
||||
*/
|
||||
if (!text || (text.to && text.from.blockId !== text.to.blockId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
blockId = text.from.blockId;
|
||||
inlineIndex = text.from.index;
|
||||
}
|
||||
if (blockId == null || inlineIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* initial state:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc <- unindent
|
||||
* - ddd
|
||||
* - eee
|
||||
* - fff
|
||||
*
|
||||
* final state:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc
|
||||
* - ddd
|
||||
* - eee
|
||||
* - fff
|
||||
*/
|
||||
|
||||
/**
|
||||
* ccc
|
||||
*/
|
||||
const model = store.getBlock(blockId)?.model;
|
||||
if (!model || !matchModels(model, [ListBlockModel])) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* bbb
|
||||
*/
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
if (store.readonly || parent.role !== 'content') {
|
||||
// Top most list cannot be unindent
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* aaa
|
||||
*/
|
||||
const grandParent = store.getParent(parent);
|
||||
if (!grandParent) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* ccc index
|
||||
*/
|
||||
const modelIndex = parent.children.indexOf(model);
|
||||
if (modelIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
return next({
|
||||
indentContext: {
|
||||
blockId,
|
||||
inlineIndex,
|
||||
type: 'dedent',
|
||||
flavour: 'affine:list',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const dedentListCommand: Command<{
|
||||
indentContext: IndentContext;
|
||||
}> = (ctx, next) => {
|
||||
const { indentContext: dedentContext, std } = ctx;
|
||||
const { store, selection, range, host } = std;
|
||||
|
||||
if (
|
||||
!dedentContext ||
|
||||
dedentContext.type !== 'dedent' ||
|
||||
dedentContext.flavour !== 'affine:list'
|
||||
) {
|
||||
console.warn(
|
||||
'you need to use `canDedentList` command before running `dedentList` command'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { blockId } = dedentContext;
|
||||
|
||||
const model = store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const grandParent = store.getParent(parent);
|
||||
if (!grandParent) return;
|
||||
|
||||
store.captureSync();
|
||||
|
||||
/**
|
||||
* step 1:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc
|
||||
* - ddd
|
||||
* - eee <- make eee as ccc's child
|
||||
* - fff
|
||||
*/
|
||||
const nextSiblings = store.getNexts(model); // [eee]
|
||||
store.moveBlocks(nextSiblings, model);
|
||||
/**
|
||||
* eee
|
||||
*/
|
||||
const nextSibling = nextSiblings.at(0);
|
||||
if (nextSibling) correctNumberedListsOrderToPrev(store, nextSibling);
|
||||
|
||||
/**
|
||||
* step 2:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc <- make ccc as aaa's child
|
||||
* - ddd
|
||||
* - eee
|
||||
* - fff
|
||||
*/
|
||||
store.moveBlocks([model], grandParent, parent, false);
|
||||
correctNumberedListsOrderToPrev(store, model);
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import { ListBlockModel, ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import type { IndentContext } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
getNearestHeadingBefore,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { correctNumberedListsOrderToPrev } from './utils.js';
|
||||
|
||||
export const canIndentListCommand: Command<
|
||||
Partial<Omit<IndentContext, 'type' | 'flavour'>>,
|
||||
{
|
||||
indentContext: IndentContext;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
let { blockId, inlineIndex } = ctx;
|
||||
const { std } = ctx;
|
||||
const { selection, store } = std;
|
||||
if (!blockId) {
|
||||
const text = selection.find(TextSelection);
|
||||
/**
|
||||
* Do nothing if the selection:
|
||||
* - is not a text selection
|
||||
* - or spans multiple blocks
|
||||
*/
|
||||
if (!text || (text.to && text.from.blockId !== text.to.blockId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
blockId = text.from.blockId;
|
||||
inlineIndex = text.from.index;
|
||||
}
|
||||
if (blockId == null || inlineIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* initial state:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc <- indent
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* final state:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
|
||||
/**
|
||||
* ccc
|
||||
*/
|
||||
const model = store.getBlock(blockId)?.model;
|
||||
if (!model || !matchModels(model, [ListBlockModel])) {
|
||||
return;
|
||||
}
|
||||
const schema = std.store.schema;
|
||||
/**
|
||||
* aaa
|
||||
*/
|
||||
const previousSibling = store.getPrev(model);
|
||||
if (
|
||||
store.readonly ||
|
||||
!previousSibling ||
|
||||
!schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
// cannot indent, do nothing
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* eee
|
||||
*/
|
||||
// const nextSibling = store.getNext(model);
|
||||
|
||||
return next({
|
||||
indentContext: {
|
||||
blockId,
|
||||
inlineIndex,
|
||||
type: 'indent',
|
||||
flavour: 'affine:list',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const indentListCommand: Command<{
|
||||
indentContext: IndentContext;
|
||||
}> = (ctx, next) => {
|
||||
const { indentContext, std } = ctx;
|
||||
if (
|
||||
!indentContext ||
|
||||
indentContext.type !== 'indent' ||
|
||||
indentContext.flavour !== 'affine:list'
|
||||
) {
|
||||
console.warn(
|
||||
'you need to use `canIndentList` command before running `indentList` command'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { blockId } = indentContext;
|
||||
const { store, selection, host, range } = std;
|
||||
|
||||
const model = store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const previousSibling = store.getPrev(model);
|
||||
if (!previousSibling) return;
|
||||
|
||||
const nextSibling = store.getNext(model);
|
||||
|
||||
store.captureSync();
|
||||
|
||||
store.moveBlocks([model], previousSibling);
|
||||
correctNumberedListsOrderToPrev(store, model);
|
||||
if (nextSibling) correctNumberedListsOrderToPrev(store, nextSibling);
|
||||
|
||||
// 123
|
||||
// > # 456
|
||||
// 789
|
||||
//
|
||||
// we need to update 456 collapsed state to false when indent 789
|
||||
const nearestHeading = getNearestHeadingBefore(model);
|
||||
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,5 @@
|
||||
export { convertToNumberedListCommand } from './convert-to-numbered-list.js';
|
||||
export { canDedentListCommand, dedentListCommand } from './dedent-list.js';
|
||||
export { canIndentListCommand, indentListCommand } from './indent-list.js';
|
||||
export { listToParagraphCommand } from './list-to-paragraph.js';
|
||||
export { splitListCommand } from './split-list.js';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ListBlockModel } from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
export const listToParagraphCommand: Command<
|
||||
{
|
||||
id: string;
|
||||
stopCapturing?: boolean;
|
||||
},
|
||||
{
|
||||
listConvertedId: string;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
const { id, stopCapturing = true } = ctx;
|
||||
const std = ctx.std;
|
||||
const doc = std.store;
|
||||
const model = doc.getBlock(id)?.model;
|
||||
|
||||
if (!model || !matchModels(model, [ListBlockModel])) return false;
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return false;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
const blockProps = {
|
||||
type: 'text' as const,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
};
|
||||
if (stopCapturing) std.store.captureSync();
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
const listConvertedId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
blockProps,
|
||||
parent,
|
||||
index
|
||||
);
|
||||
focusTextModel(std, listConvertedId);
|
||||
return next({ listConvertedId });
|
||||
};
|
||||
@@ -0,0 +1,250 @@
|
||||
import { ListBlockModel } from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
getNextContinuousNumberedLists,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command, EditorHost } from '@blocksuite/std';
|
||||
|
||||
import { canDedentListCommand, dedentListCommand } from './dedent-list.js';
|
||||
import { correctNumberedListsOrderToPrev } from './utils.js';
|
||||
|
||||
export const splitListCommand: Command<{
|
||||
blockId: string;
|
||||
inlineIndex: number;
|
||||
}> = (ctx, next) => {
|
||||
const { blockId, inlineIndex, std } = ctx;
|
||||
const host = std.host as EditorHost;
|
||||
const doc = host.doc;
|
||||
|
||||
const model = doc.getBlock(blockId)?.model;
|
||||
if (!model || !matchModels(model, [ListBlockModel])) {
|
||||
console.error(`block ${blockId} is not a list block`);
|
||||
return;
|
||||
}
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
console.error(`block ${blockId} has no parent`);
|
||||
return;
|
||||
}
|
||||
const modelIndex = parent.children.indexOf(model);
|
||||
if (modelIndex === -1) {
|
||||
console.error(`block ${blockId} is not a child of its parent`);
|
||||
return;
|
||||
}
|
||||
|
||||
doc.captureSync();
|
||||
|
||||
if (model.props.text.length === 0) {
|
||||
/**
|
||||
* case 1: target is top most, convert the list into a paragraph
|
||||
*
|
||||
* before:
|
||||
* - aaa
|
||||
* - | <- split here
|
||||
* - bbb
|
||||
*
|
||||
* after:
|
||||
* - aaa
|
||||
* |
|
||||
* - bbb
|
||||
*/
|
||||
if (parent.role === 'hub') {
|
||||
const id = doc.addBlock('affine:paragraph', {}, parent, modelIndex);
|
||||
const paragraph = doc.getBlock(id);
|
||||
if (!paragraph) return;
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: paragraph.model,
|
||||
});
|
||||
|
||||
// reset next continuous numbered list's order
|
||||
const nextContinuousNumberedLists = getNextContinuousNumberedLists(
|
||||
doc,
|
||||
paragraph.model
|
||||
);
|
||||
let base = 1;
|
||||
nextContinuousNumberedLists.forEach(list => {
|
||||
doc.transact(() => {
|
||||
list.props.order = base;
|
||||
});
|
||||
base += 1;
|
||||
});
|
||||
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
focusTextModel(std, id);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* case 2: not top most, unindent the list
|
||||
*
|
||||
* before:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - | <- split here
|
||||
* - ccc
|
||||
*
|
||||
* after:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - |
|
||||
* - ccc
|
||||
*/
|
||||
if (parent.role === 'content') {
|
||||
host.command
|
||||
.chain()
|
||||
.pipe(canDedentListCommand, {
|
||||
blockId,
|
||||
inlineIndex: 0,
|
||||
})
|
||||
.pipe(dedentListCommand)
|
||||
.run();
|
||||
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let newListId: string | null = null;
|
||||
|
||||
if (model.children.length > 0 && !model.props.collapsed) {
|
||||
const afterText = model.props.text.split(inlineIndex);
|
||||
if (inlineIndex === 0) {
|
||||
/**
|
||||
* case 3: list has children (list not collapsed), split the list at the start of line
|
||||
*
|
||||
* before:
|
||||
* - |aaa <- split here
|
||||
* - bbb
|
||||
*
|
||||
* after:
|
||||
* -
|
||||
* - |aaa
|
||||
* - bbb
|
||||
*/
|
||||
newListId = doc.addBlock(
|
||||
'affine:list',
|
||||
{
|
||||
type: model.props.type,
|
||||
text: afterText,
|
||||
order:
|
||||
model.props.type === 'numbered' && model.props.order !== null
|
||||
? model.props.order + 1
|
||||
: null,
|
||||
},
|
||||
parent,
|
||||
modelIndex + 1
|
||||
);
|
||||
const newList = doc.getBlock(newListId)?.model;
|
||||
if (!newList) return;
|
||||
// move children to new list
|
||||
doc.moveBlocks(model.children, newList);
|
||||
|
||||
if (model.props.type === 'numbered' && model.props.order !== null) {
|
||||
const nextContinuousNumberedLists = getNextContinuousNumberedLists(
|
||||
doc,
|
||||
newListId
|
||||
);
|
||||
let base = model.props.order + 2;
|
||||
nextContinuousNumberedLists.forEach(list => {
|
||||
doc.transact(() => {
|
||||
list.props.order = base;
|
||||
});
|
||||
base += 1;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* case 4: list has children (list not collapsed), split the list not at the start of line
|
||||
*
|
||||
* before:
|
||||
* - aa|a <- split here
|
||||
* - bbb
|
||||
*
|
||||
* after:
|
||||
* - aa
|
||||
* - |a
|
||||
* - bbb
|
||||
*/
|
||||
newListId = doc.addBlock(
|
||||
'affine:list',
|
||||
{
|
||||
type: model.props.type,
|
||||
text: afterText,
|
||||
order: model.props.type === 'numbered' ? 1 : null,
|
||||
},
|
||||
model,
|
||||
0
|
||||
);
|
||||
|
||||
if (model.props.type === 'numbered') {
|
||||
const nextContinuousNumberedLists = getNextContinuousNumberedLists(
|
||||
doc,
|
||||
newListId
|
||||
);
|
||||
let base = 2;
|
||||
nextContinuousNumberedLists.forEach(list => {
|
||||
doc.transact(() => {
|
||||
list.props.order = base;
|
||||
});
|
||||
base += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* case 5: list has children (list collapsed)
|
||||
*
|
||||
* before:
|
||||
* - aa|a <- split here
|
||||
* - bbb
|
||||
*
|
||||
* after:
|
||||
* - aa
|
||||
* - bbb
|
||||
* - |a
|
||||
*
|
||||
*
|
||||
* case 6: list does not have children
|
||||
*
|
||||
* before:
|
||||
* - aa|a <- split here
|
||||
* - bbb
|
||||
*
|
||||
* after:
|
||||
* - aa
|
||||
* - |a
|
||||
* - bbb
|
||||
*/
|
||||
const afterText = model.props.text.split(inlineIndex);
|
||||
newListId = doc.addBlock(
|
||||
'affine:list',
|
||||
{
|
||||
type: model.props.type,
|
||||
text: afterText,
|
||||
order: null,
|
||||
},
|
||||
parent,
|
||||
modelIndex + 1
|
||||
);
|
||||
correctNumberedListsOrderToPrev(doc, newListId);
|
||||
}
|
||||
|
||||
if (newListId) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
focusTextModel(std, newListId);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
next();
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ListBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
getNextContinuousNumberedLists,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockModel, Store } from '@blocksuite/store';
|
||||
|
||||
/**
|
||||
* correct target is a numbered list, which is divided into two steps:
|
||||
* 1. check if there is a numbered list before the target list. If so, adjust the order of the target list
|
||||
* to the order of the previous list plus 1, otherwise set the order to 1
|
||||
* 2. find continuous lists starting from the target list and keep their order continuous
|
||||
*/
|
||||
export function correctNumberedListsOrderToPrev(
|
||||
doc: Store,
|
||||
modelOrId: BlockModel | string,
|
||||
transact = true
|
||||
) {
|
||||
const model =
|
||||
typeof modelOrId === 'string' ? doc.getBlock(modelOrId)?.model : modelOrId;
|
||||
|
||||
if (!model) return;
|
||||
|
||||
if (
|
||||
!matchModels(model, [ListBlockModel]) ||
|
||||
model.props.type$.value !== 'numbered'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = () => {
|
||||
// step 1
|
||||
const previousSibling = doc.getPrev(model);
|
||||
if (
|
||||
previousSibling &&
|
||||
matchModels(previousSibling, [ListBlockModel]) &&
|
||||
previousSibling.props.type === 'numbered'
|
||||
) {
|
||||
if (!previousSibling.props.order) previousSibling.props.order = 1;
|
||||
model.props.order = previousSibling.props.order + 1;
|
||||
} else {
|
||||
model.props.order = 1;
|
||||
}
|
||||
|
||||
// step 2
|
||||
let base = model.props.order + 1;
|
||||
const continuousNumberedLists = getNextContinuousNumberedLists(doc, model);
|
||||
continuousNumberedLists.forEach(list => {
|
||||
list.props.order = base;
|
||||
base++;
|
||||
});
|
||||
};
|
||||
|
||||
if (transact) {
|
||||
doc.transact(fn);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
export function correctListOrder(doc: Store, model: ListBlockModel) {
|
||||
// old numbered list has no order
|
||||
if (model.props.type === 'numbered' && !Number.isInteger(model.props.order)) {
|
||||
correctNumberedListsOrderToPrev(doc, model, false);
|
||||
}
|
||||
// if list is not numbered, order should be null
|
||||
if (model.props.type !== 'numbered') {
|
||||
model.props.order = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user