This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
"libs": ["browser"],
"plugins": {
"node": {},
"complete_strings": {},
"es_modules": {}
}
}

View File

@@ -0,0 +1,100 @@
# How to contribute
- [Getting help](#getting-help)
- [Submitting bug reports](#submitting-bug-reports)
- [Contributing code](#contributing-code)
## Getting help
Community discussion, questions, and informal bug reporting is done on the
[discuss.ProseMirror forum](http://discuss.prosemirror.net).
## Submitting bug reports
Report bugs on the
[GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues).
Before reporting a bug, please read these pointers.
- The issue tracker is for *bugs*, not requests for help. Questions
should be asked on the [forum](http://discuss.prosemirror.net).
- Include information about the version of the code that exhibits the
problem. For browser-related issues, include the browser and browser
version on which the problem occurred.
- Mention very precisely what went wrong. "X is broken" is not a good
bug report. What did you expect to happen? What happened instead?
Describe the exact steps a maintainer has to take to make the
problem occur. A screencast can be useful, but is no substitute for
a textual description.
- A great way to make it easy to reproduce your problem, if it can not
be trivially reproduced on the website demos, is to submit a script
that triggers the issue.
## Contributing code
- Make sure you have a [GitHub Account](https://github.com/signup/free)
- Fork the relevant repository
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Create a local checkout of the code. You can use the
[main repository](https://github.com/prosemirror/prosemirror) to
easily check out all core modules.
- Make your changes, and commit them
- Follow the code style of the rest of the project (see below). Run
`npm run lint` (in the main repository checkout) to make sure that
the linter is happy.
- If your changes are easy to test or likely to regress, add tests in
the relevant `test/` directory. Either put them in an existing
`test-*.js` file, if they fit there, or add a new file.
- Make sure all tests pass. Run `npm run test` to verify tests pass
(you will need Node.js v6+).
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to ProseMirror you
- Agree to license the contributed code under the project's [MIT
license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE).
- Confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Coding standards
- ES6 syntax, targeting an ES5 runtime (i.e. don't use library
elements added by ES6, don't use ES7/ES.next syntax).
- 2 spaces per indentation level, no tabs.
- No semicolons except when necessary.
- Follow the surrounding code when it comes to spacing, brace
placement, etc.
- Brace-less single-statement bodies are encouraged (whenever they
don't impact readability).
- [getdocs](https://github.com/marijnh/getdocs)-style doc comments
above items that are part of the public API.
- When documenting non-public items, you can put the type after a
single colon, so that getdocs doesn't pick it up and add it to the
API reference.
- The linter (`npm run lint`) complains about unused variables and
functions. Prefix their names with an underscore to muffle it.
- ProseMirror does *not* follow JSHint or JSLint prescribed style.
Patches that try to 'fix' code to pass one of these linters will not
be accepted.

View File

@@ -0,0 +1,19 @@
Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,287 @@
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
var prosemirrorTransform = require('prosemirror-transform');
var prosemirrorModel = require('prosemirror-model');
var prosemirrorState = require('prosemirror-state');
var olDOM = ["ol", 0],
ulDOM = ["ul", 0],
liDOM = ["li", 0];
var orderedList = {
attrs: {
order: {
"default": 1
}
},
parseDOM: [{
tag: "ol",
getAttrs: function getAttrs(dom) {
return {
order: dom.hasAttribute("start") ? +dom.getAttribute("start") : 1
};
}
}],
toDOM: function toDOM(node) {
return node.attrs.order == 1 ? olDOM : ["ol", {
start: node.attrs.order
}, 0];
}
};
var bulletList = {
parseDOM: [{
tag: "ul"
}],
toDOM: function toDOM() {
return ulDOM;
}
};
var listItem = {
parseDOM: [{
tag: "li"
}],
toDOM: function toDOM() {
return liDOM;
},
defining: true
};
function add(obj, props) {
var copy = {};
for (var prop in obj) {
copy[prop] = obj[prop];
}
for (var _prop in props) {
copy[_prop] = props[_prop];
}
return copy;
}
function addListNodes(nodes, itemContent, listGroup) {
return nodes.append({
ordered_list: add(orderedList, {
content: "list_item+",
group: listGroup
}),
bullet_list: add(bulletList, {
content: "list_item+",
group: listGroup
}),
list_item: add(listItem, {
content: itemContent
})
});
}
function wrapInList(listType) {
var attrs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
return function (state, dispatch) {
var _state$selection = state.selection,
$from = _state$selection.$from,
$to = _state$selection.$to;
var range = $from.blockRange($to),
doJoin = false,
outerRange = range;
if (!range) return false;
if (range.depth >= 2 && $from.node(range.depth - 1).type.compatibleContent(listType) && range.startIndex == 0) {
if ($from.index(range.depth - 1) == 0) return false;
var $insert = state.doc.resolve(range.start - 2);
outerRange = new prosemirrorModel.NodeRange($insert, $insert, range.depth);
if (range.endIndex < range.parent.childCount) range = new prosemirrorModel.NodeRange($from, state.doc.resolve($to.end(range.depth)), range.depth);
doJoin = true;
}
var wrap = prosemirrorTransform.findWrapping(outerRange, listType, attrs, range);
if (!wrap) return false;
if (dispatch) dispatch(doWrapInList(state.tr, range, wrap, doJoin, listType).scrollIntoView());
return true;
};
}
function doWrapInList(tr, range, wrappers, joinBefore, listType) {
var content = prosemirrorModel.Fragment.empty;
for (var i = wrappers.length - 1; i >= 0; i--) {
content = prosemirrorModel.Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content));
}
tr.step(new prosemirrorTransform.ReplaceAroundStep(range.start - (joinBefore ? 2 : 0), range.end, range.start, range.end, new prosemirrorModel.Slice(content, 0, 0), wrappers.length, true));
var found = 0;
for (var _i = 0; _i < wrappers.length; _i++) {
if (wrappers[_i].type == listType) found = _i + 1;
}
var splitDepth = wrappers.length - found;
var splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0),
parent = range.parent;
for (var _i2 = range.startIndex, e = range.endIndex, first = true; _i2 < e; _i2++, first = false) {
if (!first && prosemirrorTransform.canSplit(tr.doc, splitPos, splitDepth)) {
tr.split(splitPos, splitDepth);
splitPos += 2 * splitDepth;
}
splitPos += parent.child(_i2).nodeSize;
}
return tr;
}
function splitListItem(itemType, itemAttrs) {
return function (state, dispatch) {
var _state$selection2 = state.selection,
$from = _state$selection2.$from,
$to = _state$selection2.$to,
node = _state$selection2.node;
if (node && node.isBlock || $from.depth < 2 || !$from.sameParent($to)) return false;
var grandParent = $from.node(-1);
if (grandParent.type != itemType) return false;
if ($from.parent.content.size == 0 && $from.node(-1).childCount == $from.indexAfter(-1)) {
if ($from.depth == 3 || $from.node(-3).type != itemType || $from.index(-2) != $from.node(-2).childCount - 1) return false;
if (dispatch) {
var wrap = prosemirrorModel.Fragment.empty;
var depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3;
for (var d = $from.depth - depthBefore; d >= $from.depth - 3; d--) {
wrap = prosemirrorModel.Fragment.from($from.node(d).copy(wrap));
}
var depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1 : $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3;
wrap = wrap.append(prosemirrorModel.Fragment.from(itemType.createAndFill()));
var start = $from.before($from.depth - (depthBefore - 1));
var _tr = state.tr.replace(start, $from.after(-depthAfter), new prosemirrorModel.Slice(wrap, 4 - depthBefore, 0));
var sel = -1;
_tr.doc.nodesBetween(start, _tr.doc.content.size, function (node, pos) {
if (sel > -1) return false;
if (node.isTextblock && node.content.size == 0) sel = pos + 1;
});
if (sel > -1) _tr.setSelection(prosemirrorState.Selection.near(_tr.doc.resolve(sel)));
dispatch(_tr.scrollIntoView());
}
return true;
}
var nextType = $to.pos == $from.end() ? grandParent.contentMatchAt(0).defaultType : null;
var tr = state.tr["delete"]($from.pos, $to.pos);
var types = nextType ? [itemAttrs ? {
type: itemType,
attrs: itemAttrs
} : null, {
type: nextType
}] : undefined;
if (!prosemirrorTransform.canSplit(tr.doc, $from.pos, 2, types)) return false;
if (dispatch) dispatch(tr.split($from.pos, 2, types).scrollIntoView());
return true;
};
}
function liftListItem(itemType) {
return function (state, dispatch) {
var _state$selection3 = state.selection,
$from = _state$selection3.$from,
$to = _state$selection3.$to;
var range = $from.blockRange($to, function (node) {
return node.childCount > 0 && node.firstChild.type == itemType;
});
if (!range) return false;
if (!dispatch) return true;
if ($from.node(range.depth - 1).type == itemType) return liftToOuterList(state, dispatch, itemType, range);else return liftOutOfList(state, dispatch, range);
};
}
function liftToOuterList(state, dispatch, itemType, range) {
var tr = state.tr,
end = range.end,
endOfList = range.$to.end(range.depth);
if (end < endOfList) {
tr.step(new prosemirrorTransform.ReplaceAroundStep(end - 1, endOfList, end, endOfList, new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
range = new prosemirrorModel.NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
}
var target = prosemirrorTransform.liftTarget(range);
if (target == null) return false;
tr.lift(range, target);
var after = tr.mapping.map(end, -1) - 1;
if (prosemirrorTransform.canJoin(tr.doc, after)) tr.join(after);
dispatch(tr.scrollIntoView());
return true;
}
function liftOutOfList(state, dispatch, range) {
var tr = state.tr,
list = range.parent;
for (var pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
pos -= list.child(i).nodeSize;
tr["delete"](pos - 1, pos + 1);
}
var $start = tr.doc.resolve(range.start),
item = $start.nodeAfter;
if (tr.mapping.map(range.end) != range.start + $start.nodeAfter.nodeSize) return false;
var atStart = range.startIndex == 0,
atEnd = range.endIndex == list.childCount;
var parent = $start.node(-1),
indexBefore = $start.index(-1);
if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1, item.content.append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list)))) return false;
var start = $start.pos,
end = start + item.nodeSize;
tr.step(new prosemirrorTransform.ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1, new prosemirrorModel.Slice((atStart ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))).append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))), atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
dispatch(tr.scrollIntoView());
return true;
}
function sinkListItem(itemType) {
return function (state, dispatch) {
var _state$selection4 = state.selection,
$from = _state$selection4.$from,
$to = _state$selection4.$to;
var range = $from.blockRange($to, function (node) {
return node.childCount > 0 && node.firstChild.type == itemType;
});
if (!range) return false;
var startIndex = range.startIndex;
if (startIndex == 0) return false;
var parent = range.parent,
nodeBefore = parent.child(startIndex - 1);
if (nodeBefore.type != itemType) return false;
if (dispatch) {
var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
var slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create(null, inner)))), nestedBefore ? 3 : 1, 0);
var before = range.start,
after = range.end;
dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, before, after, slice, 1, true)).scrollIntoView());
}
return true;
};
}
exports.addListNodes = addListNodes;
exports.bulletList = bulletList;
exports.liftListItem = liftListItem;
exports.listItem = listItem;
exports.orderedList = orderedList;
exports.sinkListItem = sinkListItem;
exports.splitListItem = splitListItem;
exports.wrapInList = wrapInList;

