@bangle.dev/core
Advanced tools
Comparing version 0.3.0 to 0.3.1
@@ -11,2 +11,3 @@ /** | ||
} from '../../test-helpers/index'; | ||
import { smartNodesBetween } from '../bullet-list'; | ||
import { listItem } from '../index'; | ||
@@ -198,4 +199,3 @@ | ||
// TODO make it more intuitive | ||
it('when calling toggleTodo on bulletList it converts to paragraph', async () => { | ||
it('nested todo list to plain li', async () => { | ||
const { editorView } = await testEditor( | ||
@@ -223,15 +223,2 @@ <doc> | ||
<para>first</para> | ||
</li> | ||
</ul> | ||
<para>[]second</para> | ||
</doc>, | ||
); | ||
}); | ||
it.skip('Toggles nested ordered list to todo item', async () => { | ||
const { editorView } = await testEditor( | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
@@ -246,22 +233,449 @@ <li> | ||
); | ||
}); | ||
}); | ||
sendKeyToPm(editorView, 'Ctrl-Shift-7'); | ||
test.each([ | ||
[ | ||
'plain list', | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>[] first</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>[] first</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
expect(editorView.state.doc).toEqualDocument( | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>[]second</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
); | ||
}); | ||
[ | ||
'plain todo list ', | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>[] first</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<para>[] first</para> | ||
</doc>, | ||
], | ||
[ | ||
'plain nested list', | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li> | ||
<para>[]second</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>[]second</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
[ | ||
'plain nested list 2', | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li> | ||
<para>mango</para> | ||
</li> | ||
<li> | ||
<para>[]second</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>[]second</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
[ | ||
'Single todo item with many vanilla lists', | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={true}> | ||
<para>alpha</para> | ||
</li> | ||
<li> | ||
<para>[]mango</para> | ||
</li> | ||
<li> | ||
<para>charlie</para> | ||
</li> | ||
<li> | ||
<para>tango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={true}> | ||
<para>[]alpha</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>charlie</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>tango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
[ | ||
'toggling parent doesnt affect children', | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>first[]</para> | ||
<ul> | ||
<li> | ||
<para>alpha</para> | ||
</li> | ||
<li> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first[]</para> | ||
<ul> | ||
<li> | ||
<para>alpha</para> | ||
</li> | ||
<li> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
[ | ||
'todo item', | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>[]first</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>alpha</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<para>first[]</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>alpha</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
[ | ||
'nested todo item', | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>[]alpha</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
</li> | ||
</ul> | ||
<para>alpha[]</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
[ | ||
'toggling nested list with selection inside the nested lists', | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>first</para> | ||
<ul> | ||
<li> | ||
<para>[alpha</para> | ||
</li> | ||
<li> | ||
<para>mang]o</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>alpha</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
// TODO this is not intuitive | ||
[ | ||
'toggling nested list with selection spanning parent and child lists', | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>f[irst</para> | ||
<ul> | ||
<li> | ||
<para>alpha</para> | ||
</li> | ||
<li> | ||
<para>mang]o</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>alpha</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
{/** the item below should have had todoChecked */} | ||
<li> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
])('toggleTodoList: Case %# %s', async (name, input, expected) => { | ||
const { editorView } = await testEditor(input); | ||
sendKeyToPm(editorView, 'Ctrl-Shift-7'); | ||
expect(editorView.state.doc).toEqualDocument(expected); | ||
}); | ||
test.each([ | ||
[ | ||
'todo list', | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>[] first</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>[] first</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
[ | ||
'plain nested list', | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
<ul> | ||
<li> | ||
<para>[]second</para> | ||
</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>first</para> | ||
</li> | ||
</ul> | ||
<para>[]second</para> | ||
</doc>, | ||
], | ||
[ | ||
"toggling parent doesn't affect children", | ||
<doc> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>firs[]t</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>alpha</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>first[]</para> | ||
<ul> | ||
<li todoChecked={false}> | ||
<para>alpha</para> | ||
</li> | ||
<li todoChecked={false}> | ||
<para>mango</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
], | ||
])('toggleBulletList: Case %# %s', async (name, input, expected) => { | ||
const { editorView } = await testEditor(input); | ||
sendKeyToPm(editorView, 'Ctrl-Shift-8'); | ||
expect(editorView.state.doc).toEqualDocument(expected); | ||
}); | ||
describe('Pressing Backspace', () => { | ||
@@ -1204,1 +1618,58 @@ it('Backspacing works', async () => { | ||
}); | ||
describe('smartNodesBetween', () => { | ||
test('works', async () => { | ||
const { view } = await testEditor( | ||
<doc> | ||
<ul> | ||
<li> | ||
<para>firs[]t</para> | ||
<ul> | ||
<li> | ||
<para>alpha</para> | ||
</li> | ||
</ul> | ||
</li> | ||
<li> | ||
<para>distant</para> | ||
</li> | ||
</ul> | ||
</doc>, | ||
); | ||
const state = view.state; | ||
const nodes = []; | ||
smartNodesBetween( | ||
state.selection.$from, | ||
state.selection.$to, | ||
state.doc, | ||
(node, pos) => { | ||
if ( | ||
['listItem', 'bulletList', 'orderedItem'].includes(node.type.name) | ||
) { | ||
nodes.push([node.type.name, node.textContent, pos]); | ||
} | ||
}, | ||
); | ||
expect(nodes).toMatchInlineSnapshot(` | ||
Array [ | ||
Array [ | ||
"listItem", | ||
"firstalpha", | ||
1, | ||
], | ||
Array [ | ||
"bulletList", | ||
"alpha", | ||
9, | ||
], | ||
Array [ | ||
"listItem", | ||
"distant", | ||
21, | ||
], | ||
] | ||
`); | ||
}); | ||
}); |
import { keymap } from 'prosemirror-keymap'; | ||
import { wrappingInputRule } from 'prosemirror-inputrules'; | ||
import { parentHasDirectParentOfType } from '../core-commands'; | ||
import { toggleList } from './list-item/commands'; | ||
import { chainCommands } from 'prosemirror-commands'; | ||
import { filter } from '../utils/index'; | ||
@@ -56,3 +57,3 @@ export const spec = specFactory; | ||
keymap({ | ||
[keybindings.toggle]: toggleList(type, schema.nodes.listItem), | ||
[keybindings.toggle]: toggleBulletList(), | ||
[keybindings.toggleTodo]: toggleTodoList(), | ||
@@ -70,92 +71,99 @@ }), | ||
export function toggleBulletList() { | ||
return (state, dispatch, view) => { | ||
return toggleList( | ||
state.schema.nodes.bulletList, | ||
state.schema.nodes.listItem, | ||
)(state, dispatch, view); | ||
}; | ||
} | ||
const handleTodos = filter( | ||
[isSelectionParentBulletList, (state) => todoCount(state).todos !== 0], | ||
(state, dispatch) => { | ||
const { selection } = state; | ||
let tr = state.tr; | ||
smartNodesBetween(selection.$from, selection.$to, tr.doc, (node, pos) => { | ||
if ( | ||
node.type.name === 'listItem' && | ||
typeof node.attrs.todoChecked === 'boolean' | ||
) { | ||
tr = tr.setNodeMarkup( | ||
pos, | ||
undefined, | ||
Object.assign({}, node.attrs, { todoChecked: null }), | ||
); | ||
} | ||
}); | ||
// TODO: implemeted two different ways to toggle | ||
// todos, none of them convert bulletList -> todoList directly | ||
export function toggleTodoList2() { | ||
return (state, dispatch, view) => { | ||
const result = toggleList( | ||
state.schema.nodes.bulletList, | ||
state.schema.nodes.listItem, | ||
true, | ||
)(state, dispatch, view); | ||
return result; | ||
}; | ||
} | ||
if (dispatch) { | ||
dispatch(tr); | ||
} | ||
export function toggleTodoList() { | ||
return (state, dispatch, view) => { | ||
const result = toggleBulletList()(state, dispatch, view); | ||
return true; | ||
}, | ||
); | ||
if (!result) { | ||
return false; | ||
} | ||
const handleBulletLists = (state, dispatch, view) => | ||
toggleList(state.schema.nodes.bulletList, state.schema.nodes.listItem)( | ||
state, | ||
dispatch, | ||
view, | ||
); | ||
state = view.state; | ||
const { selection } = state; | ||
return chainCommands(handleTodos, handleBulletLists); | ||
} | ||
const fromNode = selection.$from.node(-2); | ||
const endNode = selection.$to.node(-2); | ||
const isSelectionParentBulletList = (state) => { | ||
const { selection } = state; | ||
const fromNode = selection.$from.node(-2); | ||
const endNode = selection.$to.node(-2); | ||
// make sure the current selection is now bulletList | ||
if ( | ||
!fromNode || | ||
fromNode.type.name !== state.schema.nodes.bulletList.name || | ||
!endNode || | ||
endNode.type.name !== state.schema.nodes.bulletList.name | ||
) { | ||
// returning true as toggling was still successful | ||
return true; | ||
} | ||
return ( | ||
fromNode && | ||
fromNode.type === state.schema.nodes.bulletList && | ||
endNode && | ||
endNode.type === state.schema.nodes.bulletList | ||
); | ||
}; | ||
// at this point we have bulletList and we want to set every direct child | ||
// to todoChecked | ||
export function toggleTodoList() { | ||
// The job of the command below is to see if the selection | ||
// has some todo list-items, if yes, convert all list-items | ||
// to todo. | ||
const handleTodos = filter( | ||
[ | ||
isSelectionParentBulletList, | ||
(state) => { | ||
const { todos, lists } = todoCount(state); | ||
// If all the list items are todo or none of them are todo | ||
// return false so we can run the vanilla toggleList | ||
return todos !== lists; | ||
}, | ||
], | ||
(state, dispatch) => { | ||
let { tr, selection } = state; | ||
// NOTE: On start end depths start point to the start of listItems and end points to end of listItems | ||
// in the current selections parent bulletList. | ||
// example: even though selection is between A & B, | ||
// the start and end will be * and ~. | ||
// <ul> | ||
// *<li>[A</li> | ||
// <li><B]></li> | ||
// <li><C></li>~ | ||
// </ul> | ||
const start = selection.$from.start(-2); | ||
const end = selection.$to.end(-2); | ||
smartNodesBetween( | ||
selection.$from, | ||
selection.$to, | ||
state.tr.doc, | ||
(node, pos) => { | ||
if (node.type.name === 'listItem' && node.attrs.todoChecked == null) { | ||
tr = tr.setNodeMarkup( | ||
pos, | ||
undefined, | ||
Object.assign({}, node.attrs, { todoChecked: false }), | ||
); | ||
} | ||
}, | ||
); | ||
const parent = selection.$from.node(-2); | ||
let tr = state.tr; | ||
tr.doc.nodesBetween(start, end, (node, pos) => { | ||
// since we only need to iterate through | ||
// the direct children of bulletList | ||
if (parent === node) { | ||
return true; | ||
if (dispatch) { | ||
dispatch(tr); | ||
} | ||
if (node.type.name === 'listItem') { | ||
if (node.attrs.todoChecked == null) { | ||
tr = tr.setNodeMarkup( | ||
pos, | ||
undefined, | ||
Object.assign({}, node.attrs, { todoChecked: false }), | ||
); | ||
} | ||
} | ||
// don't dig deeper for any other node | ||
return false; | ||
}); | ||
return true; | ||
}, | ||
); | ||
if (dispatch) { | ||
dispatch(tr); | ||
} | ||
const fallback = (state, dispatch, view) => | ||
toggleList( | ||
state.schema.nodes.bulletList, | ||
state.schema.nodes.listItem, | ||
true, | ||
)(state, dispatch, view); | ||
return true; | ||
}; | ||
return chainCommands(handleTodos, fallback); | ||
} | ||
@@ -184,1 +192,66 @@ | ||
} | ||
/** | ||
* given a bullet/ordered list it will call callback for each node | ||
* which | ||
* - strictly lies inside the range provided | ||
* - nodes that are sibblings of the top level nodes | ||
* which lie in the range. | ||
* | ||
* Example | ||
* <ul> | ||
* <li>[A | ||
* <list-A's kids/> | ||
* </li> | ||
* <li><B]></li> | ||
* <li><C></li> | ||
* <li>D <list-D's kids </li> | ||
* </ul> | ||
* | ||
* In the above the callback will be called for everyone | ||
* A, list-A's kids, B, C, D _but_ not list-D's kids. | ||
*/ | ||
export function smartNodesBetween($from, $to, doc, callback) { | ||
const start = $from.start(-2); | ||
const end = $to.end(-2); | ||
const depth = Math.min(doc.resolve(start).depth, doc.resolve(end).depth); | ||
doc.nodesBetween(start, end, (node, pos) => { | ||
if (pos >= start) { | ||
callback(node, pos); | ||
} | ||
// prevent return false for nodes higher in depth | ||
// as we want to recurse in their kids. | ||
if (doc.resolve(pos).depth <= depth) { | ||
return; | ||
} | ||
// do not dig deeper into children of nodes outside of selection | ||
if (pos < $from.pos) { | ||
return false; | ||
} | ||
if (pos > $to.pos) { | ||
return false; | ||
} | ||
}); | ||
} | ||
function todoCount(state) { | ||
let lists = 0; | ||
let todos = 0; | ||
const { selection } = state; | ||
smartNodesBetween(selection.$from, selection.$to, state.doc, (node, pos) => { | ||
if (node.type.name === 'listItem') { | ||
lists++; | ||
if (typeof node.attrs.todoChecked === 'boolean') { | ||
todos++; | ||
} | ||
} | ||
}); | ||
return { | ||
lists: lists, | ||
todos: todos, | ||
}; | ||
} |
@@ -170,4 +170,4 @@ import { liftTarget, ReplaceAroundStep } from 'prosemirror-transform'; | ||
* @param {Object} itemType 'listItem' | ||
* @param {boolean} todo if toggling into a bulletList switch on todoChecked attr for | ||
* each listItem. | ||
* @param {boolean} todo if true and the final result of toggle is a bulletList | ||
* set `todoChecked` attr for each listItem. | ||
*/ | ||
@@ -174,0 +174,0 @@ export function toggleList(listType, itemType, todo) { |
{ | ||
"name": "@bangle.dev/core", | ||
"version": "0.3.0", | ||
"version": "0.3.1", | ||
"homepage": "https://bangle.dev", | ||
@@ -5,0 +5,0 @@ "authors": [ |
@@ -348,3 +348,3 @@ import { DOMSerializer } from 'prosemirror-model'; | ||
export function fNot(func) { | ||
export function complement(func) { | ||
return (...args) => !func(...args); | ||
@@ -351,0 +351,0 @@ } |
Sorry, the diff of this file is too big to display
505165
18375