mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-30 00:29:46 +08:00
204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
import { ListBlockSchema } from '@blocksuite/affine-model';
|
|
import {
|
|
BlockHtmlAdapterExtension,
|
|
type BlockHtmlAdapterMatcher,
|
|
HastUtils,
|
|
TextUtils,
|
|
} from '@blocksuite/affine-shared/adapters';
|
|
import type { DeltaInsert } from '@blocksuite/inline';
|
|
import { nanoid } from '@blocksuite/store';
|
|
import type { Element } from 'hast';
|
|
|
|
export const listBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
|
flavour: ListBlockSchema.model.flavour,
|
|
toMatch: o => HastUtils.isElement(o.node) && o.node.tagName === 'li',
|
|
fromMatch: o => o.node.flavour === ListBlockSchema.model.flavour,
|
|
toBlockSnapshot: {
|
|
enter: (o, context) => {
|
|
if (!HastUtils.isElement(o.node)) {
|
|
return;
|
|
}
|
|
|
|
const parentList = o.parent?.node as Element;
|
|
let listType = 'bulleted';
|
|
if (parentList.tagName === 'ol') {
|
|
listType = 'numbered';
|
|
} else if (Array.isArray(parentList.properties?.className)) {
|
|
if (parentList.properties.className.includes('to-do-list')) {
|
|
listType = 'todo';
|
|
} else if (parentList.properties.className.includes('toggle')) {
|
|
listType = 'toggle';
|
|
} else if (parentList.properties.className.includes('bulleted-list')) {
|
|
listType = 'bulleted';
|
|
}
|
|
}
|
|
|
|
const listNumber =
|
|
typeof parentList.properties.start === 'number'
|
|
? parentList.properties.start + parentList.children.indexOf(o.node)
|
|
: null;
|
|
const firstElementChild = HastUtils.getElementChildren(o.node)[0];
|
|
o.node = HastUtils.flatNodes(
|
|
o.node,
|
|
tagName => tagName === 'div' || tagName === 'p'
|
|
) as Element;
|
|
|
|
const { walkerContext, deltaConverter } = context;
|
|
walkerContext.openNode(
|
|
{
|
|
type: 'block',
|
|
id: nanoid(),
|
|
flavour: 'affine:list',
|
|
props: {
|
|
type: listType,
|
|
text: {
|
|
'$blocksuite:internal:text$': true,
|
|
delta:
|
|
listType !== 'toggle'
|
|
? deltaConverter.astToDelta(
|
|
HastUtils.getInlineOnlyElementAST(o.node)
|
|
)
|
|
: deltaConverter.astToDelta(
|
|
HastUtils.querySelector(o.node, 'summary') ?? o.node
|
|
),
|
|
},
|
|
checked:
|
|
listType === 'todo'
|
|
? firstElementChild &&
|
|
Array.isArray(firstElementChild.properties?.className) &&
|
|
firstElementChild.properties.className.includes('checkbox-on')
|
|
: false,
|
|
collapsed:
|
|
listType === 'toggle'
|
|
? firstElementChild &&
|
|
firstElementChild.tagName === 'details' &&
|
|
firstElementChild.properties.open === undefined
|
|
: false,
|
|
order: listNumber,
|
|
},
|
|
children: [],
|
|
},
|
|
'children'
|
|
);
|
|
},
|
|
leave: (_, context) => {
|
|
const { walkerContext } = context;
|
|
walkerContext.closeNode();
|
|
},
|
|
},
|
|
fromBlockSnapshot: {
|
|
enter: (o, context) => {
|
|
const text = (o.node.props.text ?? { delta: [] }) as {
|
|
delta: DeltaInsert[];
|
|
};
|
|
const { deltaConverter, walkerContext } = context;
|
|
const currentTNode = walkerContext.currentNode();
|
|
const liChildren = deltaConverter.deltaToAST(text.delta);
|
|
if (o.node.props.type === 'todo') {
|
|
liChildren.unshift({
|
|
type: 'element',
|
|
tagName: 'input',
|
|
properties: {
|
|
type: 'checkbox',
|
|
checked: o.node.props.checked as boolean,
|
|
},
|
|
children: [
|
|
{
|
|
type: 'element',
|
|
tagName: 'label',
|
|
properties: {
|
|
style: 'margin-right: 3px;',
|
|
},
|
|
children: [],
|
|
},
|
|
],
|
|
});
|
|
}
|
|
// check if the list is of the same type
|
|
if (
|
|
walkerContext.getNodeContext('affine:list:parent') === o.parent &&
|
|
currentTNode.type === 'element' &&
|
|
currentTNode.tagName ===
|
|
(o.node.props.type === 'numbered' ? 'ol' : 'ul') &&
|
|
!(
|
|
Array.isArray(currentTNode.properties.className) &&
|
|
currentTNode.properties.className.includes('todo-list')
|
|
) ===
|
|
TextUtils.isNullish(
|
|
o.node.props.type === 'todo'
|
|
? (o.node.props.checked as boolean)
|
|
: undefined
|
|
)
|
|
) {
|
|
// if true, add the list item to the list
|
|
} else {
|
|
// if false, create a new list
|
|
walkerContext.openNode(
|
|
{
|
|
type: 'element',
|
|
tagName: o.node.props.type === 'numbered' ? 'ol' : 'ul',
|
|
properties: {
|
|
style:
|
|
o.node.props.type === 'todo'
|
|
? 'list-style-type: none; padding-inline-start: 18px;'
|
|
: null,
|
|
className: [o.node.props.type + '-list'],
|
|
},
|
|
children: [],
|
|
},
|
|
'children'
|
|
);
|
|
walkerContext.setNodeContext('affine:list:parent', o.parent);
|
|
}
|
|
|
|
walkerContext.openNode(
|
|
{
|
|
type: 'element',
|
|
tagName: 'li',
|
|
properties: {
|
|
className: ['affine-list-block-container'],
|
|
},
|
|
children: liChildren,
|
|
},
|
|
'children'
|
|
);
|
|
},
|
|
leave: (o, context) => {
|
|
const { walkerContext } = context;
|
|
const currentTNode = walkerContext.currentNode() as Element;
|
|
const previousTNode = walkerContext.previousNode() as Element;
|
|
if (
|
|
walkerContext.getPreviousNodeContext('affine:list:parent') ===
|
|
o.parent &&
|
|
currentTNode.tagName === 'li' &&
|
|
previousTNode.tagName ===
|
|
(o.node.props.type === 'numbered' ? 'ol' : 'ul') &&
|
|
!(
|
|
Array.isArray(previousTNode.properties.className) &&
|
|
previousTNode.properties.className.includes('todo-list')
|
|
) ===
|
|
TextUtils.isNullish(
|
|
o.node.props.type === 'todo'
|
|
? (o.node.props.checked as boolean)
|
|
: undefined
|
|
)
|
|
) {
|
|
walkerContext.closeNode();
|
|
if (
|
|
o.next?.flavour !== 'affine:list' ||
|
|
o.next.props.type !== o.node.props.type
|
|
) {
|
|
// If the next node is not a list or different type of list, close the list
|
|
walkerContext.closeNode();
|
|
}
|
|
} else {
|
|
walkerContext.closeNode().closeNode();
|
|
}
|
|
},
|
|
},
|
|
};
|
|
|
|
export const ListBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
|
listBlockHtmlAdapterMatcher
|
|
);
|