View File

@@ -0,0 +1,58 @@
import { NodeSpec, NodeType, Attrs } from 'prosemirror-model';
import OrderedMap from 'orderedmap';
import { Command } from 'prosemirror-state';
/**
An ordered list [node spec](https://prosemirror.net/docs/ref/#model.NodeSpec). Has a single
attribute, `order`, which determines the number at which the list
starts counting, and defaults to 1. Represented as an `<ol>`
element.
*/
declare const orderedList: NodeSpec;
/**
A bullet list node spec, represented in the DOM as `<ul>`.
*/
declare const bulletList: NodeSpec;
/**
A list item (`<li>`) spec.
*/
declare const listItem: NodeSpec;
/**
Convenience function for adding list-related node types to a map
specifying the nodes for a schema. Adds
[`orderedList`](https://prosemirror.net/docs/ref/#schema-list.orderedList) as `"ordered_list"`,
[`bulletList`](https://prosemirror.net/docs/ref/#schema-list.bulletList) as `"bullet_list"`, and
[`listItem`](https://prosemirror.net/docs/ref/#schema-list.listItem) as `"list_item"`.
`itemContent` determines the content expression for the list items.
If you want the commands defined in this module to apply to your
list structure, it should have a shape like `"paragraph block*"` or
`"paragraph (ordered_list | bullet_list)*"`. `listGroup` can be
given to assign a group name to the list node types, for example
`"block"`.
*/
declare function addListNodes(nodes: OrderedMap<NodeSpec>, itemContent: string, listGroup?: string): OrderedMap<NodeSpec>;
/**
Returns a command function that wraps the selection in a list with
the given type an attributes. If `dispatch` is null, only return a
value to indicate whether this is possible, but don't actually
perform the change.
*/
declare function wrapInList(listType: NodeType, attrs?: Attrs | null): Command;
/**
Build a command that splits a non-empty textblock at the top level
of a list item by also splitting that list item.
*/
declare function splitListItem(itemType: NodeType, itemAttrs?: Attrs): Command;
/**
Create a command to lift the list item around the selection up into
a wrapping list.
*/
declare function liftListItem(itemType: NodeType): Command;
/**
Create a command to sink the list item around the selection down
into an inner list.
*/
declare function sinkListItem(itemType: NodeType): Command;
export { addListNodes, bulletList, liftListItem, listItem, orderedList, sinkListItem, splitListItem, wrapInList };

