@atlaskit/editor-plugin-list
Version:
List plugin for @atlaskit/editor-core
288 lines (286 loc) • 10.5 kB
JavaScript
import { OUTDENT_SCENARIOS } from '@atlaskit/editor-common/analytics';
import { JoinDirection, joinSiblingLists, normalizeListItemsSelection
// processNestedTaskListsInSameLevel,
} from '@atlaskit/editor-common/lists';
import { GapCursorSelection } from '@atlaskit/editor-common/selection';
import { getOrderFromOrderedListNode, isListItemNode, isListNode } from '@atlaskit/editor-common/utils';
import { Fragment, NodeRange, Slice } from '@atlaskit/editor-prosemirror/model';
import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { liftTarget, ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
import { getRestartListsAttributes, storeRestartListsAttributes } from '../utils/analytics';
import { findFirstParentListItemNode, findRootParentListNode } from '../utils/find';
import { createListNodeRange } from '../utils/selection';
export const outdentListItemsSelected = tr => {
const originalSelection = tr.selection;
const normalizedSelection = normalizeListItemsSelection({
selection: tr.selection,
doc: tr.doc
});
const rootList = findRootParentListNode(normalizedSelection.$from);
if (!rootList) {
return;
}
const commonList = normalizedSelection.$from.blockRange(rootList, isListNode);
if (!commonList) {
return;
}
let hasNormalizedToPositionLiftedOut = false;
let hasNormalizedFromPositionLiftedOut = false;
const {
from: oldFrom,
to: oldTo
} = normalizedSelection;
const nodeRanges = splitRangeSelection(normalizedSelection);
nodeRanges.forEach(range => {
const $from = tr.doc.resolve(tr.mapping.map(range.from));
const $to = tr.doc.resolve(tr.mapping.map(range.to));
const mappedRange = $from.blockRange($to, isListNode);
if (!mappedRange) {
return;
}
if (isListItemNode($from.node(mappedRange.depth - 1))) {
outdentRangeToParentList({
tr,
range: mappedRange
});
} else {
extractListItemsRangeFromList({
tr,
range: mappedRange
});
hasNormalizedToPositionLiftedOut = hasNormalizedToPositionLiftedOut || oldTo >= range.from && oldTo < range.to;
hasNormalizedFromPositionLiftedOut = hasNormalizedFromPositionLiftedOut || oldFrom >= range.from && oldFrom < range.to;
}
});
const hasCommonListMoved = commonList.start !== tr.mapping.map(commonList.start);
const nextSelection = calculateNewSelection({
originalSelection,
normalizedSelection,
tr,
hasCommonListMoved,
hasNormalizedToPositionLiftedOut,
hasNormalizedFromPositionLiftedOut
});
tr.setSelection(nextSelection);
// processNestedTaskListsInSameLevel(tr);
joinSiblingLists({
tr,
direction: JoinDirection.RIGHT
});
};
const calculateNewSelection = ({
tr,
originalSelection,
normalizedSelection,
hasCommonListMoved,
hasNormalizedToPositionLiftedOut,
hasNormalizedFromPositionLiftedOut
}) => {
const {
$from,
$to
} = normalizedSelection;
const isCursorSelection = normalizedSelection.empty;
let from = tr.mapping.map($from.pos);
let to = tr.mapping.map($to.pos);
const LIST_STRUCTURE_CHANGED_OFFSET = 2;
const isToFromSameListItem = $from.sameParent($to);
if (hasNormalizedFromPositionLiftedOut) {
const fromMapped = isToFromSameListItem ? $from.pos : from;
from = hasNormalizedFromPositionLiftedOut ? $from.pos : fromMapped;
from = hasCommonListMoved ? from - LIST_STRUCTURE_CHANGED_OFFSET : from;
from = Math.max(from, 0);
}
if (hasNormalizedToPositionLiftedOut) {
const toMapped = isToFromSameListItem ? $to.pos : to;
to = hasNormalizedToPositionLiftedOut ? $to.pos : toMapped;
to = hasCommonListMoved ? to - LIST_STRUCTURE_CHANGED_OFFSET : to;
to = Math.min(to, tr.doc.nodeSize - 2);
}
if (normalizedSelection instanceof GapCursorSelection) {
const nextSelectionFrom = tr.doc.resolve(from);
return new GapCursorSelection(nextSelectionFrom, normalizedSelection.side);
}
if (originalSelection instanceof NodeSelection) {
return NodeSelection.create(tr.doc, from);
}
if (isCursorSelection) {
return TextSelection.between(tr.doc.resolve(to), tr.doc.resolve(to), -1);
}
return TextSelection.between(tr.doc.resolve(from), tr.doc.resolve(to), -1);
};
const splitRangeSelection = selection => {
const commonListRange = createListNodeRange({
selection
});
if (!commonListRange) {
return [];
}
const {
$from,
$to
} = selection;
if ($from.pos === $to.pos && $from.sameParent($to)) {
return [{
from: commonListRange.start,
to: commonListRange.end,
depth: commonListRange.depth
}];
}
const lastListItem = findPreviousListItemSibling($from);
if (!lastListItem) {
return [];
}
const nodeRanges = [];
const {
doc
} = $from;
let previousListItem = findPreviousListItemSibling($to);
while (previousListItem && previousListItem.pos >= lastListItem.pos && previousListItem.pos >= commonListRange.start) {
const node = doc.nodeAt(previousListItem.pos);
if (!node || !isListItemNode(node)) {
return [];
}
let offset = 0;
if (node && node.lastChild && isListNode(node.lastChild)) {
offset = node.lastChild.nodeSize;
}
const start = previousListItem.pos + 1;
nodeRanges.push({
from: start,
to: doc.resolve(start).end() - offset,
depth: previousListItem.depth
});
previousListItem = findPreviousListItemSibling(previousListItem);
}
return nodeRanges;
};
const outdentRangeToParentList = ({
tr,
range
}) => {
const end = range.end;
const endOfList = range.$to.end(range.depth);
const {
listItem
} = tr.doc.type.schema.nodes;
if (end < endOfList) {
const slice = new Slice(Fragment.from(listItem.create(null, range.parent.copy())), 1, 0);
const step = new ReplaceAroundStep(end - 1, endOfList, end, endOfList, slice, 1, true);
tr.step(step);
range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
}
const target = liftTarget(range);
if (target) {
tr.lift(range, target);
}
};
const extractListItemsRangeFromList = ({
tr,
range
}) => {
const list = range.parent;
const $start = tr.doc.resolve(range.start);
const listStart = $start.start(range.depth);
const listEnd = $start.end(range.depth);
const isAtTop = listStart === range.start;
const isAtBottom = listEnd === range.end;
const isTheEntireList = isAtTop && isAtBottom;
let listItemContent = isAtTop ? Fragment.empty : Fragment.from(list.copy(Fragment.empty));
for (let i = range.startIndex; i < range.endIndex; i++) {
listItemContent = listItemContent.append(list.child(i).content);
}
if (isAtTop) {
for (let i = 0; i < listItemContent.childCount; i++) {
const child = listItemContent.child(i);
if (child && isListNode(child) && child.type !== list.type) {
const newNestedList = list.type.create(null, child.content);
listItemContent = listItemContent.replaceChild(i, newNestedList);
}
}
}
const nextList = list.copy(Fragment.empty);
let nextListStartNumber;
// if splitting a numbered list, keep the splitted item
// counter as the start of the next (second half) list (instead
// of reverting back to 1 as a starting number)
const order = getOrderFromOrderedListNode(list);
if (list.type.name === 'orderedList') {
nextListStartNumber = range.endIndex - 1 + order;
// @ts-ignore - [unblock prosemirror bump] assigning to readonly attrs
nextList.attrs = {
...nextList.attrs,
order: nextListStartNumber
};
const {
splitListStartNumber
} = getRestartListsAttributes(tr);
if (typeof splitListStartNumber !== 'number') {
storeRestartListsAttributes(tr, {
splitListStartNumber: nextListStartNumber
});
}
}
const nextListFragment = listItemContent.append(Fragment.from(nextList));
// if the split list with nextListStartNumber is below another list
// with order (e.g due to multi-level indent items being lifted), track the
// list above's order instead, as it will be the split list's order after sibling joins
nextListFragment.forEach((node, _offset, index) => {
var _node$attrs;
if (node.type.name === 'orderedList' && ((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.order) === nextListStartNumber) {
var _prev$attrs;
const prev = nextListFragment.child(index - 1);
if ((prev === null || prev === void 0 ? void 0 : (_prev$attrs = prev.attrs) === null || _prev$attrs === void 0 ? void 0 : _prev$attrs.order) >= 0) {
var _prev$attrs2;
storeRestartListsAttributes(tr, {
splitListStartNumber: prev === null || prev === void 0 ? void 0 : (_prev$attrs2 = prev.attrs) === null || _prev$attrs2 === void 0 ? void 0 : _prev$attrs2.order
});
}
}
});
if (isTheEntireList) {
const slice = new Slice(listItemContent, 0, 0);
const step = new ReplaceStep($start.pos - 1, range.end + 1, slice, false);
storeRestartListsAttributes(tr, {
outdentScenario: undefined
});
tr.step(step);
} else if (isAtTop) {
const slice = new Slice(nextListFragment, 0, 1);
const step = new ReplaceStep($start.pos - 1, range.end, slice, false);
tr.step(step);
} else if (isAtBottom) {
const slice = new Slice(listItemContent, 1, 0);
const step = new ReplaceStep($start.pos, listEnd + 1, slice, false);
tr.step(step);
} else {
storeRestartListsAttributes(tr, {
outdentScenario: OUTDENT_SCENARIOS.SPLIT_LIST
});
const slice = new Slice(nextListFragment, 1, 1);
const step = new ReplaceAroundStep($start.pos, listEnd, range.end, listEnd, slice, slice.size, false);
tr.step(step);
}
};
const findPreviousListItemSibling = $pos => {
const doc = $pos.doc;
const isPositionListItem = isListNode($pos.node());
const listItemPosition = $pos;
if (!isPositionListItem) {
const listItem = findFirstParentListItemNode($pos);
if (!listItem) {
return null;
}
return doc.resolve(listItem.pos);
}
const resolved = doc.resolve(listItemPosition.pos);
const foundPosition = Selection.findFrom(resolved, -1);
if (!foundPosition) {
return null;
}
const parentListItemNode = findFirstParentListItemNode(foundPosition.$from);
if (!parentListItemNode) {
return null;
}
return doc.resolve(parentListItemNode.pos);
};