UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

357 lines • 12.8 kB
import { liftTarget, NodeSelection, TextSelection, Slice, Fragment, findWrapping } from '../prosemirror'; import * as commands from '../commands'; import { LEFT } from '../keymaps'; import JSONSerializer from '../renderer/json'; export { default as ErrorReporter, } from './error-reporter'; export { filterContentByType } from './filter'; function validateNode(node) { return false; } function isMarkTypeExcludedFromMark(markType, mark) { return mark.type.excludes(markType); } function isMarkTypeAllowedInNode(markType, state) { return commands.toggleMark(markType)(state); } export function canMoveUp(state) { var selection = state.selection; if (selection instanceof TextSelection) { if (!selection.empty) { return true; } } return !atTheBeginningOfDoc(state); } export function canMoveDown(state) { var selection = state.selection; if (selection instanceof TextSelection) { if (!selection.empty) { return true; } } return !atTheEndOfDoc(state); } export function atTheEndOfDoc(state) { var selection = state.selection, doc = state.doc; return doc.nodeSize - selection.$to.pos - 2 === selection.$to.depth; } export function atTheBeginningOfDoc(state) { var selection = state.selection; return selection.$from.pos === selection.$from.depth; } export function atTheEndOfBlock(state) { var selection = state.selection; var $to = selection.$to; if (selection instanceof NodeSelection && selection.node.isBlock) { return true; } return endPositionOfParent($to) === $to.pos + 1; } export function atTheBeginningOfBlock(state) { var selection = state.selection; var $from = selection.$from; if (selection instanceof NodeSelection && selection.node.isBlock) { return true; } return startPositionOfParent($from) === $from.pos; } export function startPositionOfParent(resolvedPos) { return resolvedPos.start(resolvedPos.depth); } export function endPositionOfParent(resolvedPos) { return resolvedPos.end(resolvedPos.depth) + 1; } /** * Check if a mark is allowed at the current position based on a given state. * This method looks both at the currently active marks as well as the node and marks * at the current position to determine if the given mark type is allowed. * If there's a non-empty selection, the current position corresponds to the start * of the selection. */ export function isMarkTypeAllowedAtCurrentPosition(markType, state) { if (!isMarkTypeAllowedInNode(markType, state)) { return false; } var allowedInActiveMarks = true; var excludesMarkType = function (mark) { return isMarkTypeExcludedFromMark(markType, mark); }; if (state.tr.storedMarks) { allowedInActiveMarks = !state.tr.storedMarks.some(excludesMarkType); } else { allowedInActiveMarks = !state.selection.$from.marks().some(excludesMarkType); } return allowedInActiveMarks; } /** * Step through block-nodes between $from and $to and returns false if a node is * found that isn't of the specified type */ export function isRangeOfType(doc, $from, $to, nodeType) { return getAncestorNodesBetween(doc, $from, $to).filter(function (node) { return node.type !== nodeType; }).length === 0; } export function createSliceWithContent(content, state) { return new Slice(Fragment.from(state.schema.text(content)), 0, 0); } /** * Determines if content inside a selection can be joined with the next block. * We need this check since the built-in method for "joinDown" will join a orderedList with bulletList. */ export function canJoinDown(selection, doc, nodeType) { var res = doc.resolve(selection.$to.after(findAncestorPosition(doc, selection.$to).depth)); return res.nodeAfter && res.nodeAfter.type === nodeType; } export var setNodeSelection = function (view, pos) { var state = view.state, dispatch = view.dispatch; var tr = state.tr.setSelection(NodeSelection.create(state.doc, pos)); dispatch(tr); }; export function setTextSelection(view, anchor, head) { var state = view.state; var tr = state.tr.setSelection(TextSelection.create(state.doc, anchor, head)); view.dispatch(tr); } export function moveCursorToTheEnd(view) { var state = view.state; var anchor = Math.max(state.doc.nodeSize - 2, 0); var tr = state.tr.setSelection(TextSelection.create(state.doc, anchor)).scrollIntoView(); view.dispatch(tr); } /** * Determines if content inside a selection can be joined with the previous block. * We need this check since the built-in method for "joinUp" will join a orderedList with bulletList. */ export function canJoinUp(selection, doc, nodeType) { var res = doc.resolve(selection.$from.before(findAncestorPosition(doc, selection.$from).depth)); return res.nodeBefore && res.nodeBefore.type === nodeType; } /** * Returns all top-level ancestor-nodes between $from and $to */ export function getAncestorNodesBetween(doc, $from, $to) { var nodes = Array(); var maxDepth = findAncestorPosition(doc, $from).depth; var current = doc.resolve($from.start(maxDepth)); while (current.pos <= $to.start($to.depth)) { var depth = Math.min(current.depth, maxDepth); var node = current.node(depth); if (node) { nodes.push(node); } if (depth === 0) { break; } var next = doc.resolve(current.after(depth)); if (next.start(depth) >= doc.nodeSize - 2) { break; } if (next.depth !== current.depth) { next = doc.resolve(next.pos + 2); } if (next.depth) { current = doc.resolve(next.start(next.depth)); } else { current = doc.resolve(next.end(next.depth)); } } return nodes; } /** * Finds all "selection-groups" within a range. A selection group is based on ancestors. * * Example: * Given the following document and selection ({<} = start of selection and {>} = end) * doc * blockquote * ul * li * li{<} * li * p * p{>} * * The output will be two selection-groups. One within the ul and one with the two paragraphs. */ export function getGroupsInRange(doc, $from, $to, isNodeValid) { if (isNodeValid === void 0) { isNodeValid = validateNode; } var groups = Array(); var commonAncestor = hasCommonAncestor(doc, $from, $to); var fromAncestor = findAncestorPosition(doc, $from); if (commonAncestor || (fromAncestor.depth === 1 && isNodeValid($from.node(1)))) { groups.push({ $from: $from, $to: $to }); } else { var current = $from; while (current.pos < $to.pos) { var ancestorPos = findAncestorPosition(doc, current); while (ancestorPos.depth > 1) { ancestorPos = findAncestorPosition(doc, ancestorPos); } var endPos = doc.resolve(Math.min( // should not be smaller then start position in case of an empty paragpraph for example. Math.max(ancestorPos.start(ancestorPos.depth), ancestorPos.end(ancestorPos.depth) - 3), $to.pos)); groups.push({ $from: current, $to: endPos }); current = doc.resolve(Math.min(endPos.after(1) + 1, doc.nodeSize - 2)); } } return groups; } /** * Traverse the document until an "ancestor" is found. Any nestable block can be an ancestor. */ export function findAncestorPosition(doc, pos) { var nestableBlocks = ['blockquote', 'bulletList', 'orderedList']; if (pos.depth === 1) { return pos; } var node = pos.node(pos.depth); var newPos = pos; while (pos.depth >= 1) { pos = doc.resolve(pos.before(pos.depth)); node = pos.node(pos.depth); if (node && nestableBlocks.indexOf(node.type.name) !== -1) { newPos = pos; } } return newPos; } /** * Determine if two positions have a common ancestor. */ export function hasCommonAncestor(doc, $from, $to) { var current; var target; if ($from.depth > $to.depth) { current = findAncestorPosition(doc, $from); target = findAncestorPosition(doc, $to); } else { current = findAncestorPosition(doc, $to); target = findAncestorPosition(doc, $from); } while (current.depth > target.depth && current.depth > 1) { current = findAncestorPosition(doc, current); } return current.node(current.depth) === target.node(target.depth); } /** * Takes a selection $from and $to and lift all text nodes from their parents to document-level */ export function liftSelection(tr, doc, $from, $to) { var startPos = $from.start($from.depth); var endPos = $to.end($to.depth); var target = Math.max(0, findAncestorPosition(doc, $from).depth - 1); tr.doc.nodesBetween(startPos, endPos, function (node, pos) { if (node.isText || (node.isTextblock && !node.textContent) // Empty paragraph ) { var res = tr.doc.resolve(tr.mapping.map(pos)); var sel = new NodeSelection(res); var range = sel.$from.blockRange(sel.$to); if (liftTarget(range) !== undefined) { tr.lift(range, target); } } }); startPos = tr.mapping.map(startPos); endPos = tr.mapping.map(endPos); endPos = tr.doc.resolve(endPos).end(tr.doc.resolve(endPos).depth); // We want to select the entire node tr.setSelection(new TextSelection(tr.doc.resolve(startPos), tr.doc.resolve(endPos))); return { tr: tr, $from: tr.doc.resolve(startPos), $to: tr.doc.resolve(endPos) }; } /** * Lift nodes in block to one level above. */ export function liftSiblingNodes(view) { var tr = view.state.tr; var _a = view.state.selection, $from = _a.$from, $to = _a.$to; var blockStart = tr.doc.resolve($from.start($from.depth - 1)); var blockEnd = tr.doc.resolve($to.end($to.depth - 1)); var range = blockStart.blockRange(blockEnd); view.dispatch(tr.lift(range, blockStart.depth - 1)); } /** * Lift sibling nodes to document-level and select them. */ export function liftAndSelectSiblingNodes(view) { var tr = view.state.tr; var _a = view.state.selection, $from = _a.$from, $to = _a.$to; var blockStart = tr.doc.resolve($from.start($from.depth - 1)); var blockEnd = tr.doc.resolve($to.end($to.depth - 1)); var range = blockStart.blockRange(blockEnd); tr.setSelection(new TextSelection(blockStart, blockEnd)); tr.lift(range, blockStart.depth - 1); return tr; } export function wrapIn(nodeType, tr, $from, $to) { var range = $from.blockRange($to); var wrapping = range && findWrapping(range, nodeType); if (wrapping) { tr = tr.wrap(range, wrapping).scrollIntoView(); } return tr; } export function toJSON(node) { return new JSONSerializer().serializeFragment(node.content); } /** * Repeating string for multiple times */ export function stringRepeat(text, length) { var result = ''; for (var x = 0; x < length; x++) { result += text; } return result; } /** * A replacement for `Array.from` until it becomes widely implemented. */ export function arrayFrom(obj) { return Array.prototype.slice.call(obj); } export function moveLeft(view) { var event = new CustomEvent('keydown', { bubbles: true, cancelable: true, }); event.keyCode = LEFT; view.dispatchEvent(event); } /** * Function will create a list of wrapper blocks present in a selection. */ function getSelectedWrapperNodes(state) { var nodes = []; if (state.selection) { var _a = state.selection, $from = _a.$from, $to = _a.$to; var _b = state.schema.nodes, blockquote_1 = _b.blockquote, panel_1 = _b.panel, orderedList_1 = _b.orderedList, bulletList_1 = _b.bulletList, listItem_1 = _b.listItem, codeBlock_1 = _b.codeBlock; state.doc.nodesBetween($from.pos, $to.pos, function (node, pos) { if ((node.isBlock && [blockquote_1, panel_1, orderedList_1, bulletList_1, listItem_1].indexOf(node.type) >= 0) || node.type === codeBlock_1) { nodes.push(node.type); } }); } return nodes; } /** * Function will check if changing block types: Paragraph, Heading is enabled. */ export function areBlockTypesDisabled(state) { var nodesTypes = getSelectedWrapperNodes(state); var panel = state.schema.nodes.panel; return nodesTypes.filter(function (type) { return type !== panel; }).length > 0; } export var isTemporary = function (id) { return id.indexOf('temporary:') === 0; }; //# sourceMappingURL=index.js.map