View File

@@ -0,0 +1,258 @@
import { findWrapping, ReplaceAroundStep, canSplit, liftTarget, canJoin } from 'prosemirror-transform';
import { NodeRange, Fragment, Slice } from 'prosemirror-model';
import { Selection } from 'prosemirror-state';
const olDOM = ["ol", 0], ulDOM = ["ul", 0], liDOM = ["li", 0];
/**
An ordered list [node spec](https://prosemirror.net/docs/ref/#model.NodeSpec). Has a single
attribute, `order`, which determines the number at which the list
starts counting, and defaults to 1. Represented as an `<ol>`
element.
*/
const orderedList = {
attrs: { order: { default: 1 } },
parseDOM: [{ tag: "ol", getAttrs(dom) {
return { order: dom.hasAttribute("start") ? +dom.getAttribute("start") : 1 };
} }],
toDOM(node) {
return node.attrs.order == 1 ? olDOM : ["ol", { start: node.attrs.order }, 0];
}
};
/**
A bullet list node spec, represented in the DOM as `<ul>`.
*/
const bulletList = {
parseDOM: [{ tag: "ul" }],
toDOM() { return ulDOM; }
};
/**
A list item (`<li>`) spec.
*/
const listItem = {
parseDOM: [{ tag: "li" }],
toDOM() { return liDOM; },
defining: true
};
function add(obj, props) {
let copy = {};
for (let prop in obj)
copy[prop] = obj[prop];
for (let prop in props)
copy[prop] = props[prop];
return copy;
}
/**
Convenience function for adding list-related node types to a map
specifying the nodes for a schema. Adds
[`orderedList`](https://prosemirror.net/docs/ref/#schema-list.orderedList) as `"ordered_list"`,
[`bulletList`](https://prosemirror.net/docs/ref/#schema-list.bulletList) as `"bullet_list"`, and
[`listItem`](https://prosemirror.net/docs/ref/#schema-list.listItem) as `"list_item"`.
`itemContent` determines the content expression for the list items.
If you want the commands defined in this module to apply to your
list structure, it should have a shape like `"paragraph block*"` or
`"paragraph (ordered_list | bullet_list)*"`. `listGroup` can be
given to assign a group name to the list node types, for example
`"block"`.
*/
function addListNodes(nodes, itemContent, listGroup) {
return nodes.append({
ordered_list: add(orderedList, { content: "list_item+", group: listGroup }),
bullet_list: add(bulletList, { content: "list_item+", group: listGroup }),
list_item: add(listItem, { content: itemContent })
});
}
/**
Returns a command function that wraps the selection in a list with
the given type an attributes. If `dispatch` is null, only return a
value to indicate whether this is possible, but don't actually
perform the change.
*/
function wrapInList(listType, attrs = null) {
return function (state, dispatch) {
let { $from, $to } = state.selection;
let range = $from.blockRange($to), doJoin = false, outerRange = range;
if (!range)
return false;
// This is at the top of an existing list item
if (range.depth >= 2 && $from.node(range.depth - 1).type.compatibleContent(listType) && range.startIndex == 0) {
// Don't do anything if this is the top of the list
if ($from.index(range.depth - 1) == 0)
return false;
let $insert = state.doc.resolve(range.start - 2);
outerRange = new NodeRange($insert, $insert, range.depth);
if (range.endIndex < range.parent.childCount)
range = new NodeRange($from, state.doc.resolve($to.end(range.depth)), range.depth);
doJoin = true;
}
let wrap = findWrapping(outerRange, listType, attrs, range);
if (!wrap)
return false;
if (dispatch)
dispatch(doWrapInList(state.tr, range, wrap, doJoin, listType).scrollIntoView());
return true;
};
}
function doWrapInList(tr, range, wrappers, joinBefore, listType) {
let content = Fragment.empty;
for (let i = wrappers.length - 1; i >= 0; i--)
content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content));
tr.step(new ReplaceAroundStep(range.start - (joinBefore ? 2 : 0), range.end, range.start, range.end, new Slice(content, 0, 0), wrappers.length, true));
let found = 0;
for (let i = 0; i < wrappers.length; i++)
if (wrappers[i].type == listType)
found = i + 1;
let splitDepth = wrappers.length - found;
let splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0), parent = range.parent;
for (let i = range.startIndex, e = range.endIndex, first = true; i < e; i++, first = false) {
if (!first && canSplit(tr.doc, splitPos, splitDepth)) {
tr.split(splitPos, splitDepth);
splitPos += 2 * splitDepth;
}
splitPos += parent.child(i).nodeSize;
}
return tr;
}
/**
Build a command that splits a non-empty textblock at the top level
of a list item by also splitting that list item.
*/
function splitListItem(itemType, itemAttrs) {
return function (state, dispatch) {
let { $from, $to, node } = state.selection;
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to))
return false;
let grandParent = $from.node(-1);
if (grandParent.type != itemType)
return false;
if ($from.parent.content.size == 0 && $from.node(-1).childCount == $from.indexAfter(-1)) {
// In an empty block. If this is a nested list, the wrapping
// list item should be split. Otherwise, bail out and let next
// command handle lifting.
if ($from.depth == 3 || $from.node(-3).type != itemType ||
$from.index(-2) != $from.node(-2).childCount - 1)
return false;
if (dispatch) {
let wrap = Fragment.empty;
let depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3;
// Build a fragment containing empty versions of the structure
// from the outer list item to the parent node of the cursor
for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d--)
wrap = Fragment.from($from.node(d).copy(wrap));
let depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1
: $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3;
// Add a second list item with an empty default start node
wrap = wrap.append(Fragment.from(itemType.createAndFill()));
let start = $from.before($from.depth - (depthBefore - 1));
let tr = state.tr.replace(start, $from.after(-depthAfter), new Slice(wrap, 4 - depthBefore, 0));
let sel = -1;
tr.doc.nodesBetween(start, tr.doc.content.size, (node, pos) => {
if (sel > -1)
return false;
if (node.isTextblock && node.content.size == 0)
sel = pos + 1;
});
if (sel > -1)
tr.setSelection(Selection.near(tr.doc.resolve(sel)));
dispatch(tr.scrollIntoView());
}
return true;
}
let nextType = $to.pos == $from.end() ? grandParent.contentMatchAt(0).defaultType : null;
let tr = state.tr.delete($from.pos, $to.pos);
let types = nextType ? [itemAttrs ? { type: itemType, attrs: itemAttrs } : null, { type: nextType }] : undefined;
if (!canSplit(tr.doc, $from.pos, 2, types))
return false;
if (dispatch)
dispatch(tr.split($from.pos, 2, types).scrollIntoView());
return true;
};
}
/**
Create a command to lift the list item around the selection up into
a wrapping list.
*/
function liftListItem(itemType) {
return function (state, dispatch) {
let { $from, $to } = state.selection;
let range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild.type == itemType);
if (!range)
return false;
if (!dispatch)
return true;
if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
return liftToOuterList(state, dispatch, itemType, range);
else // Outer list node
return liftOutOfList(state, dispatch, range);
};
}
function liftToOuterList(state, dispatch, itemType, range) {
let tr = state.tr, end = range.end, endOfList = range.$to.end(range.depth);
if (end < endOfList) {
// There are siblings after the lifted items, which must become
// children of the last item
tr.step(new ReplaceAroundStep(end - 1, endOfList, end, endOfList, new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
}
const target = liftTarget(range);
if (target == null)
return false;
tr.lift(range, target);
let after = tr.mapping.map(end, -1) - 1;
if (canJoin(tr.doc, after))
tr.join(after);
dispatch(tr.scrollIntoView());
return true;
}
function liftOutOfList(state, dispatch, range) {
let tr = state.tr, list = range.parent;
// Merge the list items into a single big item
for (let pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
pos -= list.child(i).nodeSize;
tr.delete(pos - 1, pos + 1);
}
let $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
if (tr.mapping.map(range.end) != range.start + $start.nodeAfter.nodeSize)
return false;
let atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
let parent = $start.node(-1), indexBefore = $start.index(-1);
if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1, item.content.append(atEnd ? Fragment.empty : Fragment.from(list))))
return false;
let start = $start.pos, end = start + item.nodeSize;
// Strip off the surrounding list. At the sides where we're not at
// the end of the list, the existing list is closed. At sides where
// this is the end, it is overwritten to its end.
tr.step(new ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1, new Slice((atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)))
.append(atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))), atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
dispatch(tr.scrollIntoView());
return true;
}
/**
Create a command to sink the list item around the selection down
into an inner list.
*/
function sinkListItem(itemType) {
return function (state, dispatch) {
let { $from, $to } = state.selection;
let range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild.type == itemType);
if (!range)
return false;
let startIndex = range.startIndex;
if (startIndex == 0)
return false;
let parent = range.parent, nodeBefore = parent.child(startIndex - 1);
if (nodeBefore.type != itemType)
return false;
if (dispatch) {
let nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
let inner = Fragment.from(nestedBefore ? itemType.create() : null);
let slice = new Slice(Fragment.from(itemType.create(null, Fragment.from(parent.type.create(null, inner)))), nestedBefore ? 3 : 1, 0);
let before = range.start, after = range.end;
dispatch(state.tr.step(new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, before, after, slice, 1, true))
.scrollIntoView());
}
return true;
};
}
export { addListNodes, bulletList, liftListItem, listItem, orderedList, sinkListItem, splitListItem, wrapInList };

