Lots of additions from outline.

This commit is contained in:
Laur Ivan 2022-06-10 14:35:18 +02:00
parent 77cb482a76
commit 5e2de33a3e
2 changed files with 309 additions and 0 deletions

View 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);
};
}

View 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' };
}
}