generated from laur/svelte-tailwind-storybook
Lots of additions from outline.
This commit is contained in:
parent
77cb482a76
commit
5e2de33a3e
47
src/lib/editor/commands/toggleList.ts
Normal file
47
src/lib/editor/commands/toggleList.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { NodeType } from 'prosemirror-model';
|
||||
import { wrapInList, liftListItem } from 'prosemirror-schema-list';
|
||||
import type { EditorState } from 'prosemirror-state';
|
||||
import { findParentNode } from 'prosemirror-utils';
|
||||
import chainTransactions from '../lib/chainTransactions';
|
||||
import isList from '../queries/isList';
|
||||
import type { Dispatch } from '../types/dispatch';
|
||||
import clearNodes from './clearNodes';
|
||||
|
||||
export default function toggleList(listType: NodeType, itemType: NodeType) {
|
||||
return (state: EditorState, dispatch?: Dispatch) => {
|
||||
const { schema, selection } = state;
|
||||
const { $from, $to } = selection;
|
||||
const range = $from.blockRange($to);
|
||||
const { tr } = state;
|
||||
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentList = findParentNode((node) => isList(node, schema))(selection);
|
||||
|
||||
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
|
||||
if (parentList.node.type === listType) {
|
||||
return liftListItem(itemType)(state, dispatch);
|
||||
}
|
||||
|
||||
if (isList(parentList.node, schema) && listType.validContent(parentList.node.content)) {
|
||||
tr.setNodeMarkup(parentList.pos, listType);
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const canWrapInList = wrapInList(listType)(state);
|
||||
|
||||
if (canWrapInList) {
|
||||
return wrapInList(listType)(state, dispatch);
|
||||
}
|
||||
|
||||
return chainTransactions(clearNodes(), wrapInList(listType))(state, dispatch);
|
||||
};
|
||||
}
|
262
src/lib/editor/nodes/ListItem.ts
Normal file
262
src/lib/editor/nodes/ListItem.ts
Normal file
@ -0,0 +1,262 @@
|
||||
import type { NodeSpec, NodeType, Node as ProsemirrorNode } from 'prosemirror-model';
|
||||
import { splitListItem, sinkListItem, liftListItem } from 'prosemirror-schema-list';
|
||||
import { Transaction, EditorState, Plugin, TextSelection } from 'prosemirror-state';
|
||||
import { findParentNodeClosestToPos } from 'prosemirror-utils';
|
||||
import { DecorationSet, Decoration } from 'prosemirror-view';
|
||||
import { MarkdownSerializerState } from '../lib/markdown/serializer';
|
||||
import getParentListItem from '../queries/getParentListItem';
|
||||
import isInList from '../queries/isInList';
|
||||
import isList from '../queries/isList';
|
||||
import { Dispatch } from '../types';
|
||||
import Node from './Node';
|
||||
|
||||
export default class ListItem extends Node {
|
||||
get name() {
|
||||
return 'list_item';
|
||||
}
|
||||
|
||||
get schema(): NodeSpec {
|
||||
return {
|
||||
content: 'paragraph block*',
|
||||
defining: true,
|
||||
draggable: true,
|
||||
parseDOM: [{ tag: 'li' }],
|
||||
toDOM: () => ['li', 0]
|
||||
};
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
state: {
|
||||
init() {
|
||||
return DecorationSet.empty;
|
||||
},
|
||||
apply: (
|
||||
tr: Transaction,
|
||||
set: DecorationSet,
|
||||
oldState: EditorState,
|
||||
newState: EditorState
|
||||
) => {
|
||||
const action = tr.getMeta('li');
|
||||
if (!action && !tr.docChanged) {
|
||||
return set;
|
||||
}
|
||||
|
||||
// Adjust decoration positions to changes made by the transaction
|
||||
set = set.map(tr.mapping, tr.doc);
|
||||
|
||||
switch (action?.event) {
|
||||
case 'mouseover': {
|
||||
const result = findParentNodeClosestToPos(
|
||||
newState.doc.resolve(action.pos),
|
||||
(node) => node.type.name === this.name || node.type.name === 'checkbox_item'
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return set;
|
||||
}
|
||||
|
||||
const list = findParentNodeClosestToPos(newState.doc.resolve(action.pos), (node) =>
|
||||
isList(node, this.editor.schema)
|
||||
);
|
||||
|
||||
if (!list) {
|
||||
return set;
|
||||
}
|
||||
|
||||
const start = list.node.attrs.order || 1;
|
||||
|
||||
let listItemNumber = 0;
|
||||
list.node.content.forEach((li, _, index) => {
|
||||
if (li === result.node) {
|
||||
listItemNumber = index;
|
||||
}
|
||||
});
|
||||
|
||||
const counterLength = String(start + listItemNumber).length;
|
||||
|
||||
return set.add(tr.doc, [
|
||||
Decoration.node(
|
||||
result.pos,
|
||||
result.pos + result.node.nodeSize,
|
||||
{
|
||||
class: `hovering`
|
||||
},
|
||||
{
|
||||
hover: true
|
||||
}
|
||||
),
|
||||
Decoration.node(result.pos, result.pos + result.node.nodeSize, {
|
||||
class: `counter-${counterLength}`
|
||||
})
|
||||
]);
|
||||
}
|
||||
case 'mouseout': {
|
||||
const result = findParentNodeClosestToPos(
|
||||
newState.doc.resolve(action.pos),
|
||||
(node) => node.type.name === this.name || node.type.name === 'checkbox_item'
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return set;
|
||||
}
|
||||
|
||||
return set.remove(
|
||||
set.find(result.pos, result.pos + result.node.nodeSize, (spec) => spec.hover)
|
||||
);
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
handleDOMEvents: {
|
||||
mouseover: (view, event) => {
|
||||
const { state, dispatch } = view;
|
||||
const target = event.target as HTMLElement;
|
||||
const li = target?.closest('li');
|
||||
|
||||
if (!li) {
|
||||
return false;
|
||||
}
|
||||
if (!view.dom.contains(li)) {
|
||||
return false;
|
||||
}
|
||||
const pos = view.posAtDOM(li, 0);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
state.tr.setMeta('li', {
|
||||
event: 'mouseover',
|
||||
pos
|
||||
})
|
||||
);
|
||||
return false;
|
||||
},
|
||||
mouseout: (view, event) => {
|
||||
const { state, dispatch } = view;
|
||||
const target = event.target as HTMLElement;
|
||||
const li = target?.closest('li');
|
||||
|
||||
if (!li) {
|
||||
return false;
|
||||
}
|
||||
if (!view.dom.contains(li)) {
|
||||
return false;
|
||||
}
|
||||
const pos = view.posAtDOM(li, 0);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
state.tr.setMeta('li', {
|
||||
event: 'mouseout',
|
||||
pos
|
||||
})
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
keys({ type }: { type: NodeType }) {
|
||||
return {
|
||||
Enter: splitListItem(type),
|
||||
Tab: sinkListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
'Mod-]': sinkListItem(type),
|
||||
'Mod-[': liftListItem(type),
|
||||
'Shift-Enter': (state: EditorState, dispatch: Dispatch) => {
|
||||
if (!isInList(state)) {
|
||||
return false;
|
||||
}
|
||||
if (!state.selection.empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { tr, selection } = state;
|
||||
dispatch(tr.split(selection.to));
|
||||
return true;
|
||||
},
|
||||
'Alt-ArrowUp': (state: EditorState, dispatch: Dispatch) => {
|
||||
if (!state.selection.empty) {
|
||||
return false;
|
||||
}
|
||||
const result = getParentListItem(state);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [li, pos] = result;
|
||||
const $pos = state.doc.resolve(pos);
|
||||
|
||||
if (
|
||||
!$pos.nodeBefore ||
|
||||
!['list_item', 'checkbox_item'].includes($pos.nodeBefore.type.name)
|
||||
) {
|
||||
console.log('Node before not a list item');
|
||||
return false;
|
||||
}
|
||||
|
||||
const { tr } = state;
|
||||
const newPos = pos - $pos.nodeBefore.nodeSize;
|
||||
|
||||
dispatch(
|
||||
tr
|
||||
.delete(pos, pos + li.nodeSize)
|
||||
.insert(newPos, li)
|
||||
.setSelection(TextSelection.near(tr.doc.resolve(newPos)))
|
||||
);
|
||||
return true;
|
||||
},
|
||||
'Alt-ArrowDown': (state: EditorState, dispatch: Dispatch) => {
|
||||
if (!state.selection.empty) {
|
||||
return false;
|
||||
}
|
||||
const result = getParentListItem(state);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [li, pos] = result;
|
||||
const $pos = state.doc.resolve(pos + li.nodeSize);
|
||||
|
||||
if (!$pos.nodeAfter || !['list_item', 'checkbox_item'].includes($pos.nodeAfter.type.name)) {
|
||||
console.log('Node after not a list item');
|
||||
return false;
|
||||
}
|
||||
|
||||
const { tr } = state;
|
||||
const newPos = pos + li.nodeSize + $pos.nodeAfter.nodeSize;
|
||||
|
||||
dispatch(
|
||||
tr
|
||||
.insert(newPos, li)
|
||||
.setSelection(TextSelection.near(tr.doc.resolve(newPos)))
|
||||
.delete(pos, pos + li.nodeSize)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
||||
state.renderContent(node);
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { block: 'list_item' };
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user