View File

@@ -0,0 +1,36 @@
{
"name": "prosemirror-schema-list",
"version": "1.3.0",
"description": "List-related schema elements and commands for ProseMirror",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"sideEffects": false,
"license": "MIT",
"maintainers": [
{
"name": "Marijn Haverbeke",
"email": "marijn@haverbeke.berlin",
"web": "http://marijnhaverbeke.nl"
}
],
"repository": {
"type": "git",
"url": "git://github.com/prosemirror/prosemirror-schema-list.git"
},
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.7.3",
"prosemirror-state": "^1.0.0"
},
"devDependencies": {
"prosemirror-state": "^1.0.0",
"@prosemirror/buildhelper": "^0.1.5",
"prosemirror-test-builder": "^1.0.0"
}
}

View File

@@ -0,0 +1,27 @@
This module exports list-related schema elements and commands. The
commands assume lists to be nestable, with the restriction that the
first child of a list item is a plain paragraph.
These are the node specs:
@orderedList
@bulletList
@listItem
@addListNodes
Using this would look something like this:
```javascript
const mySchema = new Schema({
nodes: addListNodes(baseSchema.spec.nodes, "paragraph block*", "block"),
marks: baseSchema.spec.marks
})
```
The following functions are [commands](/docs/guide/#commands):
@wrapInList
@splitListItem
@liftListItem
@sinkListItem

View File

@@ -0,0 +1,241 @@
import {findWrapping, liftTarget, canSplit, ReplaceAroundStep, canJoin} from "prosemirror-transform"
import {Slice, Fragment, NodeSpec, DOMOutputSpec, NodeType, Attrs, NodeRange} from "prosemirror-model"
import OrderedMap from "orderedmap"
import {Command, EditorState, Transaction, NodeSelection, Selection} from "prosemirror-state"
const olDOM: DOMOutputSpec = ["ol", 0], ulDOM: DOMOutputSpec = ["ul", 0], liDOM: DOMOutputSpec = ["li", 0]
/// An ordered list [node spec](#model.NodeSpec). Has a single
/// attribute, `order`, which determines the number at which the list
/// starts counting, and defaults to 1. Represented as an `<ol>`
/// element.
export const orderedList = {
attrs: {order: {default: 1}},
parseDOM: [{tag: "ol", getAttrs(dom: HTMLElement) {
return {order: dom.hasAttribute("start") ? +dom.getAttribute("start")! : 1}
}}],
toDOM(node) {
return node.attrs.order == 1 ? olDOM : ["ol", {start: node.attrs.order}, 0]
}
} as NodeSpec
/// A bullet list node spec, represented in the DOM as `<ul>`.
export const bulletList: NodeSpec = {
parseDOM: [{tag: "ul"}],
toDOM() { return ulDOM }
}
/// A list item (`<li>`) spec.
export const listItem: NodeSpec = {
parseDOM: [{tag: "li"}],
toDOM() { return liDOM },
defining: true
}
function add(obj: {[prop: string]: any}, props: {[prop: string]: any}) {
let copy: {[prop: string]: any} = {}
for (let prop in obj) copy[prop] = obj[prop]
for (let prop in props) copy[prop] = props[prop]
return copy
}
/// Convenience function for adding list-related node types to a map
/// specifying the nodes for a schema. Adds
/// [`orderedList`](#schema-list.orderedList) as `"ordered_list"`,
/// [`bulletList`](#schema-list.bulletList) as `"bullet_list"`, and
/// [`listItem`](#schema-list.listItem) as `"list_item"`.
///
/// `itemContent` determines the content expression for the list items.
/// If you want the commands defined in this module to apply to your
/// list structure, it should have a shape like `"paragraph block*"` or
/// `"paragraph (ordered_list | bullet_list)*"`. `listGroup` can be
/// given to assign a group name to the list node types, for example
/// `"block"`.
export function addListNodes(nodes: OrderedMap<NodeSpec>, itemContent: string, listGroup?: string): OrderedMap<NodeSpec> {
return nodes.append({
ordered_list: add(orderedList, {content: "list_item+", group: listGroup}),
bullet_list: add(bulletList, {content: "list_item+", group: listGroup}),
list_item: add(listItem, {content: itemContent})
})
}
/// Returns a command function that wraps the selection in a list with
/// the given type an attributes. If `dispatch` is null, only return a
/// value to indicate whether this is possible, but don't actually
/// perform the change.
export function wrapInList(listType: NodeType, attrs: Attrs | null = null): Command {
return function(state: EditorState, dispatch?: (tr: Transaction) => void) {
let {$from, $to} = state.selection
let range = $from.blockRange($to), doJoin = false, outerRange = range
if (!range) return false
// This is at the top of an existing list item
if (range.depth >= 2 && $from.node(range.depth - 1).type.compatibleContent(listType) && range.startIndex == 0) {
// Don't do anything if this is the top of the list
if ($from.index(range.depth - 1) == 0) return false
let $insert = state.doc.resolve(range.start - 2)
outerRange = new NodeRange($insert, $insert, range.depth)
if (range.endIndex < range.parent.childCount)
range = new NodeRange($from, state.doc.resolve($to.end(range.depth)), range.depth)
doJoin = true
}
let wrap = findWrapping(outerRange!, listType, attrs, range)
if (!wrap) return false
if (dispatch) dispatch(doWrapInList(state.tr, range, wrap, doJoin, listType).scrollIntoView())
return true
}
}
function doWrapInList(tr: Transaction, range: NodeRange, wrappers: {type: NodeType, attrs?: Attrs | null}[],
joinBefore: boolean, listType: NodeType) {
let content = Fragment.empty
for (let i = wrappers.length - 1; i >= 0; i--)
content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content))
tr.step(new ReplaceAroundStep(range.start - (joinBefore ? 2 : 0), range.end, range.start, range.end,
new Slice(content, 0, 0), wrappers.length, true))
let found = 0
for (let i = 0; i < wrappers.length; i++) if (wrappers[i].type == listType) found = i + 1
let splitDepth = wrappers.length - found
let splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0), parent = range.parent
for (let i = range.startIndex, e = range.endIndex, first = true; i < e; i++, first = false) {
if (!first && canSplit(tr.doc, splitPos, splitDepth)) {
tr.split(splitPos, splitDepth)
splitPos += 2 * splitDepth
}
splitPos += parent.child(i).nodeSize
}
return tr
}
/// Build a command that splits a non-empty textblock at the top level
/// of a list item by also splitting that list item.
export function splitListItem(itemType: NodeType, itemAttrs?: Attrs): Command {
return function(state: EditorState, dispatch?: (tr: Transaction) => void) {
let {$from, $to, node} = state.selection as NodeSelection
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false
let grandParent = $from.node(-1)
if (grandParent.type != itemType) return false
if ($from.parent.content.size == 0 && $from.node(-1).childCount == $from.indexAfter(-1)) {
// In an empty block. If this is a nested list, the wrapping
// list item should be split. Otherwise, bail out and let next
// command handle lifting.
if ($from.depth == 3 || $from.node(-3).type != itemType ||
$from.index(-2) != $from.node(-2).childCount - 1) return false
if (dispatch) {
let wrap = Fragment.empty
let depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3
// Build a fragment containing empty versions of the structure
// from the outer list item to the parent node of the cursor
for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d--)
wrap = Fragment.from($from.node(d).copy(wrap))
let depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1
: $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3
// Add a second list item with an empty default start node
wrap = wrap.append(Fragment.from(itemType.createAndFill()))
let start = $from.before($from.depth - (depthBefore - 1))
let tr = state.tr.replace(start, $from.after(-depthAfter), new Slice(wrap, 4 - depthBefore, 0))
let sel = -1
tr.doc.nodesBetween(start, tr.doc.content.size, (node, pos) => {
if (sel > -1) return false
if (node.isTextblock && node.content.size == 0) sel = pos + 1
})
if (sel > -1) tr.setSelection(Selection.near(tr.doc.resolve(sel)))
dispatch(tr.scrollIntoView())
}
return true
}
let nextType = $to.pos == $from.end() ? grandParent.contentMatchAt(0).defaultType : null
let tr = state.tr.delete($from.pos, $to.pos)
let types = nextType ? [itemAttrs ? {type: itemType, attrs: itemAttrs} : null, {type: nextType}] : undefined
if (!canSplit(tr.doc, $from.pos, 2, types)) return false
if (dispatch) dispatch(tr.split($from.pos, 2, types).scrollIntoView())
return true
}
}
/// Create a command to lift the list item around the selection up into
/// a wrapping list.
export function liftListItem(itemType: NodeType): Command {
return function(state: EditorState, dispatch?: (tr: Transaction) => void) {
let {$from, $to} = state.selection
let range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild!.type == itemType)
if (!range) return false
if (!dispatch) return true
if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
return liftToOuterList(state, dispatch, itemType, range)
else // Outer list node
return liftOutOfList(state, dispatch, range)
}
}
function liftToOuterList(state: EditorState, dispatch: (tr: Transaction) => void, itemType: NodeType, range: NodeRange) {
let tr = state.tr, end = range.end, endOfList = range.$to.end(range.depth)
if (end < endOfList) {
// There are siblings after the lifted items, which must become
// children of the last item
tr.step(new ReplaceAroundStep(end - 1, endOfList, end, endOfList,
new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true))
range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth)
}
const target = liftTarget(range)
if (target == null) return false
tr.lift(range, target)
let after = tr.mapping.map(end, -1) - 1
if (canJoin(tr.doc, after)) tr.join(after)
dispatch(tr.scrollIntoView())
return true
}
function liftOutOfList(state: EditorState, dispatch: (tr: Transaction) => void, range: NodeRange) {
let tr = state.tr, list = range.parent
// Merge the list items into a single big item
for (let pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
pos -= list.child(i).nodeSize
tr.delete(pos - 1, pos + 1)
}
let $start = tr.doc.resolve(range.start), item = $start.nodeAfter!
if (tr.mapping.map(range.end) != range.start + $start.nodeAfter!.nodeSize) return false
let atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount
let parent = $start.node(-1), indexBefore = $start.index(-1)
if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
item.content.append(atEnd ? Fragment.empty : Fragment.from(list))))
return false
let start = $start.pos, end = start + item.nodeSize
// Strip off the surrounding list. At the sides where we're not at
// the end of the list, the existing list is closed. At sides where
// this is the end, it is overwritten to its end.
tr.step(new ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
new Slice((atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)))
.append(atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))),
atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1))
dispatch(tr.scrollIntoView())
return true
}
/// Create a command to sink the list item around the selection down
/// into an inner list.
export function sinkListItem(itemType: NodeType): Command {
return function(state, dispatch) {
let {$from, $to} = state.selection
let range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild!.type == itemType)
if (!range) return false
let startIndex = range.startIndex
if (startIndex == 0) return false
let parent = range.parent, nodeBefore = parent.child(startIndex - 1)
if (nodeBefore.type != itemType) return false
if (dispatch) {
let nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type
let inner = Fragment.from(nestedBefore ? itemType.create() : null)
let slice = new Slice(Fragment.from(itemType.create(null, Fragment.from(parent.type.create(null, inner)))),
nestedBefore ? 3 : 1, 0)
let before = range.start, after = range.end
dispatch(state.tr.step(new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
before, after, slice, 1, true))
.scrollIntoView())
}
return true
}
}