import { TextSelection, NodeSelection, AllSelection, Selection } from 'prosemirror-state';
import { DOMSerializer, Fragment, Mark, Slice, DOMParser } from 'prosemirror-model';
import { dropPoint } from 'prosemirror-transform';
const domIndex = function (node) {
for (var index = 0;; index++) {
node = node.previousSibling;
if (!node)
return index;
}
};
const parentNode = function (node) {
let parent = node.assignedSlot || node.parentNode;
return parent && parent.nodeType == 11 ? parent.host : parent;
};
let reusedRange = null;
// Note that this will always return the same range, because DOM range
// objects are every expensive, and keep slowing down subsequent DOM
// updates, for some reason.
const textRange = function (node, from, to) {
let range = reusedRange || (reusedRange = document.createRange());
range.setEnd(node, to == null ? node.nodeValue.length : to);
range.setStart(node, from || 0);
return range;
};
// Scans forward and backward through DOM positions equivalent to the
// given one to see if the two are in the same place (i.e. after a
// text node vs at the end of that text node)
const isEquivalentPosition = function (node, off, targetNode, targetOff) {
return targetNode && (scanFor(node, off, targetNode, targetOff, -1) ||
scanFor(node, off, targetNode, targetOff, 1));
};
const atomElements = /^(img|br|input|textarea|hr)$/i;
function scanFor(node, off, targetNode, targetOff, dir) {
for (;;) {
if (node == targetNode && off == targetOff)
return true;
if (off == (dir < 0 ? 0 : nodeSize(node))) {
let parent = node.parentNode;
if (!parent || parent.nodeType != 1 || hasBlockDesc(node) || atomElements.test(node.nodeName) ||
node.contentEditable == "false")
return false;
off = domIndex(node) + (dir < 0 ? 0 : 1);
node = parent;
}
else if (node.nodeType == 1) {
node = node.childNodes[off + (dir < 0 ? -1 : 0)];
if (node.contentEditable == "false")
return false;
off = dir < 0 ? nodeSize(node) : 0;
}
else {
return false;
}
}
}
function nodeSize(node) {
return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
}
function isOnEdge(node, offset, parent) {
for (let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) {
if (node == parent)
return true;
let index = domIndex(node);
node = node.parentNode;
if (!node)
return false;
atStart = atStart && index == 0;
atEnd = atEnd && index == nodeSize(node);
}
}
function hasBlockDesc(dom) {
let desc;
for (let cur = dom; cur; cur = cur.parentNode)
if (desc = cur.pmViewDesc)
break;
return desc && desc.node && desc.node.isBlock && (desc.dom == dom || desc.contentDOM == dom);
}
// Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523
// (isCollapsed inappropriately returns true in shadow dom)
const selectionCollapsed = function (domSel) {
return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, domSel.anchorNode, domSel.anchorOffset);
};
function keyEvent(keyCode, key) {
let event = document.createEvent("Event");
event.initEvent("keydown", true, true);
event.keyCode = keyCode;
event.key = event.code = key;
return event;
}
function deepActiveElement(doc) {
let elt = doc.activeElement;
while (elt && elt.shadowRoot)
elt = elt.shadowRoot.activeElement;
return elt;
}
function caretFromPoint(doc, x, y) {
if (doc.caretPositionFromPoint) {
try { // Firefox throws for this call in hard-to-predict circumstances (#994)
let pos = doc.caretPositionFromPoint(x, y);
if (pos)
return { node: pos.offsetNode, offset: pos.offset };
}
catch (_) { }
}
if (doc.caretRangeFromPoint) {
let range = doc.caretRangeFromPoint(x, y);
if (range)
return { node: range.startContainer, offset: range.startOffset };
}
}
const nav = typeof navigator != "undefined" ? navigator : null;
const doc = typeof document != "undefined" ? document : null;
const agent = (nav && nav.userAgent) || "";
const ie_edge = /Edge\/(\d+)/.exec(agent);
const ie_upto10 = /MSIE \d/.exec(agent);
const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent);
const ie = !!(ie_upto10 || ie_11up || ie_edge);
const ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0;
const gecko = !ie && /gecko\/(\d+)/i.test(agent);
gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1];
const _chrome = !ie && /Chrome\/(\d+)/.exec(agent);
const chrome = !!_chrome;
const chrome_version = _chrome ? +_chrome[1] : 0;
const safari = !ie && !!nav && /Apple Computer/.test(nav.vendor);
// Is true for both iOS and iPadOS for convenience
const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2);
const mac = ios || (nav ? /Mac/.test(nav.platform) : false);
const windows = nav ? /Win/.test(nav.platform) : false;
const android = /Android \d/.test(agent);
const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style;
const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;
function windowRect(doc) {
return { left: 0, right: doc.documentElement.clientWidth,
top: 0, bottom: doc.documentElement.clientHeight };
}
function getSide(value, side) {
return typeof value == "number" ? value : value[side];
}
function clientRect(node) {
let rect = node.getBoundingClientRect();
// Adjust for elements with style "transform: scale()"
let scaleX = (rect.width / node.offsetWidth) || 1;
let scaleY = (rect.height / node.offsetHeight) || 1;
// Make sure scrollbar width isn't included in the rectangle
return { left: rect.left, right: rect.left + node.clientWidth * scaleX,
top: rect.top, bottom: rect.top + node.clientHeight * scaleY };
}
function scrollRectIntoView(view, rect, startDOM) {
let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5;
let doc = view.dom.ownerDocument;
for (let parent = startDOM || view.dom;; parent = parentNode(parent)) {
if (!parent)
break;
if (parent.nodeType != 1)
continue;
let elt = parent;
let atTop = elt == doc.body;
let bounding = atTop ? windowRect(doc) : clientRect(elt);
let moveX = 0, moveY = 0;
if (rect.top < bounding.top + getSide(scrollThreshold, "top"))
moveY = -(bounding.top - rect.top + getSide(scrollMargin, "top"));
else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom"))
moveY = rect.bottom - rect.top > bounding.bottom - bounding.top
? rect.top + getSide(scrollMargin, "top") - bounding.top
: rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom");
if (rect.left < bounding.left + getSide(scrollThreshold, "left"))
moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left"));
else if (rect.right > bounding.right - getSide(scrollThreshold, "right"))
moveX = rect.right - bounding.right + getSide(scrollMargin, "right");
if (moveX || moveY) {
if (atTop) {
doc.defaultView.scrollBy(moveX, moveY);
}
else {
let startX = elt.scrollLeft, startY = elt.scrollTop;
if (moveY)
elt.scrollTop += moveY;
if (moveX)
elt.scrollLeft += moveX;
let dX = elt.scrollLeft - startX, dY = elt.scrollTop - startY;
rect = { left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY };
}
}
if (atTop || /^(fixed|sticky)$/.test(getComputedStyle(parent).position))
break;
}
}
// Store the scroll position of the editor's parent nodes, along with
// the top position of an element near the top of the editor, which
// will be used to make sure the visible viewport remains stable even
// when the size of the content above changes.
function storeScrollPos(view) {
let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top);
let refDOM, refTop;
for (let x = (rect.left + rect.right) / 2, y = startY + 1; y < Math.min(innerHeight, rect.bottom); y += 5) {
let dom = view.root.elementFromPoint(x, y);
if (!dom || dom == view.dom || !view.dom.contains(dom))
continue;
let localRect = dom.getBoundingClientRect();
if (localRect.top >= startY - 20) {
refDOM = dom;
refTop = localRect.top;
break;
}
}
return { refDOM: refDOM, refTop: refTop, stack: scrollStack(view.dom) };
}
function scrollStack(dom) {
let stack = [], doc = dom.ownerDocument;
for (let cur = dom; cur; cur = parentNode(cur)) {
stack.push({ dom: cur, top: cur.scrollTop, left: cur.scrollLeft });
if (dom == doc)
break;
}
return stack;
}
// Reset the scroll position of the editor's parent nodes to that what
// it was before, when storeScrollPos was called.
function resetScrollPos({ refDOM, refTop, stack }) {
let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0;
restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop);
}
function restoreScrollStack(stack, dTop) {
for (let i = 0; i < stack.length; i++) {
let { dom, top, left } = stack[i];
if (dom.scrollTop != top + dTop)
dom.scrollTop = top + dTop;
if (dom.scrollLeft != left)
dom.scrollLeft = left;
}
}
let preventScrollSupported = null;
// Feature-detects support for .focus({preventScroll: true}), and uses
// a fallback kludge when not supported.
function focusPreventScroll(dom) {
if (dom.setActive)
return dom.setActive(); // in IE
if (preventScrollSupported)
return dom.focus(preventScrollSupported);
let stored = scrollStack(dom);
dom.focus(preventScrollSupported == null ? {
get preventScroll() {
preventScrollSupported = { preventScroll: true };
return true;
}
} : undefined);
if (!preventScrollSupported) {
preventScrollSupported = false;
restoreScrollStack(stored, 0);
}
}
function findOffsetInNode(node, coords) {
let closest, dxClosest = 2e8, coordsClosest, offset = 0;
let rowBot = coords.top, rowTop = coords.top;
let firstBelow, coordsBelow;
for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) {
let rects;
if (child.nodeType == 1)
rects = child.getClientRects();
else if (child.nodeType == 3)
rects = textRange(child).getClientRects();
else
continue;
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
if (rect.top <= rowBot && rect.bottom >= rowTop) {
rowBot = Math.max(rect.bottom, rowBot);
rowTop = Math.min(rect.top, rowTop);
let dx = rect.left > coords.left ? rect.left - coords.left
: rect.right < coords.left ? coords.left - rect.right : 0;
if (dx < dxClosest) {
closest = child;
dxClosest = dx;
coordsClosest = dx && closest.nodeType == 3 ? {
left: rect.right < coords.left ? rect.right : rect.left,
top: coords.top
} : coords;
if (child.nodeType == 1 && dx)
offset = childIndex + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0);
continue;
}
}
else if (rect.top > coords.top && !firstBelow && rect.left <= coords.left && rect.right >= coords.left) {
firstBelow = child;
coordsBelow = { left: Math.max(rect.left, Math.min(rect.right, coords.left)), top: rect.top };
}
if (!closest && (coords.left >= rect.right && coords.top >= rect.top ||
coords.left >= rect.left && coords.top >= rect.bottom))
offset = childIndex + 1;
}
}
if (!closest && firstBelow) {
closest = firstBelow;
coordsClosest = coordsBelow;
dxClosest = 0;
}
if (closest && closest.nodeType == 3)
return findOffsetInText(closest, coordsClosest);
if (!closest || (dxClosest && closest.nodeType == 1))
return { node, offset };
return findOffsetInNode(closest, coordsClosest);
}
function findOffsetInText(node, coords) {
let len = node.nodeValue.length;
let range = document.createRange();
for (let i = 0; i < len; i++) {
range.setEnd(node, i + 1);
range.setStart(node, i);
let rect = singleRect(range, 1);
if (rect.top == rect.bottom)
continue;
if (inRect(coords, rect))
return { node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0) };
}
return { node, offset: 0 };
}
function inRect(coords, rect) {
return coords.left >= rect.left - 1 && coords.left <= rect.right + 1 &&
coords.top >= rect.top - 1 && coords.top <= rect.bottom + 1;
}
function targetKludge(dom, coords) {
let parent = dom.parentNode;
if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left)
return parent;
return dom;
}
function posFromElement(view, elt, coords) {
let { node, offset } = findOffsetInNode(elt, coords), bias = -1;
if (node.nodeType == 1 && !node.firstChild) {
let rect = node.getBoundingClientRect();
bias = rect.left != rect.right && coords.left > (rect.left + rect.right) / 2 ? 1 : -1;
}
return view.docView.posFromDOM(node, offset, bias);
}
function posFromCaret(view, node, offset, coords) {
// Browser (in caretPosition/RangeFromPoint) will agressively
// normalize towards nearby inline nodes. Since we are interested in
// positions between block nodes too, we first walk up the hierarchy
// of nodes to see if there are block nodes that the coordinates
// fall outside of. If so, we take the position before/after that
// block. If not, we call `posFromDOM` on the raw node/offset.
let outsideBlock = -1;
for (let cur = node, sawBlock = false;;) {
if (cur == view.dom)
break;
let desc = view.docView.nearestDesc(cur, true);
if (!desc)
return null;
if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent && !sawBlock || !desc.contentDOM)) {
let rect = desc.dom.getBoundingClientRect();
if (desc.node.isBlock && desc.parent && !sawBlock) {
sawBlock = true;
if (rect.left > coords.left || rect.top > coords.top)
outsideBlock = desc.posBefore;
else if (rect.right < coords.left || rect.bottom < coords.top)
outsideBlock = desc.posAfter;
}
if (!desc.contentDOM && outsideBlock < 0 && !desc.node.isText) {
// If we are inside a leaf, return the side of the leaf closer to the coords
let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2
: coords.left < (rect.left + rect.right) / 2;
return before ? desc.posBefore : desc.posAfter;
}
}
cur = desc.dom.parentNode;
}
return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1);
}
function elementFromPoint(element, coords, box) {
let len = element.childNodes.length;
if (len && box.top < box.bottom) {
for (let startI = Math.max(0, Math.min(len - 1, Math.floor(len * (coords.top - box.top) / (box.bottom - box.top)) - 2)), i = startI;;) {
let child = element.childNodes[i];
if (child.nodeType == 1) {
let rects = child.getClientRects();
for (let j = 0; j < rects.length; j++) {
let rect = rects[j];
if (inRect(coords, rect))
return elementFromPoint(child, coords, rect);
}
}
if ((i = (i + 1) % len) == startI)
break;
}
}
return element;
}
// Given an x,y position on the editor, get the position in the document.
function posAtCoords(view, coords) {
let doc = view.dom.ownerDocument, node, offset = 0;
let caret = caretFromPoint(doc, coords.left, coords.top);
if (caret)
({ node, offset } = caret);
let elt = (view.root.elementFromPoint ? view.root : doc)
.elementFromPoint(coords.left, coords.top);
let pos;
if (!elt || !view.dom.contains(elt.nodeType != 1 ? elt.parentNode : elt)) {
let box = view.dom.getBoundingClientRect();
if (!inRect(coords, box))
return null;
elt = elementFromPoint(view.dom, coords, box);
if (!elt)
return null;
}
// Safari's caretRangeFromPoint returns nonsense when on a draggable element
if (safari) {
for (let p = elt; node && p; p = parentNode(p))
if (p.draggable)
node = undefined;
}
elt = targetKludge(elt, coords);
if (node) {
if (gecko && node.nodeType == 1) {
// Firefox will sometimes return offsets into nodes, which
// have no actual children, from caretPositionFromPoint (#953)
offset = Math.min(offset, node.childNodes.length);
// It'll also move the returned position before image nodes,
// even if those are behind it.
if (offset < node.childNodes.length) {
let next = node.childNodes[offset], box;
if (next.nodeName == "IMG" && (box = next.getBoundingClientRect()).right <= coords.left &&
box.bottom > coords.top)
offset++;
}
}
let prev;
// When clicking above the right side of an uneditable node, Chrome will report a cursor position after that node.
if (webkit && offset && node.nodeType == 1 && (prev = node.childNodes[offset - 1]).nodeType == 1 &&
prev.contentEditable == "false" && prev.getBoundingClientRect().top >= coords.top)
offset--;
// Suspiciously specific kludge to work around caret*FromPoint
// never returning a position at the end of the document
if (node == view.dom && offset == node.childNodes.length - 1 && node.lastChild.nodeType == 1 &&
coords.top > node.lastChild.getBoundingClientRect().bottom)
pos = view.state.doc.content.size;
// Ignore positions directly after a BR, since caret*FromPoint
// 'round up' positions that would be more accurately placed
// before the BR node.
else if (offset == 0 || node.nodeType != 1 || node.childNodes[offset - 1].nodeName != "BR")
pos = posFromCaret(view, node, offset, coords);
}
if (pos == null)
pos = posFromElement(view, elt, coords);
let desc = view.docView.nearestDesc(elt, true);
return { pos, inside: desc ? desc.posAtStart - desc.border : -1 };
}
function nonZero(rect) {
return rect.top < rect.bottom || rect.left < rect.right;
}
function singleRect(target, bias) {
let rects = target.getClientRects();
if (rects.length) {
let first = rects[bias < 0 ? 0 : rects.length - 1];
if (nonZero(first))
return first;
}
return Array.prototype.find.call(rects, nonZero) || target.getBoundingClientRect();
}
const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
// Given a position in the document model, get a bounding box of the
// character at that position, relative to the window.
function coordsAtPos(view, pos, side) {
let { node, offset, atom } = view.docView.domFromPos(pos, side < 0 ? -1 : 1);
let supportEmptyRange = webkit || gecko;
if (node.nodeType == 3) {
// These browsers support querying empty text ranges. Prefer that in
// bidi context or when at the end of a node.
if (supportEmptyRange && (BIDI.test(node.nodeValue) || (side < 0 ? !offset : offset == node.nodeValue.length))) {
let rect = singleRect(textRange(node, offset, offset), side);
// Firefox returns bad results (the position before the space)
// when querying a position directly after line-broken
// whitespace. Detect this situation and and kludge around it
if (gecko && offset && /\s/.test(node.nodeValue[offset - 1]) && offset < node.nodeValue.length) {
let rectBefore = singleRect(textRange(node, offset - 1, offset - 1), -1);
if (rectBefore.top == rect.top) {
let rectAfter = singleRect(textRange(node, offset, offset + 1), -1);
if (rectAfter.top != rect.top)
return flattenV(rectAfter, rectAfter.left < rectBefore.left);
}
}
return rect;
}
else {
let from = offset, to = offset, takeSide = side < 0 ? 1 : -1;
if (side < 0 && !offset) {
to++;
takeSide = -1;
}
else if (side >= 0 && offset == node.nodeValue.length) {
from--;
takeSide = 1;
}
else if (side < 0) {
from--;
}
else {
to++;
}
return flattenV(singleRect(textRange(node, from, to), takeSide), takeSide < 0);
}
}
let $dom = view.state.doc.resolve(pos - (atom || 0));
// Return a horizontal line in block context
if (!$dom.parent.inlineContent) {
if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
let before = node.childNodes[offset - 1];
if (before.nodeType == 1)
return flattenH(before.getBoundingClientRect(), false);
}
if (atom == null && offset < nodeSize(node)) {
let after = node.childNodes[offset];
if (after.nodeType == 1)
return flattenH(after.getBoundingClientRect(), true);
}
return flattenH(node.getBoundingClientRect(), side >= 0);
}
// Inline, not in text node (this is not Bidi-safe)
if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
let before = node.childNodes[offset - 1];
let target = before.nodeType == 3 ? textRange(before, nodeSize(before) - (supportEmptyRange ? 0 : 1))
// BR nodes tend to only return the rectangle before them.
// Only use them if they are the last element in their parent
: before.nodeType == 1 && (before.nodeName != "BR" || !before.nextSibling) ? before : null;
if (target)
return flattenV(singleRect(target, 1), false);
}
if (atom == null && offset < nodeSize(node)) {
let after = node.childNodes[offset];
while (after.pmViewDesc && after.pmViewDesc.ignoreForCoords)
after = after.nextSibling;
let target = !after ? null : after.nodeType == 3 ? textRange(after, 0, (supportEmptyRange ? 0 : 1))
: after.nodeType == 1 ? after : null;
if (target)
return flattenV(singleRect(target, -1), true);
}
// All else failed, just try to get a rectangle for the target node
return flattenV(singleRect(node.nodeType == 3 ? textRange(node) : node, -side), side >= 0);
}
function flattenV(rect, left) {
if (rect.width == 0)
return rect;
let x = left ? rect.left : rect.right;
return { top: rect.top, bottom: rect.bottom, left: x, right: x };
}
function flattenH(rect, top) {
if (rect.height == 0)
return rect;
let y = top ? rect.top : rect.bottom;
return { top: y, bottom: y, left: rect.left, right: rect.right };
}
function withFlushedState(view, state, f) {
let viewState = view.state, active = view.root.activeElement;
if (viewState != state)
view.updateState(state);
if (active != view.dom)
view.focus();
try {
return f();
}
finally {
if (viewState != state)
view.updateState(viewState);
if (active != view.dom && active)
active.focus();
}
}
// Whether vertical position motion in a given direction
// from a position would leave a text block.
function endOfTextblockVertical(view, state, dir) {
let sel = state.selection;
let $pos = dir == "up" ? sel.$from : sel.$to;
return withFlushedState(view, state, () => {
let { node: dom } = view.docView.domFromPos($pos.pos, dir == "up" ? -1 : 1);
for (;;) {
let nearest = view.docView.nearestDesc(dom, true);
if (!nearest)
break;
if (nearest.node.isBlock) {
dom = nearest.contentDOM || nearest.dom;
break;
}
dom = nearest.dom.parentNode;
}
let coords = coordsAtPos(view, $pos.pos, 1);
for (let child = dom.firstChild; child; child = child.nextSibling) {
let boxes;
if (child.nodeType == 1)
boxes = child.getClientRects();
else if (child.nodeType == 3)
boxes = textRange(child, 0, child.nodeValue.length).getClientRects();
else
continue;
for (let i = 0; i < boxes.length; i++) {
let box = boxes[i];
if (box.bottom > box.top + 1 &&
(dir == "up" ? coords.top - box.top > (box.bottom - coords.top) * 2
: box.bottom - coords.bottom > (coords.bottom - box.top) * 2))
return false;
}
}
return true;
});
}
const maybeRTL = /[\u0590-\u08ac]/;
function endOfTextblockHorizontal(view, state, dir) {
let { $head } = state.selection;
if (!$head.parent.isTextblock)
return false;
let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size;
let sel = view.domSelection();
// If the textblock is all LTR, or the browser doesn't support
// Selection.modify (Edge), fall back to a primitive approach
if (!maybeRTL.test($head.parent.textContent) || !sel.modify)
return dir == "left" || dir == "backward" ? atStart : atEnd;
return withFlushedState(view, state, () => {
// This is a huge hack, but appears to be the best we can
// currently do: use `Selection.modify` to move the selection by
// one character, and see if that moves the cursor out of the
// textblock (or doesn't move it at all, when at the start/end of
// the document).
let { focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset } = view.domSelectionRange();
let oldBidiLevel = sel.caretBidiLevel // Only for Firefox
;
sel.modify("move", dir, "character");
let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom;
let { focusNode: newNode, focusOffset: newOff } = view.domSelectionRange();
let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) ||
(oldNode == newNode && oldOff == newOff);
// Restore the previous selection
try {
sel.collapse(anchorNode, anchorOffset);
if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend)
sel.extend(oldNode, oldOff);
}
catch (_) { }
if (oldBidiLevel != null)
sel.caretBidiLevel = oldBidiLevel;
return result;
});
}
let cachedState = null;
let cachedDir = null;
let cachedResult = false;
function endOfTextblock(view, state, dir) {
if (cachedState == state && cachedDir == dir)
return cachedResult;
cachedState = state;
cachedDir = dir;
return cachedResult = dir == "up" || dir == "down"
? endOfTextblockVertical(view, state, dir)
: endOfTextblockHorizontal(view, state, dir);
}
// View descriptions are data structures that describe the DOM that is
// used to represent the editor's content. They are used for:
//
// - Incremental redrawing when the document changes
//
// - Figuring out what part of the document a given DOM position
// corresponds to
//
// - Wiring in custom implementations of the editing interface for a
// given node
//
// They form a doubly-linked mutable tree, starting at `view.docView`.
const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3;
// Superclass for the various kinds of descriptions. Defines their
// basic structure and shared methods.
class ViewDesc {
constructor(parent, children, dom,
// This is the node that holds the child views. It may be null for
// descs that don't have children.
contentDOM) {
this.parent = parent;
this.children = children;
this.dom = dom;
this.contentDOM = contentDOM;
this.dirty = NOT_DIRTY;
// An expando property on the DOM node provides a link back to its
// description.
dom.pmViewDesc = this;
}
// Used to check whether a given description corresponds to a
// widget/mark/node.
matchesWidget(widget) { return false; }
matchesMark(mark) { return false; }
matchesNode(node, outerDeco, innerDeco) { return false; }
matchesHack(nodeName) { return false; }
// When parsing in-editor content (in domchange.js), we allow
// descriptions to determine the parse rules that should be used to
// parse them.
parseRule() { return null; }
// Used by the editor's event handler to ignore events that come
// from certain descs.
stopEvent(event) { return false; }
// The size of the content represented by this desc.
get size() {
let size = 0;
for (let i = 0; i < this.children.length; i++)
size += this.children[i].size;
return size;
}
// For block nodes, this represents the space taken up by their
// start/end tokens.
get border() { return 0; }
destroy() {
this.parent = undefined;
if (this.dom.pmViewDesc == this)
this.dom.pmViewDesc = undefined;
for (let i = 0; i < this.children.length; i++)
this.children[i].destroy();
}
posBeforeChild(child) {
for (let i = 0, pos = this.posAtStart;; i++) {
let cur = this.children[i];
if (cur == child)
return pos;
pos += cur.size;
}
}
get posBefore() {
return this.parent.posBeforeChild(this);
}
get posAtStart() {
return this.parent ? this.parent.posBeforeChild(this) + this.border : 0;
}
get posAfter() {
return this.posBefore + this.size;
}
get posAtEnd() {
return this.posAtStart + this.size - 2 * this.border;
}
localPosFromDOM(dom, offset, bias) {
// If the DOM position is in the content, use the child desc after
// it to figure out a position.
if (this.contentDOM && this.contentDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode)) {
if (bias < 0) {
let domBefore, desc;
if (dom == this.contentDOM) {
domBefore = dom.childNodes[offset - 1];
}
else {
while (dom.parentNode != this.contentDOM)
dom = dom.parentNode;
domBefore = dom.previousSibling;
}
while (domBefore && !((desc = domBefore.pmViewDesc) && desc.parent == this))
domBefore = domBefore.previousSibling;
return domBefore ? this.posBeforeChild(desc) + desc.size : this.posAtStart;
}
else {
let domAfter, desc;
if (dom == this.contentDOM) {
domAfter = dom.childNodes[offset];
}
else {
while (dom.parentNode != this.contentDOM)
dom = dom.parentNode;
domAfter = dom.nextSibling;
}
while (domAfter && !((desc = domAfter.pmViewDesc) && desc.parent == this))
domAfter = domAfter.nextSibling;
return domAfter ? this.posBeforeChild(desc) : this.posAtEnd;
}
}
// Otherwise, use various heuristics, falling back on the bias
// parameter, to determine whether to return the position at the
// start or at the end of this view desc.
let atEnd;
if (dom == this.dom && this.contentDOM) {
atEnd = offset > domIndex(this.contentDOM);
}
else if (this.contentDOM && this.contentDOM != this.dom && this.dom.contains(this.contentDOM)) {
atEnd = dom.compareDocumentPosition(this.contentDOM) & 2;
}
else if (this.dom.firstChild) {
if (offset == 0)
for (let search = dom;; search = search.parentNode) {
if (search == this.dom) {
atEnd = false;
break;
}
if (search.previousSibling)
break;
}
if (atEnd == null && offset == dom.childNodes.length)
for (let search = dom;; search = search.parentNode) {
if (search == this.dom) {
atEnd = true;
break;
}
if (search.nextSibling)
break;
}
}
return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart;
}
nearestDesc(dom, onlyNodes = false) {
for (let first = true, cur = dom; cur; cur = cur.parentNode) {
let desc = this.getDesc(cur), nodeDOM;
if (desc && (!onlyNodes || desc.node)) {
// If dom is outside of this desc's nodeDOM, don't count it.
if (first && (nodeDOM = desc.nodeDOM) &&
!(nodeDOM.nodeType == 1 ? nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : nodeDOM == dom))
first = false;
else
return desc;
}
}
}
getDesc(dom) {
let desc = dom.pmViewDesc;
for (let cur = desc; cur; cur = cur.parent)
if (cur == this)
return desc;
}
posFromDOM(dom, offset, bias) {
for (let scan = dom; scan; scan = scan.parentNode) {
let desc = this.getDesc(scan);
if (desc)
return desc.localPosFromDOM(dom, offset, bias);
}
return -1;
}
// Find the desc for the node after the given pos, if any. (When a
// parent node overrode rendering, there might not be one.)
descAt(pos) {
for (let i = 0, offset = 0; i < this.children.length; i++) {
let child = this.children[i], end = offset + child.size;
if (offset == pos && end != offset) {
while (!child.border && child.children.length)
child = child.children[0];
return child;
}
if (pos < end)
return child.descAt(pos - offset - child.border);
offset = end;
}
}
domFromPos(pos, side) {
if (!this.contentDOM)
return { node: this.dom, offset: 0, atom: pos + 1 };
// First find the position in the child array
let i = 0, offset = 0;
for (let curPos = 0; i < this.children.length; i++) {
let child = this.children[i], end = curPos + child.size;
if (end > pos || child instanceof TrailingHackViewDesc) {
offset = pos - curPos;
break;
}
curPos = end;
}
// If this points into the middle of a child, call through
if (offset)
return this.children[i].domFromPos(offset - this.children[i].border, side);
// Go back if there were any zero-length widgets with side >= 0 before this point
for (let prev; i && !(prev = this.children[i - 1]).size && prev instanceof WidgetViewDesc && prev.side >= 0; i--) { }
// Scan towards the first useable node
if (side <= 0) {
let prev, enter = true;
for (;; i--, enter = false) {
prev = i ? this.children[i - 1] : null;
if (!prev || prev.dom.parentNode == this.contentDOM)
break;
}
if (prev && side && enter && !prev.border && !prev.domAtom)
return prev.domFromPos(prev.size, side);
return { node: this.contentDOM, offset: prev ? domIndex(prev.dom) + 1 : 0 };
}
else {
let next, enter = true;
for (;; i++, enter = false) {
next = i < this.children.length ? this.children[i] : null;
if (!next || next.dom.parentNode == this.contentDOM)
break;
}
if (next && enter && !next.border && !next.domAtom)
return next.domFromPos(0, side);
return { node: this.contentDOM, offset: next ? domIndex(next.dom) : this.contentDOM.childNodes.length };
}
}
// Used to find a DOM range in a single parent for a given changed
// range.
parseRange(from, to, base = 0) {
if (this.children.length == 0)
return { node: this.contentDOM, from, to, fromOffset: 0, toOffset: this.contentDOM.childNodes.length };
let fromOffset = -1, toOffset = -1;
for (let offset = base, i = 0;; i++) {
let child = this.children[i], end = offset + child.size;
if (fromOffset == -1 && from <= end) {
let childBase = offset + child.border;
// FIXME maybe descend mark views to parse a narrower range?
if (from >= childBase && to <= end - child.border && child.node &&
child.contentDOM && this.contentDOM.contains(child.contentDOM))
return child.parseRange(from, to, childBase);
from = offset;
for (let j = i; j > 0; j--) {
let prev = this.children[j - 1];
if (prev.size && prev.dom.parentNode == this.contentDOM && !prev.emptyChildAt(1)) {
fromOffset = domIndex(prev.dom) + 1;
break;
}
from -= prev.size;
}
if (fromOffset == -1)
fromOffset = 0;
}
if (fromOffset > -1 && (end > to || i == this.children.length - 1)) {
to = end;
for (let j = i + 1; j < this.children.length; j++) {
let next = this.children[j];
if (next.size && next.dom.parentNode == this.contentDOM && !next.emptyChildAt(-1)) {
toOffset = domIndex(next.dom);
break;
}
to += next.size;
}
if (toOffset == -1)
toOffset = this.contentDOM.childNodes.length;
break;
}
offset = end;
}
return { node: this.contentDOM, from, to, fromOffset, toOffset };
}
emptyChildAt(side) {
if (this.border || !this.contentDOM || !this.children.length)
return false;
let child = this.children[side < 0 ? 0 : this.children.length - 1];
return child.size == 0 || child.emptyChildAt(side);
}
domAfterPos(pos) {
let { node, offset } = this.domFromPos(pos, 0);
if (node.nodeType != 1 || offset == node.childNodes.length)
throw new RangeError("No node after pos " + pos);
return node.childNodes[offset];
}
// View descs are responsible for setting any selection that falls
// entirely inside of them, so that custom implementations can do
// custom things with the selection. Note that this falls apart when
// a selection starts in such a node and ends in another, in which
// case we just use whatever domFromPos produces as a best effort.
setSelection(anchor, head, root, force = false) {
// If the selection falls entirely in a child, give it to that child
let from = Math.min(anchor, head), to = Math.max(anchor, head);
for (let i = 0, offset = 0; i < this.children.length; i++) {
let child = this.children[i], end = offset + child.size;
if (from > offset && to < end)
return child.setSelection(anchor - offset - child.border, head - offset - child.border, root, force);
offset = end;
}
let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1);
let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1);
let domSel = root.getSelection();
let brKludge = false;
// On Firefox, using Selection.collapse to put the cursor after a
// BR node for some reason doesn't always work (#1073). On Safari,
// the cursor sometimes inexplicable visually lags behind its
// reported position in such situations (#1092).
if ((gecko || safari) && anchor == head) {
let { node, offset } = anchorDOM;
if (node.nodeType == 3) {
brKludge = !!(offset && node.nodeValue[offset - 1] == "\n");
// Issue #1128
if (brKludge && offset == node.nodeValue.length) {
for (let scan = node, after; scan; scan = scan.parentNode) {
if (after = scan.nextSibling) {
if (after.nodeName == "BR")
anchorDOM = headDOM = { node: after.parentNode, offset: domIndex(after) + 1 };
break;
}
let desc = scan.pmViewDesc;
if (desc && desc.node && desc.node.isBlock)
break;
}
}
}
else {
let prev = node.childNodes[offset - 1];
brKludge = prev && (prev.nodeName == "BR" || prev.contentEditable == "false");
}
}
// Firefox can act strangely when the selection is in front of an
// uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536
if (gecko && domSel.focusNode && domSel.focusNode != headDOM.node && domSel.focusNode.nodeType == 1) {
let after = domSel.focusNode.childNodes[domSel.focusOffset];
if (after && after.contentEditable == "false")
force = true;
}
if (!(force || brKludge && safari) &&
isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) &&
isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset))
return;
// Selection.extend can be used to create an 'inverted' selection
// (one where the focus is before the anchor), but not all
// browsers support it yet.
let domSelExtended = false;
if ((domSel.extend || anchor == head) && !brKludge) {
domSel.collapse(anchorDOM.node, anchorDOM.offset);
try {
if (anchor != head)
domSel.extend(headDOM.node, headDOM.offset);
domSelExtended = true;
}
catch (_) {
// In some cases with Chrome the selection is empty after calling
// collapse, even when it should be valid. This appears to be a bug, but
// it is difficult to isolate. If this happens fallback to the old path
// without using extend.
// Similarly, this could crash on Safari if the editor is hidden, and
// there was no selection.
}
}
if (!domSelExtended) {
if (anchor > head) {
let tmp = anchorDOM;
anchorDOM = headDOM;
headDOM = tmp;
}
let range = document.createRange();
range.setEnd(headDOM.node, headDOM.offset);
range.setStart(anchorDOM.node, anchorDOM.offset);
domSel.removeAllRanges();
domSel.addRange(range);
}
}
ignoreMutation(mutation) {
return !this.contentDOM && mutation.type != "selection";
}
get contentLost() {
return this.contentDOM && this.contentDOM != this.dom && !this.dom.contains(this.contentDOM);
}
// Remove a subtree of the element tree that has been touched
// by a DOM change, so that the next update will redraw it.
markDirty(from, to) {
for (let offset = 0, i = 0; i < this.children.length; i++) {
let child = this.children[i], end = offset + child.size;
if (offset == end ? from <= end && to >= offset : from < end && to > offset) {
let startInside = offset + child.border, endInside = end - child.border;
if (from >= startInside && to <= endInside) {
this.dirty = from == offset || to == end ? CONTENT_DIRTY : CHILD_DIRTY;
if (from == startInside && to == endInside &&
(child.contentLost || child.dom.parentNode != this.contentDOM))
child.dirty = NODE_DIRTY;
else
child.markDirty(from - startInside, to - startInside);
return;
}
else {
child.dirty = child.dom == child.contentDOM && child.dom.parentNode == this.contentDOM && !child.children.length
? CONTENT_DIRTY : NODE_DIRTY;
}
}
offset = end;
}
this.dirty = CONTENT_DIRTY;
}
markParentsDirty() {
let level = 1;
for (let node = this.parent; node; node = node.parent, level++) {
let dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY;
if (node.dirty < dirty)
node.dirty = dirty;
}
}
get domAtom() { return false; }
get ignoreForCoords() { return false; }
}
// A widget desc represents a widget decoration, which is a DOM node
// drawn between the document nodes.
class WidgetViewDesc extends ViewDesc {
constructor(parent, widget, view, pos) {
let self, dom = widget.type.toDOM;
if (typeof dom == "function")
dom = dom(view, () => {
if (!self)
return pos;
if (self.parent)
return self.parent.posBeforeChild(self);
});
if (!widget.type.spec.raw) {
if (dom.nodeType != 1) {
let wrap = document.createElement("span");
wrap.appendChild(dom);
dom = wrap;
}
dom.contentEditable = "false";
dom.classList.add("ProseMirror-widget");
}
super(parent, [], dom, null);
this.widget = widget;
this.widget = widget;
self = this;
}
matchesWidget(widget) {
return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type);
}
parseRule() { return { ignore: true }; }
stopEvent(event) {
let stop = this.widget.spec.stopEvent;
return stop ? stop(event) : false;
}
ignoreMutation(mutation) {
return mutation.type != "selection" || this.widget.spec.ignoreSelection;
}
destroy() {
this.widget.type.destroy(this.dom);
super.destroy();
}
get domAtom() { return true; }
get side() { return this.widget.type.side; }
}
class CompositionViewDesc extends ViewDesc {
constructor(parent, dom, textDOM, text) {
super(parent, [], dom, null);
this.textDOM = textDOM;
this.text = text;
}
get size() { return this.text.length; }
localPosFromDOM(dom, offset) {
if (dom != this.textDOM)
return this.posAtStart + (offset ? this.size : 0);
return this.posAtStart + offset;
}
domFromPos(pos) {
return { node: this.textDOM, offset: pos };
}
ignoreMutation(mut) {
return mut.type === 'characterData' && mut.target.nodeValue == mut.oldValue;
}
}
// A mark desc represents a mark. May have multiple children,
// depending on how the mark is split. Note that marks are drawn using
// a fixed nesting order, for simplicity and predictability, so in
// some cases they will be split more often than would appear
// necessary.
class MarkViewDesc extends ViewDesc {
constructor(parent, mark, dom, contentDOM) {
super(parent, [], dom, contentDOM);
this.mark = mark;
}
static create(parent, mark, inline, view) {
let custom = view.nodeViews[mark.type.name];
let spec = custom && custom(mark, view, inline);
if (!spec || !spec.dom)
spec = DOMSerializer.renderSpec(document, mark.type.spec.toDOM(mark, inline));
return new MarkViewDesc(parent, mark, spec.dom, spec.contentDOM || spec.dom);
}
parseRule() {
if ((this.dirty & NODE_DIRTY) || this.mark.type.spec.reparseInView)
return null;
return { mark: this.mark.type.name, attrs: this.mark.attrs, contentElement: this.contentDOM };
}
matchesMark(mark) { return this.dirty != NODE_DIRTY && this.mark.eq(mark); }
markDirty(from, to) {
super.markDirty(from, to);
// Move dirty info to nearest node view
if (this.dirty != NOT_DIRTY) {
let parent = this.parent;
while (!parent.node)
parent = parent.parent;
if (parent.dirty < this.dirty)
parent.dirty = this.dirty;
this.dirty = NOT_DIRTY;
}
}
slice(from, to, view) {
let copy = MarkViewDesc.create(this.parent, this.mark, true, view);
let nodes = this.children, size = this.size;
if (to < size)
nodes = replaceNodes(nodes, to, size, view);
if (from > 0)
nodes = replaceNodes(nodes, 0, from, view);
for (let i = 0; i < nodes.length; i++)
nodes[i].parent = copy;
copy.children = nodes;
return copy;
}
}
// Node view descs are the main, most common type of view desc, and
// correspond to an actual node in the document. Unlike mark descs,
// they populate their child array themselves.
class NodeViewDesc extends ViewDesc {
constructor(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos) {
super(parent, [], dom, contentDOM);
this.node = node;
this.outerDeco = outerDeco;
this.innerDeco = innerDeco;
this.nodeDOM = nodeDOM;
}
// By default, a node is rendered using the `toDOM` method from the
// node type spec. But client code can use the `nodeViews` spec to
// supply a custom node view, which can influence various aspects of
// the way the node works.
//
// (Using subclassing for this was intentionally decided against,
// since it'd require exposing a whole slew of finicky
// implementation details to the user code that they probably will
// never need.)
static create(parent, node, outerDeco, innerDeco, view, pos) {
let custom = view.nodeViews[node.type.name], descObj;
let spec = custom && custom(node, view, () => {
// (This is a function that allows the custom view to find its
// own position)
if (!descObj)
return pos;
if (descObj.parent)
return descObj.parent.posBeforeChild(descObj);
}, outerDeco, innerDeco);
let dom = spec && spec.dom, contentDOM = spec && spec.contentDOM;
if (node.isText) {
if (!dom)
dom = document.createTextNode(node.text);
else if (dom.nodeType != 3)
throw new RangeError("Text must be rendered as a DOM text node");
}
else if (!dom) {
({ dom, contentDOM } = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node)));
}
if (!contentDOM && !node.isText && dom.nodeName != "BR") { // Chrome gets confused by
if (!dom.hasAttribute("contenteditable"))
dom.contentEditable = "false";
if (node.type.spec.draggable)
dom.draggable = true;
}
let nodeDOM = dom;
dom = applyOuterDeco(dom, outerDeco, node);
if (spec)
return descObj = new CustomNodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, spec, view, pos + 1);
else if (node.isText)
return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM, view);
else
return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, view, pos + 1);
}
parseRule() {
// Experimental kludge to allow opt-in re-parsing of nodes
if (this.node.type.spec.reparseInView)
return null;
// FIXME the assumption that this can always return the current
// attrs means that if the user somehow manages to change the
// attrs in the dom, that won't be picked up. Not entirely sure
// whether this is a problem
let rule = { node: this.node.type.name, attrs: this.node.attrs };
if (this.node.type.whitespace == "pre")
rule.preserveWhitespace = "full";
if (!this.contentDOM) {
rule.getContent = () => this.node.content;
}
else if (!this.contentLost) {
rule.contentElement = this.contentDOM;
}
else {
// Chrome likes to randomly recreate parent nodes when
// backspacing things. When that happens, this tries to find the
// new parent.
for (let i = this.children.length - 1; i >= 0; i--) {
let child = this.children[i];
if (this.dom.contains(child.dom.parentNode)) {
rule.contentElement = child.dom.parentNode;
break;
}
}
if (!rule.contentElement)
rule.getContent = () => Fragment.empty;
}
return rule;
}
matchesNode(node, outerDeco, innerDeco) {
return this.dirty == NOT_DIRTY && node.eq(this.node) &&
sameOuterDeco(outerDeco, this.outerDeco) && innerDeco.eq(this.innerDeco);
}
get size() { return this.node.nodeSize; }
get border() { return this.node.isLeaf ? 0 : 1; }
// Syncs `this.children` to match `this.node.content` and the local
// decorations, possibly introducing nesting for marks. Then, in a
// separate step, syncs the DOM inside `this.contentDOM` to
// `this.children`.
updateChildren(view, pos) {
let inline = this.node.inlineContent, off = pos;
let composition = view.composing ? this.localCompositionInfo(view, pos) : null;
let localComposition = composition && composition.pos > -1 ? composition : null;
let compositionInChild = composition && composition.pos < 0;
let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view);
iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => {
if (widget.spec.marks)
updater.syncToMarks(widget.spec.marks, inline, view);
else if (widget.type.side >= 0 && !insideNode)
updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view);
// If the next node is a desc matching this widget, reuse it,
// otherwise insert the widget as a new view desc.
updater.placeWidget(widget, view, off);
}, (child, outerDeco, innerDeco, i) => {
// Make sure the wrapping mark descs match the node's marks.
updater.syncToMarks(child.marks, inline, view);
// Try several strategies for drawing this node
let compIndex;
if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) ;
else if (compositionInChild && view.state.selection.from > off &&
view.state.selection.to < off + child.nodeSize &&
(compIndex = updater.findIndexWithChild(composition.node)) > -1 &&
updater.updateNodeAt(child, outerDeco, innerDeco, compIndex, view)) ;
else if (updater.updateNextNode(child, outerDeco, innerDeco, view, i, off)) ;
else {
// Add it as a new view
updater.addNode(child, outerDeco, innerDeco, view, off);
}
off += child.nodeSize;
});
// Drop all remaining descs after the current position.
updater.syncToMarks([], inline, view);
if (this.node.isTextblock)
updater.addTextblockHacks();
updater.destroyRest();
// Sync the DOM if anything changed
if (updater.changed || this.dirty == CONTENT_DIRTY) {
// May have to protect focused DOM from being changed if a composition is active
if (localComposition)
this.protectLocalComposition(view, localComposition);
renderDescs(this.contentDOM, this.children, view);
if (ios)
iosHacks(this.dom);
}
}
localCompositionInfo(view, pos) {
// Only do something if both the selection and a focused text node
// are inside of this node
let { from, to } = view.state.selection;
if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size)
return null;
let sel = view.domSelectionRange();
let textNode = nearbyTextNode(sel.focusNode, sel.focusOffset);
if (!textNode || !this.dom.contains(textNode.parentNode))
return null;
if (this.node.inlineContent) {
// Find the text in the focused node in the node, stop if it's not
// there (may have been modified through other means, in which
// case it should overwritten)
let text = textNode.nodeValue;
let textPos = findTextInFragment(this.node.content, text, from - pos, to - pos);
return textPos < 0 ? null : { node: textNode, pos: textPos, text };
}
else {
return { node: textNode, pos: -1, text: "" };
}
}
protectLocalComposition(view, { node, pos, text }) {
// The node is already part of a local view desc, leave it there
if (this.getDesc(node))
return;
// Create a composition view for the orphaned nodes
let topNode = node;
for (;; topNode = topNode.parentNode) {
if (topNode.parentNode == this.contentDOM)
break;
while (topNode.previousSibling)
topNode.parentNode.removeChild(topNode.previousSibling);
while (topNode.nextSibling)
topNode.parentNode.removeChild(topNode.nextSibling);
if (topNode.pmViewDesc)
topNode.pmViewDesc = undefined;
}
let desc = new CompositionViewDesc(this, topNode, node, text);
view.input.compositionNodes.push(desc);
// Patch up this.children to contain the composition view
this.children = replaceNodes(this.children, pos, pos + text.length, view, desc);
}
// If this desc must be updated to match the given node decoration,
// do so and return true.
update(node, outerDeco, innerDeco, view) {
if (this.dirty == NODE_DIRTY ||
!node.sameMarkup(this.node))
return false;
this.updateInner(node, outerDeco, innerDeco, view);
return true;
}
updateInner(node, outerDeco, innerDeco, view) {
this.updateOuterDeco(outerDeco);
this.node = node;
this.innerDeco = innerDeco;
if (this.contentDOM)
this.updateChildren(view, this.posAtStart);
this.dirty = NOT_DIRTY;
}
updateOuterDeco(outerDeco) {
if (sameOuterDeco(outerDeco, this.outerDeco))
return;
let needsWrap = this.nodeDOM.nodeType != 1;
let oldDOM = this.dom;
this.dom = patchOuterDeco(this.dom, this.nodeDOM, computeOuterDeco(this.outerDeco, this.node, needsWrap), computeOuterDeco(outerDeco, this.node, needsWrap));
if (this.dom != oldDOM) {
oldDOM.pmViewDesc = undefined;
this.dom.pmViewDesc = this;
}
this.outerDeco = outerDeco;
}
// Mark this node as being the selected node.
selectNode() {
if (this.nodeDOM.nodeType == 1)
this.nodeDOM.classList.add("ProseMirror-selectednode");
if (this.contentDOM || !this.node.type.spec.draggable)
this.dom.draggable = true;
}
// Remove selected node marking from this node.
deselectNode() {
if (this.nodeDOM.nodeType == 1)
this.nodeDOM.classList.remove("ProseMirror-selectednode");
if (this.contentDOM || !this.node.type.spec.draggable)
this.dom.removeAttribute("draggable");
}
get domAtom() { return this.node.isAtom; }
}
// Create a view desc for the top-level document node, to be exported
// and used by the view class.
function docViewDesc(doc, outerDeco, innerDeco, dom, view) {
applyOuterDeco(dom, outerDeco, doc);
let docView = new NodeViewDesc(undefined, doc, outerDeco, innerDeco, dom, dom, dom, view, 0);
if (docView.contentDOM)
docView.updateChildren(view, 0);
return docView;
}
class TextViewDesc extends NodeViewDesc {
constructor(parent, node, outerDeco, innerDeco, dom, nodeDOM, view) {
super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM, view, 0);
}
parseRule() {
let skip = this.nodeDOM.parentNode;
while (skip && skip != this.dom && !skip.pmIsDeco)
skip = skip.parentNode;
return { skip: (skip || true) };
}
update(node, outerDeco, innerDeco, view) {
if (this.dirty == NODE_DIRTY || (this.dirty != NOT_DIRTY && !this.inParent()) ||
!node.sameMarkup(this.node))
return false;
this.updateOuterDeco(outerDeco);
if ((this.dirty != NOT_DIRTY || node.text != this.node.text) && node.text != this.nodeDOM.nodeValue) {
this.nodeDOM.nodeValue = node.text;
if (view.trackWrites == this.nodeDOM)
view.trackWrites = null;
}
this.node = node;
this.dirty = NOT_DIRTY;
return true;
}
inParent() {
let parentDOM = this.parent.contentDOM;
for (let n = this.nodeDOM; n; n = n.parentNode)
if (n == parentDOM)
return true;
return false;
}
domFromPos(pos) {
return { node: this.nodeDOM, offset: pos };
}
localPosFromDOM(dom, offset, bias) {
if (dom == this.nodeDOM)
return this.posAtStart + Math.min(offset, this.node.text.length);
return super.localPosFromDOM(dom, offset, bias);
}
ignoreMutation(mutation) {
return mutation.type != "characterData" && mutation.type != "selection";
}
slice(from, to, view) {
let node = this.node.cut(from, to), dom = document.createTextNode(node.text);
return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom, view);
}
markDirty(from, to) {
super.markDirty(from, to);
if (this.dom != this.nodeDOM && (from == 0 || to == this.nodeDOM.nodeValue.length))
this.dirty = NODE_DIRTY;
}
get domAtom() { return false; }
}
// A dummy desc used to tag trailing BR or IMG nodes created to work
// around contentEditable terribleness.
class TrailingHackViewDesc extends ViewDesc {
parseRule() { return { ignore: true }; }
matchesHack(nodeName) { return this.dirty == NOT_DIRTY && this.dom.nodeName == nodeName; }
get domAtom() { return true; }
get ignoreForCoords() { return this.dom.nodeName == "IMG"; }
}
// A separate subclass is used for customized node views, so that the
// extra checks only have to be made for nodes that are actually
// customized.
class CustomNodeViewDesc extends NodeViewDesc {
constructor(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, spec, view, pos) {
super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos);
this.spec = spec;
}
// A custom `update` method gets to decide whether the update goes
// through. If it does, and there's a `contentDOM` node, our logic
// updates the children.
update(node, outerDeco, innerDeco, view) {
if (this.dirty == NODE_DIRTY)
return false;
if (this.spec.update) {
let result = this.spec.update(node, outerDeco, innerDeco);
if (result)
this.updateInner(node, outerDeco, innerDeco, view);
return result;
}
else if (!this.contentDOM && !node.isLeaf) {
return false;
}
else {
return super.update(node, outerDeco, innerDeco, view);
}
}
selectNode() {
this.spec.selectNode ? this.spec.selectNode() : super.selectNode();
}
deselectNode() {
this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode();
}
setSelection(anchor, head, root, force) {
this.spec.setSelection ? this.spec.setSelection(anchor, head, root)
: super.setSelection(anchor, head, root, force);
}
destroy() {
if (this.spec.destroy)
this.spec.destroy();
super.destroy();
}
stopEvent(event) {
return this.spec.stopEvent ? this.spec.stopEvent(event) : false;
}
ignoreMutation(mutation) {
return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation);
}
}
// Sync the content of the given DOM node with the nodes associated
// with the given array of view descs, recursing into mark descs
// because this should sync the subtree for a whole node at a time.
function renderDescs(parentDOM, descs, view) {
let dom = parentDOM.firstChild, written = false;
for (let i = 0; i < descs.length; i++) {
let desc = descs[i], childDOM = desc.dom;
if (childDOM.parentNode == parentDOM) {
while (childDOM != dom) {
dom = rm(dom);
written = true;
}
dom = dom.nextSibling;
}
else {
written = true;
parentDOM.insertBefore(childDOM, dom);
}
if (desc instanceof MarkViewDesc) {
let pos = dom ? dom.previousSibling : parentDOM.lastChild;
renderDescs(desc.contentDOM, desc.children, view);
dom = pos ? pos.nextSibling : parentDOM.firstChild;
}
}
while (dom) {
dom = rm(dom);
written = true;
}
if (written && view.trackWrites == parentDOM)
view.trackWrites = null;
}
const OuterDecoLevel = function (nodeName) {
if (nodeName)
this.nodeName = nodeName;
};
OuterDecoLevel.prototype = Object.create(null);
const noDeco = [new OuterDecoLevel];
function computeOuterDeco(outerDeco, node, needsWrap) {
if (outerDeco.length == 0)
return noDeco;
let top = needsWrap ? noDeco[0] : new OuterDecoLevel, result = [top];
for (let i = 0; i < outerDeco.length; i++) {
let attrs = outerDeco[i].type.attrs;
if (!attrs)
continue;
if (attrs.nodeName)
result.push(top = new OuterDecoLevel(attrs.nodeName));
for (let name in attrs) {
let val = attrs[name];
if (val == null)
continue;
if (needsWrap && result.length == 1)
result.push(top = new OuterDecoLevel(node.isInline ? "span" : "div"));
if (name == "class")
top.class = (top.class ? top.class + " " : "") + val;
else if (name == "style")
top.style = (top.style ? top.style + ";" : "") + val;
else if (name != "nodeName")
top[name] = val;
}
}
return result;
}
function patchOuterDeco(outerDOM, nodeDOM, prevComputed, curComputed) {
// Shortcut for trivial case
if (prevComputed == noDeco && curComputed == noDeco)
return nodeDOM;
let curDOM = nodeDOM;
for (let i = 0; i < curComputed.length; i++) {
let deco = curComputed[i], prev = prevComputed[i];
if (i) {
let parent;
if (prev && prev.nodeName == deco.nodeName && curDOM != outerDOM &&
(parent = curDOM.parentNode) && parent.nodeName.toLowerCase() == deco.nodeName) {
curDOM = parent;
}
else {
parent = document.createElement(deco.nodeName);
parent.pmIsDeco = true;
parent.appendChild(curDOM);
prev = noDeco[0];
curDOM = parent;
}
}
patchAttributes(curDOM, prev || noDeco[0], deco);
}
return curDOM;
}
function patchAttributes(dom, prev, cur) {
for (let name in prev)
if (name != "class" && name != "style" && name != "nodeName" && !(name in cur))
dom.removeAttribute(name);
for (let name in cur)
if (name != "class" && name != "style" && name != "nodeName" && cur[name] != prev[name])
dom.setAttribute(name, cur[name]);
if (prev.class != cur.class) {
let prevList = prev.class ? prev.class.split(" ").filter(Boolean) : [];
let curList = cur.class ? cur.class.split(" ").filter(Boolean) : [];
for (let i = 0; i < prevList.length; i++)
if (curList.indexOf(prevList[i]) == -1)
dom.classList.remove(prevList[i]);
for (let i = 0; i < curList.length; i++)
if (prevList.indexOf(curList[i]) == -1)
dom.classList.add(curList[i]);
if (dom.classList.length == 0)
dom.removeAttribute("class");
}
if (prev.style != cur.style) {
if (prev.style) {
let prop = /\s*([\w\-\xa1-\uffff]+)\s*:(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\(.*?\)|[^;])*/g, m;
while (m = prop.exec(prev.style))
dom.style.removeProperty(m[1]);
}
if (cur.style)
dom.style.cssText += cur.style;
}
}
function applyOuterDeco(dom, deco, node) {
return patchOuterDeco(dom, dom, noDeco, computeOuterDeco(deco, node, dom.nodeType != 1));
}
function sameOuterDeco(a, b) {
if (a.length != b.length)
return false;
for (let i = 0; i < a.length; i++)
if (!a[i].type.eq(b[i].type))
return false;
return true;
}
// Remove a DOM node and return its next sibling.
function rm(dom) {
let next = dom.nextSibling;
dom.parentNode.removeChild(dom);
return next;
}
// Helper class for incrementally updating a tree of mark descs and
// the widget and node descs inside of them.
class ViewTreeUpdater {
constructor(top, lock, view) {
this.lock = lock;
this.view = view;
// Index into `this.top`'s child array, represents the current
// update position.
this.index = 0;
// When entering a mark, the current top and index are pushed
// onto this.
this.stack = [];
// Tracks whether anything was changed
this.changed = false;
this.top = top;
this.preMatch = preMatch(top.node.content, top);
}
// Destroy and remove the children between the given indices in
// `this.top`.
destroyBetween(start, end) {
if (start == end)
return;
for (let i = start; i < end; i++)
this.top.children[i].destroy();
this.top.children.splice(start, end - start);
this.changed = true;
}
// Destroy all remaining children in `this.top`.
destroyRest() {
this.destroyBetween(this.index, this.top.children.length);
}
// Sync the current stack of mark descs with the given array of
// marks, reusing existing mark descs when possible.
syncToMarks(marks, inline, view) {
let keep = 0, depth = this.stack.length >> 1;
let maxKeep = Math.min(depth, marks.length);
while (keep < maxKeep &&
(keep == depth - 1 ? this.top : this.stack[(keep + 1) << 1])
.matchesMark(marks[keep]) && marks[keep].type.spec.spanning !== false)
keep++;
while (keep < depth) {
this.destroyRest();
this.top.dirty = NOT_DIRTY;
this.index = this.stack.pop();
this.top = this.stack.pop();
depth--;
}
while (depth < marks.length) {
this.stack.push(this.top, this.index + 1);
let found = -1;
for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
let next = this.top.children[i];
if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
found = i;
break;
}
}
if (found > -1) {
if (found > this.index) {
this.changed = true;
this.destroyBetween(this.index, found);
}
this.top = this.top.children[this.index];
}
else {
let markDesc = MarkViewDesc.create(this.top, marks[depth], inline, view);
this.top.children.splice(this.index, 0, markDesc);
this.top = markDesc;
this.changed = true;
}
this.index = 0;
depth++;
}
}
// Try to find a node desc matching the given data. Skip over it and
// return true when successful.
findNodeMatch(node, outerDeco, innerDeco, index) {
let found = -1, targetDesc;
if (index >= this.preMatch.index &&
(targetDesc = this.preMatch.matches[index - this.preMatch.index]).parent == this.top &&
targetDesc.matchesNode(node, outerDeco, innerDeco)) {
found = this.top.children.indexOf(targetDesc, this.index);
}
else {
for (let i = this.index, e = Math.min(this.top.children.length, i + 5); i < e; i++) {
let child = this.top.children[i];
if (child.matchesNode(node, outerDeco, innerDeco) && !this.preMatch.matched.has(child)) {
found = i;
break;
}
}
}
if (found < 0)
return false;
this.destroyBetween(this.index, found);
this.index++;
return true;
}
updateNodeAt(node, outerDeco, innerDeco, index, view) {
let child = this.top.children[index];
if (child.dirty == NODE_DIRTY && child.dom == child.contentDOM)
child.dirty = CONTENT_DIRTY;
if (!child.update(node, outerDeco, innerDeco, view))
return false;
this.destroyBetween(this.index, index);
this.index++;
return true;
}
findIndexWithChild(domNode) {
for (;;) {
let parent = domNode.parentNode;
if (!parent)
return -1;
if (parent == this.top.contentDOM) {
let desc = domNode.pmViewDesc;
if (desc)
for (let i = this.index; i < this.top.children.length; i++) {
if (this.top.children[i] == desc)
return i;
}
return -1;
}
domNode = parent;
}
}
// Try to update the next node, if any, to the given data. Checks
// pre-matches to avoid overwriting nodes that could still be used.
updateNextNode(node, outerDeco, innerDeco, view, index, pos) {
for (let i = this.index; i < this.top.children.length; i++) {
let next = this.top.children[i];
if (next instanceof NodeViewDesc) {
let preMatch = this.preMatch.matched.get(next);
if (preMatch != null && preMatch != index)
return false;
let nextDOM = next.dom, updated;
// Can't update if nextDOM is or contains this.lock, except if
// it's a text node whose content already matches the new text
// and whose decorations match the new ones.
let locked = this.isLocked(nextDOM) &&
!(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text &&
next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco));
if (!locked && next.update(node, outerDeco, innerDeco, view)) {
this.destroyBetween(this.index, i);
if (next.dom != nextDOM)
this.changed = true;
this.index++;
return true;
}
else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) {
this.top.children[this.index] = updated;
if (updated.contentDOM) {
updated.dirty = CONTENT_DIRTY;
updated.updateChildren(view, pos + 1);
updated.dirty = NOT_DIRTY;
}
this.changed = true;
this.index++;
return true;
}
break;
}
}
return false;
}
// When a node with content is replaced by a different node with
// identical content, move over its children.
recreateWrapper(next, node, outerDeco, innerDeco, view, pos) {
if (next.dirty || node.isAtom || !next.children.length ||
!next.node.content.eq(node.content))
return null;
let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos);
if (wrapper.contentDOM) {
wrapper.children = next.children;
next.children = [];
for (let ch of wrapper.children)
ch.parent = wrapper;
}
next.destroy();
return wrapper;
}
// Insert the node as a newly created node desc.
addNode(node, outerDeco, innerDeco, view, pos) {
let desc = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos);
if (desc.contentDOM)
desc.updateChildren(view, pos + 1);
this.top.children.splice(this.index++, 0, desc);
this.changed = true;
}
placeWidget(widget, view, pos) {
let next = this.index < this.top.children.length ? this.top.children[this.index] : null;
if (next && next.matchesWidget(widget) &&
(widget == next.widget || !next.widget.type.toDOM.parentNode)) {
this.index++;
}
else {
let desc = new WidgetViewDesc(this.top, widget, view, pos);
this.top.children.splice(this.index++, 0, desc);
this.changed = true;
}
}
// Make sure a textblock looks and behaves correctly in
// contentEditable.
addTextblockHacks() {
let lastChild = this.top.children[this.index - 1], parent = this.top;
while (lastChild instanceof MarkViewDesc) {
parent = lastChild;
lastChild = parent.children[parent.children.length - 1];
}
if (!lastChild || // Empty textblock
!(lastChild instanceof TextViewDesc) ||
/\n$/.test(lastChild.node.text) ||
(this.view.requiresGeckoHackNode && /\s$/.test(lastChild.node.text))) {
// Avoid bugs in Safari's cursor drawing (#1165) and Chrome's mouse selection (#1152)
if ((safari || chrome) && lastChild && lastChild.dom.contentEditable == "false")
this.addHackNode("IMG", parent);
this.addHackNode("BR", this.top);
}
}
addHackNode(nodeName, parent) {
if (parent == this.top && this.index < parent.children.length && parent.children[this.index].matchesHack(nodeName)) {
this.index++;
}
else {
let dom = document.createElement(nodeName);
if (nodeName == "IMG") {
dom.className = "ProseMirror-separator";
dom.alt = "";
}
if (nodeName == "BR")
dom.className = "ProseMirror-trailingBreak";
let hack = new TrailingHackViewDesc(this.top, [], dom, null);
if (parent != this.top)
parent.children.push(hack);
else
parent.children.splice(this.index++, 0, hack);
this.changed = true;
}
}
isLocked(node) {
return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode));
}
}
// Iterate from the end of the fragment and array of descs to find
// directly matching ones, in order to avoid overeagerly reusing those
// for other nodes. Returns the fragment index of the first node that
// is part of the sequence of matched nodes at the end of the
// fragment.
function preMatch(frag, parentDesc) {
let curDesc = parentDesc, descI = curDesc.children.length;
let fI = frag.childCount, matched = new Map, matches = [];
outer: while (fI > 0) {
let desc;
for (;;) {
if (descI) {
let next = curDesc.children[descI - 1];
if (next instanceof MarkViewDesc) {
curDesc = next;
descI = next.children.length;
}
else {
desc = next;
descI--;
break;
}
}
else if (curDesc == parentDesc) {
break outer;
}
else {
// FIXME
descI = curDesc.parent.children.indexOf(curDesc);
curDesc = curDesc.parent;
}
}
let node = desc.node;
if (!node)
continue;
if (node != frag.child(fI - 1))
break;
--fI;
matched.set(desc, fI);
matches.push(desc);
}
return { index: fI, matched, matches: matches.reverse() };
}
function compareSide(a, b) {
return a.type.side - b.type.side;
}
// This function abstracts iterating over the nodes and decorations in
// a fragment. Calls `onNode` for each node, with its local and child
// decorations. Splits text nodes when there is a decoration starting
// or ending inside of them. Calls `onWidget` for each widget.
function iterDeco(parent, deco, onWidget, onNode) {
let locals = deco.locals(parent), offset = 0;
// Simple, cheap variant for when there are no local decorations
if (locals.length == 0) {
for (let i = 0; i < parent.childCount; i++) {
let child = parent.child(i);
onNode(child, locals, deco.forChild(offset, child), i);
offset += child.nodeSize;
}
return;
}
let decoIndex = 0, active = [], restNode = null;
for (let parentIndex = 0;;) {
let widget, widgets;
while (decoIndex < locals.length && locals[decoIndex].to == offset) {
let next = locals[decoIndex++];
if (next.widget) {
if (!widget)
widget = next;
else
(widgets || (widgets = [widget])).push(next);
}
}
if (widget) {
if (widgets) {
widgets.sort(compareSide);
for (let i = 0; i < widgets.length; i++)
onWidget(widgets[i], parentIndex, !!restNode);
}
else {
onWidget(widget, parentIndex, !!restNode);
}
}
let child, index;
if (restNode) {
index = -1;
child = restNode;
restNode = null;
}
else if (parentIndex < parent.childCount) {
index = parentIndex;
child = parent.child(parentIndex++);
}
else {
break;
}
for (let i = 0; i < active.length; i++)
if (active[i].to <= offset)
active.splice(i--, 1);
while (decoIndex < locals.length && locals[decoIndex].from <= offset && locals[decoIndex].to > offset)
active.push(locals[decoIndex++]);
let end = offset + child.nodeSize;
if (child.isText) {
let cutAt = end;
if (decoIndex < locals.length && locals[decoIndex].from < cutAt)
cutAt = locals[decoIndex].from;
for (let i = 0; i < active.length; i++)
if (active[i].to < cutAt)
cutAt = active[i].to;
if (cutAt < end) {
restNode = child.cut(cutAt - offset);
child = child.cut(0, cutAt - offset);
end = cutAt;
index = -1;
}
}
else {
while (decoIndex < locals.length && locals[decoIndex].to < end)
decoIndex++;
}
let outerDeco = child.isInline && !child.isLeaf ? active.filter(d => !d.inline) : active.slice();
onNode(child, outerDeco, deco.forChild(offset, child), index);
offset = end;
}
}
// List markers in Mobile Safari will mysteriously disappear
// sometimes. This works around that.
function iosHacks(dom) {
if (dom.nodeName == "UL" || dom.nodeName == "OL") {
let oldCSS = dom.style.cssText;
dom.style.cssText = oldCSS + "; list-style: square !important";
window.getComputedStyle(dom).listStyle;
dom.style.cssText = oldCSS;
}
}
function nearbyTextNode(node, offset) {
for (;;) {
if (node.nodeType == 3)
return node;
if (node.nodeType == 1 && offset > 0) {
if (node.childNodes.length > offset && node.childNodes[offset].nodeType == 3)
return node.childNodes[offset];
node = node.childNodes[offset - 1];
offset = nodeSize(node);
}
else if (node.nodeType == 1 && offset < node.childNodes.length) {
node = node.childNodes[offset];
offset = 0;
}
else {
return null;
}
}
}
// Find a piece of text in an inline fragment, overlapping from-to
function findTextInFragment(frag, text, from, to) {
for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) {
let child = frag.child(i++), childStart = pos;
pos += child.nodeSize;
if (!child.isText)
continue;
let str = child.text;
while (i < frag.childCount) {
let next = frag.child(i++);
pos += next.nodeSize;
if (!next.isText)
break;
str += next.text;
}
if (pos >= from) {
if (pos >= to && str.slice(to - text.length - childStart, to - childStart) == text)
return to - text.length;
let found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1;
if (found >= 0 && found + text.length + childStart >= from)
return childStart + found;
if (from == to && str.length >= (to + text.length) - childStart &&
str.slice(to - childStart, to - childStart + text.length) == text)
return to;
}
}
return -1;
}
// Replace range from-to in an array of view descs with replacement
// (may be null to just delete). This goes very much against the grain
// of the rest of this code, which tends to create nodes with the
// right shape in one go, rather than messing with them after
// creation, but is necessary in the composition hack.
function replaceNodes(nodes, from, to, view, replacement) {
let result = [];
for (let i = 0, off = 0; i < nodes.length; i++) {
let child = nodes[i], start = off, end = off += child.size;
if (start >= to || end <= from) {
result.push(child);
}
else {
if (start < from)
result.push(child.slice(0, from - start, view));
if (replacement) {
result.push(replacement);
replacement = undefined;
}
if (end > to)
result.push(child.slice(to - start, child.size, view));
}
}
return result;
}
function selectionFromDOM(view, origin = null) {
let domSel = view.domSelectionRange(), doc = view.state.doc;
if (!domSel.focusNode)
return null;
let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0;
let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1);
if (head < 0)
return null;
let $head = doc.resolve(head), $anchor, selection;
if (selectionCollapsed(domSel)) {
$anchor = $head;
while (nearestDesc && !nearestDesc.node)
nearestDesc = nearestDesc.parent;
let nearestDescNode = nearestDesc.node;
if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent
&& !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) {
let pos = nearestDesc.posBefore;
selection = new NodeSelection(head == pos ? $head : doc.resolve(pos));
}
}
else {
let anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1);
if (anchor < 0)
return null;
$anchor = doc.resolve(anchor);
}
if (!selection) {
let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1;
selection = selectionBetween(view, $anchor, $head, bias);
}
return selection;
}
function editorOwnsSelection(view) {
return view.editable ? view.hasFocus() :
hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom);
}
function selectionToDOM(view, force = false) {
let sel = view.state.selection;
syncNodeSelection(view, sel);
if (!editorOwnsSelection(view))
return;
// The delayed drag selection causes issues with Cell Selections
// in Safari. And the drag selection delay is to workarond issues
// which only present in Chrome.
if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && chrome) {
let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection;
if (domSel.anchorNode && curSel.anchorNode &&
isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) {
view.input.mouseDown.delayedSelectionSync = true;
view.domObserver.setCurSelection();
return;
}
}
view.domObserver.disconnectSelection();
if (view.cursorWrapper) {
selectCursorWrapper(view);
}
else {
let { anchor, head } = sel, resetEditableFrom, resetEditableTo;
if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) {
if (!sel.$from.parent.inlineContent)
resetEditableFrom = temporarilyEditableNear(view, sel.from);
if (!sel.empty && !sel.$from.parent.inlineContent)
resetEditableTo = temporarilyEditableNear(view, sel.to);
}
view.docView.setSelection(anchor, head, view.root, force);
if (brokenSelectBetweenUneditable) {
if (resetEditableFrom)
resetEditable(resetEditableFrom);
if (resetEditableTo)
resetEditable(resetEditableTo);
}
if (sel.visible) {
view.dom.classList.remove("ProseMirror-hideselection");
}
else {
view.dom.classList.add("ProseMirror-hideselection");
if ("onselectionchange" in document)
removeClassOnSelectionChange(view);
}
}
view.domObserver.setCurSelection();
view.domObserver.connectSelection();
}
// Kludge to work around Webkit not allowing a selection to start/end
// between non-editable block nodes. We briefly make something
// editable, set the selection, then set it uneditable again.
const brokenSelectBetweenUneditable = safari || chrome && chrome_version < 63;
function temporarilyEditableNear(view, pos) {
let { node, offset } = view.docView.domFromPos(pos, 0);
let after = offset < node.childNodes.length ? node.childNodes[offset] : null;
let before = offset ? node.childNodes[offset - 1] : null;
if (safari && after && after.contentEditable == "false")
return setEditable(after);
if ((!after || after.contentEditable == "false") &&
(!before || before.contentEditable == "false")) {
if (after)
return setEditable(after);
else if (before)
return setEditable(before);
}
}
function setEditable(element) {
element.contentEditable = "true";
if (safari && element.draggable) {
element.draggable = false;
element.wasDraggable = true;
}
return element;
}
function resetEditable(element) {
element.contentEditable = "false";
if (element.wasDraggable) {
element.draggable = true;
element.wasDraggable = null;
}
}
function removeClassOnSelectionChange(view) {
let doc = view.dom.ownerDocument;
doc.removeEventListener("selectionchange", view.input.hideSelectionGuard);
let domSel = view.domSelectionRange();
let node = domSel.anchorNode, offset = domSel.anchorOffset;
doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => {
if (domSel.anchorNode != node || domSel.anchorOffset != offset) {
doc.removeEventListener("selectionchange", view.input.hideSelectionGuard);
setTimeout(() => {
if (!editorOwnsSelection(view) || view.state.selection.visible)
view.dom.classList.remove("ProseMirror-hideselection");
}, 20);
}
});
}
function selectCursorWrapper(view) {
let domSel = view.domSelection(), range = document.createRange();
let node = view.cursorWrapper.dom, img = node.nodeName == "IMG";
if (img)
range.setEnd(node.parentNode, domIndex(node) + 1);
else
range.setEnd(node, 0);
range.collapse(false);
domSel.removeAllRanges();
domSel.addRange(range);
// Kludge to kill 'control selection' in IE11 when selecting an
// invisible cursor wrapper, since that would result in those weird
// resize handles and a selection that considers the absolutely
// positioned wrapper, rather than the root editable node, the
// focused element.
if (!img && !view.state.selection.visible && ie && ie_version <= 11) {
node.disabled = true;
node.disabled = false;
}
}
function syncNodeSelection(view, sel) {
if (sel instanceof NodeSelection) {
let desc = view.docView.descAt(sel.from);
if (desc != view.lastSelectedViewDesc) {
clearNodeSelection(view);
if (desc)
desc.selectNode();
view.lastSelectedViewDesc = desc;
}
}
else {
clearNodeSelection(view);
}
}
// Clear all DOM statefulness of the last node selection.
function clearNodeSelection(view) {
if (view.lastSelectedViewDesc) {
if (view.lastSelectedViewDesc.parent)
view.lastSelectedViewDesc.deselectNode();
view.lastSelectedViewDesc = undefined;
}
}
function selectionBetween(view, $anchor, $head, bias) {
return view.someProp("createSelectionBetween", f => f(view, $anchor, $head))
|| TextSelection.between($anchor, $head, bias);
}
function hasFocusAndSelection(view) {
if (view.editable && !view.hasFocus())
return false;
return hasSelection(view);
}
function hasSelection(view) {
let sel = view.domSelectionRange();
if (!sel.anchorNode)
return false;
try {
// Firefox will raise 'permission denied' errors when accessing
// properties of `sel.anchorNode` when it's in a generated CSS
// element.
return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) &&
(view.editable || view.dom.contains(sel.focusNode.nodeType == 3 ? sel.focusNode.parentNode : sel.focusNode));
}
catch (_) {
return false;
}
}
function anchorInRightPlace(view) {
let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0);
let domSel = view.domSelectionRange();
return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset);
}
function moveSelectionBlock(state, dir) {
let { $anchor, $head } = state.selection;
let $side = dir > 0 ? $anchor.max($head) : $anchor.min($head);
let $start = !$side.parent.inlineContent ? $side : $side.depth ? state.doc.resolve(dir > 0 ? $side.after() : $side.before()) : null;
return $start && Selection.findFrom($start, dir);
}
function apply(view, sel) {
view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
return true;
}
function selectHorizontally(view, dir, mods) {
let sel = view.state.selection;
if (sel instanceof TextSelection) {
if (mods.indexOf("s") > -1) {
let { $head } = sel, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter;
if (!node || node.isText || !node.isLeaf)
return false;
let $newHead = view.state.doc.resolve($head.pos + node.nodeSize * (dir < 0 ? -1 : 1));
return apply(view, new TextSelection(sel.$anchor, $newHead));
}
else if (!sel.empty) {
return false;
}
else if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) {
let next = moveSelectionBlock(view.state, dir);
if (next && (next instanceof NodeSelection))
return apply(view, next);
return false;
}
else if (!(mac && mods.indexOf("m") > -1)) {
let $head = sel.$head, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter, desc;
if (!node || node.isText)
return false;
let nodePos = dir < 0 ? $head.pos - node.nodeSize : $head.pos;
if (!(node.isAtom || (desc = view.docView.descAt(nodePos)) && !desc.contentDOM))
return false;
if (NodeSelection.isSelectable(node)) {
return apply(view, new NodeSelection(dir < 0 ? view.state.doc.resolve($head.pos - node.nodeSize) : $head));
}
else if (webkit) {
// Chrome and Safari will introduce extra pointless cursor
// positions around inline uneditable nodes, so we have to
// take over and move the cursor past them (#937)
return apply(view, new TextSelection(view.state.doc.resolve(dir < 0 ? nodePos : nodePos + node.nodeSize)));
}
else {
return false;
}
}
}
else if (sel instanceof NodeSelection && sel.node.isInline) {
return apply(view, new TextSelection(dir > 0 ? sel.$to : sel.$from));
}
else {
let next = moveSelectionBlock(view.state, dir);
if (next)
return apply(view, next);
return false;
}
}
function nodeLen(node) {
return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
}
function isIgnorable(dom, dir) {
let desc = dom.pmViewDesc;
return desc && desc.size == 0 && (dir < 0 || dom.nextSibling || dom.nodeName != "BR");
}
function skipIgnoredNodes(view, dir) {
return dir < 0 ? skipIgnoredNodesBefore(view) : skipIgnoredNodesAfter(view);
}
// Make sure the cursor isn't directly after one or more ignored
// nodes, which will confuse the browser's cursor motion logic.
function skipIgnoredNodesBefore(view) {
let sel = view.domSelectionRange();
let node = sel.focusNode, offset = sel.focusOffset;
if (!node)
return;
let moveNode, moveOffset, force = false;
// Gecko will do odd things when the selection is directly in front
// of a non-editable node, so in that case, move it into the next
// node if possible. Issue prosemirror/prosemirror#832.
if (gecko && node.nodeType == 1 && offset < nodeLen(node) && isIgnorable(node.childNodes[offset], -1))
force = true;
for (;;) {
if (offset > 0) {
if (node.nodeType != 1) {
break;
}
else {
let before = node.childNodes[offset - 1];
if (isIgnorable(before, -1)) {
moveNode = node;
moveOffset = --offset;
}
else if (before.nodeType == 3) {
node = before;
offset = node.nodeValue.length;
}
else
break;
}
}
else if (isBlockNode(node)) {
break;
}
else {
let prev = node.previousSibling;
while (prev && isIgnorable(prev, -1)) {
moveNode = node.parentNode;
moveOffset = domIndex(prev);
prev = prev.previousSibling;
}
if (!prev) {
node = node.parentNode;
if (node == view.dom)
break;
offset = 0;
}
else {
node = prev;
offset = nodeLen(node);
}
}
}
if (force)
setSelFocus(view, node, offset);
else if (moveNode)
setSelFocus(view, moveNode, moveOffset);
}
// Make sure the cursor isn't directly before one or more ignored
// nodes.
function skipIgnoredNodesAfter(view) {
let sel = view.domSelectionRange();
let node = sel.focusNode, offset = sel.focusOffset;
if (!node)
return;
let len = nodeLen(node);
let moveNode, moveOffset;
for (;;) {
if (offset < len) {
if (node.nodeType != 1)
break;
let after = node.childNodes[offset];
if (isIgnorable(after, 1)) {
moveNode = node;
moveOffset = ++offset;
}
else
break;
}
else if (isBlockNode(node)) {
break;
}
else {
let next = node.nextSibling;
while (next && isIgnorable(next, 1)) {
moveNode = next.parentNode;
moveOffset = domIndex(next) + 1;
next = next.nextSibling;
}
if (!next) {
node = node.parentNode;
if (node == view.dom)
break;
offset = len = 0;
}
else {
node = next;
offset = 0;
len = nodeLen(node);
}
}
}
if (moveNode)
setSelFocus(view, moveNode, moveOffset);
}
function isBlockNode(dom) {
let desc = dom.pmViewDesc;
return desc && desc.node && desc.node.isBlock;
}
function textNodeAfter(node, offset) {
while (node && offset == node.childNodes.length && !hasBlockDesc(node)) {
offset = domIndex(node) + 1;
node = node.parentNode;
}
while (node && offset < node.childNodes.length) {
let next = node.childNodes[offset];
if (next.nodeType == 3)
return next;
if (next.nodeType == 1 && next.contentEditable == "false")
break;
node = next;
offset = 0;
}
}
function textNodeBefore(node, offset) {
while (node && !offset && !hasBlockDesc(node)) {
offset = domIndex(node);
node = node.parentNode;
}
while (node && offset) {
let next = node.childNodes[offset - 1];
if (next.nodeType == 3)
return next;
if (next.nodeType == 1 && next.contentEditable == "false")
break;
node = next;
offset = node.childNodes.length;
}
}
function setSelFocus(view, node, offset) {
if (node.nodeType != 3) {
let before, after;
if (after = textNodeAfter(node, offset)) {
node = after;
offset = 0;
}
else if (before = textNodeBefore(node, offset)) {
node = before;
offset = before.nodeValue.length;
}
}
let sel = view.domSelection();
if (selectionCollapsed(sel)) {
let range = document.createRange();
range.setEnd(node, offset);
range.setStart(node, offset);
sel.removeAllRanges();
sel.addRange(range);
}
else if (sel.extend) {
sel.extend(node, offset);
}
view.domObserver.setCurSelection();
let { state } = view;
// If no state update ends up happening, reset the selection.
setTimeout(() => {
if (view.state == state)
selectionToDOM(view);
}, 50);
}
function findDirection(view, pos) {
let $pos = view.state.doc.resolve(pos);
if (!(chrome || windows) && $pos.parent.inlineContent) {
let coords = view.coordsAtPos(pos);
if (pos > $pos.start()) {
let before = view.coordsAtPos(pos - 1);
let mid = (before.top + before.bottom) / 2;
if (mid > coords.top && mid < coords.bottom && Math.abs(before.left - coords.left) > 1)
return before.left < coords.left ? "ltr" : "rtl";
}
if (pos < $pos.end()) {
let after = view.coordsAtPos(pos + 1);
let mid = (after.top + after.bottom) / 2;
if (mid > coords.top && mid < coords.bottom && Math.abs(after.left - coords.left) > 1)
return after.left > coords.left ? "ltr" : "rtl";
}
}
let computed = getComputedStyle(view.dom).direction;
return computed == "rtl" ? "rtl" : "ltr";
}
// Check whether vertical selection motion would involve node
// selections. If so, apply it (if not, the result is left to the
// browser)
function selectVertically(view, dir, mods) {
let sel = view.state.selection;
if (sel instanceof TextSelection && !sel.empty || mods.indexOf("s") > -1)
return false;
if (mac && mods.indexOf("m") > -1)
return false;
let { $from, $to } = sel;
if (!$from.parent.inlineContent || view.endOfTextblock(dir < 0 ? "up" : "down")) {
let next = moveSelectionBlock(view.state, dir);
if (next && (next instanceof NodeSelection))
return apply(view, next);
}
if (!$from.parent.inlineContent) {
let side = dir < 0 ? $from : $to;
let beyond = sel instanceof AllSelection ? Selection.near(side, dir) : Selection.findFrom(side, dir);
return beyond ? apply(view, beyond) : false;
}
return false;
}
function stopNativeHorizontalDelete(view, dir) {
if (!(view.state.selection instanceof TextSelection))
return true;
let { $head, $anchor, empty } = view.state.selection;
if (!$head.sameParent($anchor))
return true;
if (!empty)
return false;
if (view.endOfTextblock(dir > 0 ? "forward" : "backward"))
return true;
let nextNode = !$head.textOffset && (dir < 0 ? $head.nodeBefore : $head.nodeAfter);
if (nextNode && !nextNode.isText) {
let tr = view.state.tr;
if (dir < 0)
tr.delete($head.pos - nextNode.nodeSize, $head.pos);
else
tr.delete($head.pos, $head.pos + nextNode.nodeSize);
view.dispatch(tr);
return true;
}
return false;
}
function switchEditable(view, node, state) {
view.domObserver.stop();
node.contentEditable = state;
view.domObserver.start();
}
// Issue #867 / #1090 / https://bugs.chromium.org/p/chromium/issues/detail?id=903821
// In which Safari (and at some point in the past, Chrome) does really
// wrong things when the down arrow is pressed when the cursor is
// directly at the start of a textblock and has an uneditable node
// after it
function safariDownArrowBug(view) {
if (!safari || view.state.selection.$head.parentOffset > 0)
return false;
let { focusNode, focusOffset } = view.domSelectionRange();
if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 &&
focusNode.firstChild && focusNode.firstChild.contentEditable == "false") {
let child = focusNode.firstChild;
switchEditable(view, child, "true");
setTimeout(() => switchEditable(view, child, "false"), 20);
}
return false;
}
// A backdrop key mapping used to make sure we always suppress keys
// that have a dangerous default effect, even if the commands they are
// bound to return false, and to make sure that cursor-motion keys
// find a cursor (as opposed to a node selection) when pressed. For
// cursor-motion keys, the code in the handlers also takes care of
// block selections.
function getMods(event) {
let result = "";
if (event.ctrlKey)
result += "c";
if (event.metaKey)
result += "m";
if (event.altKey)
result += "a";
if (event.shiftKey)
result += "s";
return result;
}
function captureKeyDown(view, event) {
let code = event.keyCode, mods = getMods(event);
if (code == 8 || (mac && code == 72 && mods == "c")) { // Backspace, Ctrl-h on Mac
return stopNativeHorizontalDelete(view, -1) || skipIgnoredNodes(view, -1);
}
else if ((code == 46 && !event.shiftKey) || (mac && code == 68 && mods == "c")) { // Delete, Ctrl-d on Mac
return stopNativeHorizontalDelete(view, 1) || skipIgnoredNodes(view, 1);
}
else if (code == 13 || code == 27) { // Enter, Esc
return true;
}
else if (code == 37 || (mac && code == 66 && mods == "c")) { // Left arrow, Ctrl-b on Mac
let dir = code == 37 ? (findDirection(view, view.state.selection.from) == "ltr" ? -1 : 1) : -1;
return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir);
}
else if (code == 39 || (mac && code == 70 && mods == "c")) { // Right arrow, Ctrl-f on Mac
let dir = code == 39 ? (findDirection(view, view.state.selection.from) == "ltr" ? 1 : -1) : 1;
return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir);
}
else if (code == 38 || (mac && code == 80 && mods == "c")) { // Up arrow, Ctrl-p on Mac
return selectVertically(view, -1, mods) || skipIgnoredNodes(view, -1);
}
else if (code == 40 || (mac && code == 78 && mods == "c")) { // Down arrow, Ctrl-n on Mac
return safariDownArrowBug(view) || selectVertically(view, 1, mods) || skipIgnoredNodes(view, 1);
}
else if (mods == (mac ? "m" : "c") &&
(code == 66 || code == 73 || code == 89 || code == 90)) { // Mod-[biyz]
return true;
}
return false;
}
function serializeForClipboard(view, slice) {
view.someProp("transformCopied", f => { slice = f(slice, view); });
let context = [], { content, openStart, openEnd } = slice;
while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild.childCount == 1) {
openStart--;
openEnd--;
let node = content.firstChild;
context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null);
content = node.content;
}
let serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema);
let doc = detachedDoc(), wrap = doc.createElement("div");
wrap.appendChild(serializer.serializeFragment(content, { document: doc }));
let firstChild = wrap.firstChild, needsWrap, wrappers = 0;
while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) {
for (let i = needsWrap.length - 1; i >= 0; i--) {
let wrapper = doc.createElement(needsWrap[i]);
while (wrap.firstChild)
wrapper.appendChild(wrap.firstChild);
wrap.appendChild(wrapper);
wrappers++;
}
firstChild = wrap.firstChild;
}
if (firstChild && firstChild.nodeType == 1)
firstChild.setAttribute("data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`);
let text = view.someProp("clipboardTextSerializer", f => f(slice, view)) ||
slice.content.textBetween(0, slice.content.size, "\n\n");
return { dom: wrap, text };
}
// Read a slice of content from the clipboard (or drop data).
function parseFromClipboard(view, text, html, plainText, $context) {
let inCode = $context.parent.type.spec.code;
let dom, slice;
if (!html && !text)
return null;
let asText = text && (plainText || inCode || !html);
if (asText) {
view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view); });
if (inCode)
return text ? new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) : Slice.empty;
let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText, view));
if (parsed) {
slice = parsed;
}
else {
let marks = $context.marks();
let { schema } = view.state, serializer = DOMSerializer.fromSchema(schema);
dom = document.createElement("div");
text.split(/(?:\r\n?|\n)+/).forEach(block => {
let p = dom.appendChild(document.createElement("p"));
if (block)
p.appendChild(serializer.serializeNode(schema.text(block, marks)));
});
}
}
else {
view.someProp("transformPastedHTML", f => { html = f(html, view); });
dom = readHTML(html);
if (webkit)
restoreReplacedSpaces(dom);
}
let contextNode = dom && dom.querySelector("[data-pm-slice]");
let sliceData = contextNode && /^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(contextNode.getAttribute("data-pm-slice") || "");
if (sliceData && sliceData[3])
for (let i = +sliceData[3]; i > 0; i--) {
let child = dom.firstChild;
while (child && child.nodeType != 1)
child = child.nextSibling;
if (!child)
break;
dom = child;
}
if (!slice) {
let parser = view.someProp("clipboardParser") || view.someProp("domParser") || DOMParser.fromSchema(view.state.schema);
slice = parser.parseSlice(dom, {
preserveWhitespace: !!(asText || sliceData),
context: $context,
ruleFromNode(dom) {
if (dom.nodeName == "BR" && !dom.nextSibling &&
dom.parentNode && !inlineParents.test(dom.parentNode.nodeName))
return { ignore: true };
return null;
}
});
}
if (sliceData) {
slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[4]);
}
else { // HTML wasn't created by ProseMirror. Make sure top-level siblings are coherent
slice = Slice.maxOpen(normalizeSiblings(slice.content, $context), true);
if (slice.openStart || slice.openEnd) {
let openStart = 0, openEnd = 0;
for (let node = slice.content.firstChild; openStart < slice.openStart && !node.type.spec.isolating; openStart++, node = node.firstChild) { }
for (let node = slice.content.lastChild; openEnd < slice.openEnd && !node.type.spec.isolating; openEnd++, node = node.lastChild) { }
slice = closeSlice(slice, openStart, openEnd);
}
}
view.someProp("transformPasted", f => { slice = f(slice, view); });
return slice;
}
const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i;
// Takes a slice parsed with parseSlice, which means there hasn't been
// any content-expression checking done on the top nodes, tries to
// find a parent node in the current context that might fit the nodes,
// and if successful, rebuilds the slice so that it fits into that parent.
//
// This addresses the problem that Transform.replace expects a
// coherent slice, and will fail to place a set of siblings that don't
// fit anywhere in the schema.
function normalizeSiblings(fragment, $context) {
if (fragment.childCount < 2)
return fragment;
for (let d = $context.depth; d >= 0; d--) {
let parent = $context.node(d);
let match = parent.contentMatchAt($context.index(d));
let lastWrap, result = [];
fragment.forEach(node => {
if (!result)
return;
let wrap = match.findWrapping(node.type), inLast;
if (!wrap)
return result = null;
if (inLast = result.length && lastWrap.length && addToSibling(wrap, lastWrap, node, result[result.length - 1], 0)) {
result[result.length - 1] = inLast;
}
else {
if (result.length)
result[result.length - 1] = closeRight(result[result.length - 1], lastWrap.length);
let wrapped = withWrappers(node, wrap);
result.push(wrapped);
match = match.matchType(wrapped.type);
lastWrap = wrap;
}
});
if (result)
return Fragment.from(result);
}
return fragment;
}
function withWrappers(node, wrap, from = 0) {
for (let i = wrap.length - 1; i >= from; i--)
node = wrap[i].create(null, Fragment.from(node));
return node;
}
// Used to group adjacent nodes wrapped in similar parents by
// normalizeSiblings into the same parent node
function addToSibling(wrap, lastWrap, node, sibling, depth) {
if (depth < wrap.length && depth < lastWrap.length && wrap[depth] == lastWrap[depth]) {
let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild, depth + 1);
if (inner)
return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner));
let match = sibling.contentMatchAt(sibling.childCount);
if (match.matchType(depth == wrap.length - 1 ? node.type : wrap[depth + 1]))
return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1))));
}
}
function closeRight(node, depth) {
if (depth == 0)
return node;
let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild, depth - 1));
let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true);
return node.copy(fragment.append(fill));
}
function closeRange(fragment, side, from, to, depth, openEnd) {
let node = side < 0 ? fragment.firstChild : fragment.lastChild, inner = node.content;
if (fragment.childCount > 1)
openEnd = 0;
if (depth < to - 1)
inner = closeRange(inner, side, from, to, depth + 1, openEnd);
if (depth >= from)
inner = side < 0 ? node.contentMatchAt(0).fillBefore(inner, openEnd <= depth).append(inner)
: inner.append(node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true));
return fragment.replaceChild(side < 0 ? 0 : fragment.childCount - 1, node.copy(inner));
}
function closeSlice(slice, openStart, openEnd) {
if (openStart < slice.openStart)
slice = new Slice(closeRange(slice.content, -1, openStart, slice.openStart, 0, slice.openEnd), openStart, slice.openEnd);
if (openEnd < slice.openEnd)
slice = new Slice(closeRange(slice.content, 1, openEnd, slice.openEnd, 0, 0), slice.openStart, openEnd);
return slice;
}
// Trick from jQuery -- some elements must be wrapped in other
// elements for innerHTML to work. I.e. if you do `div.innerHTML =
// "