From 97cda01c8c01860e4ffc1b181af3796ee2dacc36 Mon Sep 17 00:00:00 2001 From: robertu7 Date: Sun, 13 Oct 2024 11:36:06 +0000 Subject: [PATCH] deploy: dc65fbdf79b392829ec58660654bb1bffb095f16 --- dist/index.umd.js | 1173 +++++++++++++++++++++++++++------------------ 1 file changed, 696 insertions(+), 477 deletions(-) diff --git a/dist/index.umd.js b/dist/index.umd.js index 5da6b86..15f7b07 100644 --- a/dist/index.umd.js +++ b/dist/index.umd.js @@ -2043,7 +2043,7 @@ let result = []; for (let typeName in types) { let type = types[typeName]; - if (type.groups.indexOf(name) > -1) + if (type.isInGroup(name)) result.push(type); } if (result.length == 0) @@ -2322,6 +2322,13 @@ */ get isAtom() { return this.isLeaf || !!this.spec.atom; } /** + Return true when this node type is part of the given + [group](https://prosemirror.net/docs/ref/#model.NodeSpec.group). + */ + isInGroup(group) { + return this.groups.indexOf(group) > -1; + } + /** The node type's [whitespace](https://prosemirror.net/docs/ref/#model.NodeSpec.whitespace) option. */ get whitespace() { @@ -2778,7 +2785,7 @@ */ parse(dom, options = {}) { let context = new ParseContext(this, options, false); - context.addAll(dom, options.from, options.to); + context.addAll(dom, Mark$1.none, options.from, options.to); return context.finish(); } /** @@ -2791,7 +2798,7 @@ */ parseSlice(dom, options = {}) { let context = new ParseContext(this, options, true); - context.addAll(dom, options.from, options.to); + context.addAll(dom, Mark$1.none, options.from, options.to); return Slice.maxOpen(context.finish()); } /** @@ -2900,22 +2907,15 @@ return type && type.whitespace == "pre" ? OPT_PRESERVE_WS | OPT_PRESERVE_WS_FULL : base & ~OPT_OPEN_LEFT; } class NodeContext { - constructor(type, attrs, - // Marks applied to this node itself - marks, - // Marks that can't apply here, but will be used in children if possible - pendingMarks, solid, match, options) { + constructor(type, attrs, marks, solid, match, options) { this.type = type; this.attrs = attrs; this.marks = marks; - this.pendingMarks = pendingMarks; this.solid = solid; this.options = options; this.content = []; // Marks applied to the node's children this.activeMarks = Mark$1.none; - // Nested Marks with same type - this.stashMarks = []; this.match = match || (options & OPT_OPEN_LEFT ? null : type.contentMatch); } findWrapping(node) { @@ -2955,21 +2955,6 @@ content = content.append(this.match.fillBefore(Fragment.empty, true)); return this.type ? this.type.create(this.attrs, content, this.marks) : content; } - popFromStashMark(mark) { - for (let i = this.stashMarks.length - 1; i >= 0; i--) - if (mark.eq(this.stashMarks[i])) - return this.stashMarks.splice(i, 1)[0]; - } - applyPending(nextType) { - for (let i = 0, pending = this.pendingMarks; i < pending.length; i++) { - let mark = pending[i]; - if ((this.type ? this.type.allowsMarkType(mark.type) : markMayApply(mark.type, nextType)) && - !mark.isInSet(this.activeMarks)) { - this.activeMarks = mark.addToSet(this.activeMarks); - this.pendingMarks = mark.removeFromSet(this.pendingMarks); - } - } - } inlineContext(node) { if (this.type) return this.type.inlineContent; @@ -2991,11 +2976,11 @@ let topNode = options.topNode, topContext; let topOptions = wsOptionsFor(null, options.preserveWhitespace, 0) | (isOpen ? OPT_OPEN_LEFT : 0); if (topNode) - topContext = new NodeContext(topNode.type, topNode.attrs, Mark$1.none, Mark$1.none, true, options.topMatch || topNode.type.contentMatch, topOptions); + topContext = new NodeContext(topNode.type, topNode.attrs, Mark$1.none, true, options.topMatch || topNode.type.contentMatch, topOptions); else if (isOpen) - topContext = new NodeContext(null, null, Mark$1.none, Mark$1.none, true, null, topOptions); + topContext = new NodeContext(null, null, Mark$1.none, true, null, topOptions); else - topContext = new NodeContext(parser.schema.topNodeType, null, Mark$1.none, Mark$1.none, true, null, topOptions); + topContext = new NodeContext(parser.schema.topNodeType, null, Mark$1.none, true, null, topOptions); this.nodes = [topContext]; this.find = options.findPositions; this.needsBlock = false; @@ -3006,31 +2991,13 @@ // Add a DOM node to the content. Text is inserted as text node, // otherwise, the node is passed to `addElement` or, if it has a // `style` attribute, `addElementWithStyles`. - addDOM(dom) { + addDOM(dom, marks) { if (dom.nodeType == 3) - this.addTextNode(dom); + this.addTextNode(dom, marks); else if (dom.nodeType == 1) - this.addElement(dom); - } - withStyleRules(dom, f) { - let style = dom.style; - if (!style || !style.length) - return f(); - let marks = this.readStyles(dom.style); - if (!marks) - return; // A style with ignore: true - let [addMarks, removeMarks] = marks, top = this.top; - for (let i = 0; i < removeMarks.length; i++) - this.removePendingMark(removeMarks[i], top); - for (let i = 0; i < addMarks.length; i++) - this.addPendingMark(addMarks[i]); - f(); - for (let i = 0; i < addMarks.length; i++) - this.removePendingMark(addMarks[i], top); - for (let i = 0; i < removeMarks.length; i++) - this.addPendingMark(removeMarks[i]); - } - addTextNode(dom) { + this.addElement(dom, marks); + } + addTextNode(dom, marks) { let value = dom.nodeValue; let top = this.top; if (top.options & OPT_PRESERVE_WS_FULL || @@ -3057,7 +3024,7 @@ value = value.replace(/\r\n?/g, "\n"); } if (value) - this.insertNode(this.parser.schema.text(value)); + this.insertNode(this.parser.schema.text(value), marks); this.findInText(dom); } else { @@ -3066,7 +3033,7 @@ } // Try to find a handler for the given tag and use that to parse. If // none is found, the element's content nodes are added directly. - addElement(dom, matchAfter) { + addElement(dom, marks, matchAfter) { let name = dom.nodeName.toLowerCase(), ruleID; if (listTags.hasOwnProperty(name) && this.parser.normalizeLists) normalizeList(dom); @@ -3074,7 +3041,7 @@ (ruleID = this.parser.matchTag(dom, this, matchAfter)); if (rule ? rule.ignore : ignoreTags.hasOwnProperty(name)) { this.findInside(dom); - this.ignoreFallback(dom); + this.ignoreFallback(dom, marks); } else if (!rule || rule.skip || rule.closeParent) { if (rule && rule.closeParent) @@ -3092,45 +3059,44 @@ this.needsBlock = true; } else if (!dom.firstChild) { - this.leafFallback(dom); + this.leafFallback(dom, marks); return; } - if (rule && rule.skip) - this.addAll(dom); - else - this.withStyleRules(dom, () => this.addAll(dom)); + let innerMarks = rule && rule.skip ? marks : this.readStyles(dom, marks); + if (innerMarks) + this.addAll(dom, innerMarks); if (sync) this.sync(top); this.needsBlock = oldNeedsBlock; } else { - this.withStyleRules(dom, () => { - this.addElementByRule(dom, rule, rule.consuming === false ? ruleID : undefined); - }); + let innerMarks = this.readStyles(dom, marks); + if (innerMarks) + this.addElementByRule(dom, rule, innerMarks, rule.consuming === false ? ruleID : undefined); } } // Called for leaf DOM nodes that would otherwise be ignored - leafFallback(dom) { + leafFallback(dom, marks) { if (dom.nodeName == "BR" && this.top.type && this.top.type.inlineContent) - this.addTextNode(dom.ownerDocument.createTextNode("\n")); + this.addTextNode(dom.ownerDocument.createTextNode("\n"), marks); } // Called for ignored nodes - ignoreFallback(dom) { + ignoreFallback(dom, marks) { // Ignored BR nodes should at least create an inline context if (dom.nodeName == "BR" && (!this.top.type || !this.top.type.inlineContent)) - this.findPlace(this.parser.schema.text("-")); + this.findPlace(this.parser.schema.text("-"), marks); } // Run any style parser associated with the node's styles. Either - // return an array of marks, or null to indicate some of the styles - // had a rule with `ignore` set. - readStyles(styles) { - let add = Mark$1.none, remove = Mark$1.none; + // return an updated array of marks, or null to indicate some of the + // styles had a rule with `ignore` set. + readStyles(dom, marks) { + let styles = dom.style; // Because many properties will only show up in 'normalized' form // in `style.item` (i.e. text-decoration becomes // text-decoration-line, text-decoration-color, etc), we directly // query the styles mentioned in our rules instead of iterating // over the items. - if (styles.length) + if (styles && styles.length) for (let i = 0; i < this.parser.matchedStyles.length; i++) { let name = this.parser.matchedStyles[i], value = styles.getPropertyValue(name); if (value) @@ -3140,52 +3106,50 @@ break; if (rule.ignore) return null; - if (rule.clearMark) { - this.top.pendingMarks.concat(this.top.activeMarks).forEach(m => { - if (rule.clearMark(m)) - remove = m.addToSet(remove); - }); - } - else { - add = this.parser.schema.marks[rule.mark].create(rule.attrs).addToSet(add); - } + if (rule.clearMark) + marks = marks.filter(m => !rule.clearMark(m)); + else + marks = marks.concat(this.parser.schema.marks[rule.mark].create(rule.attrs)); if (rule.consuming === false) after = rule; else break; } } - return [add, remove]; + return marks; } // Look up a handler for the given node. If none are found, return // false. Otherwise, apply it, use its return value to drive the way // the node's content is wrapped, and return true. - addElementByRule(dom, rule, continueAfter) { - let sync, nodeType, mark; + addElementByRule(dom, rule, marks, continueAfter) { + let sync, nodeType; if (rule.node) { nodeType = this.parser.schema.nodes[rule.node]; if (!nodeType.isLeaf) { - sync = this.enter(nodeType, rule.attrs || null, rule.preserveWhitespace); + let inner = this.enter(nodeType, rule.attrs || null, marks, rule.preserveWhitespace); + if (inner) { + sync = true; + marks = inner; + } } - else if (!this.insertNode(nodeType.create(rule.attrs))) { - this.leafFallback(dom); + else if (!this.insertNode(nodeType.create(rule.attrs), marks)) { + this.leafFallback(dom, marks); } } else { let markType = this.parser.schema.marks[rule.mark]; - mark = markType.create(rule.attrs); - this.addPendingMark(mark); + marks = marks.concat(markType.create(rule.attrs)); } let startIn = this.top; if (nodeType && nodeType.isLeaf) { this.findInside(dom); } else if (continueAfter) { - this.addElement(dom, continueAfter); + this.addElement(dom, marks, continueAfter); } else if (rule.getContent) { this.findInside(dom); - rule.getContent(dom, this.parser.schema).forEach(node => this.insertNode(node)); + rule.getContent(dom, this.parser.schema).forEach(node => this.insertNode(node, marks)); } else { let contentDOM = dom; @@ -3196,28 +3160,27 @@ else if (rule.contentElement) contentDOM = rule.contentElement; this.findAround(dom, contentDOM, true); - this.addAll(contentDOM); + this.addAll(contentDOM, marks); + this.findAround(dom, contentDOM, false); } if (sync && this.sync(startIn)) this.open--; - if (mark) - this.removePendingMark(mark, startIn); } // Add all child nodes between `startIndex` and `endIndex` (or the // whole node, if not given). If `sync` is passed, use it to // synchronize after every block element. - addAll(parent, startIndex, endIndex) { + addAll(parent, marks, startIndex, endIndex) { let index = startIndex || 0; for (let dom = startIndex ? parent.childNodes[startIndex] : parent.firstChild, end = endIndex == null ? null : parent.childNodes[endIndex]; dom != end; dom = dom.nextSibling, ++index) { this.findAtPoint(parent, index); - this.addDOM(dom); + this.addDOM(dom, marks); } this.findAtPoint(parent, index); } // Try to find a way to fit the given node type into the current // context. May add intermediate wrappers and/or leave non-solid // nodes that we're in. - findPlace(node) { + findPlace(node, marks) { let route, sync; for (let depth = this.open; depth >= 0; depth--) { let cx = this.nodes[depth]; @@ -3232,53 +3195,61 @@ break; } if (!route) - return false; + return null; this.sync(sync); for (let i = 0; i < route.length; i++) - this.enterInner(route[i], null, false); - return true; + marks = this.enterInner(route[i], null, marks, false); + return marks; } // Try to insert the given node, adjusting the context when needed. - insertNode(node) { + insertNode(node, marks) { if (node.isInline && this.needsBlock && !this.top.type) { let block = this.textblockFromContext(); if (block) - this.enterInner(block); + marks = this.enterInner(block, null, marks); } - if (this.findPlace(node)) { + let innerMarks = this.findPlace(node, marks); + if (innerMarks) { this.closeExtra(); let top = this.top; - top.applyPending(node.type); if (top.match) top.match = top.match.matchType(node.type); - let marks = top.activeMarks; - for (let i = 0; i < node.marks.length; i++) - if (!top.type || top.type.allowsMarkType(node.marks[i].type)) - marks = node.marks[i].addToSet(marks); - top.content.push(node.mark(marks)); + let nodeMarks = Mark$1.none; + for (let m of innerMarks.concat(node.marks)) + if (top.type ? top.type.allowsMarkType(m.type) : markMayApply(m.type, node.type)) + nodeMarks = m.addToSet(nodeMarks); + top.content.push(node.mark(nodeMarks)); return true; } return false; } // Try to start a node of the given type, adjusting the context when // necessary. - enter(type, attrs, preserveWS) { - let ok = this.findPlace(type.create(attrs)); - if (ok) - this.enterInner(type, attrs, true, preserveWS); - return ok; + enter(type, attrs, marks, preserveWS) { + let innerMarks = this.findPlace(type.create(attrs), marks); + if (innerMarks) + innerMarks = this.enterInner(type, attrs, marks, true, preserveWS); + return innerMarks; } // Open a node of the given type - enterInner(type, attrs = null, solid = false, preserveWS) { + enterInner(type, attrs, marks, solid = false, preserveWS) { this.closeExtra(); let top = this.top; - top.applyPending(type); top.match = top.match && top.match.matchType(type); let options = wsOptionsFor(type, preserveWS, top.options); if ((top.options & OPT_OPEN_LEFT) && top.content.length == 0) options |= OPT_OPEN_LEFT; - this.nodes.push(new NodeContext(type, attrs, top.activeMarks, top.pendingMarks, solid, null, options)); + let applyMarks = Mark$1.none; + marks = marks.filter(m => { + if (top.type ? top.type.allowsMarkType(m.type) : markMayApply(m.type, type)) { + applyMarks = m.addToSet(applyMarks); + return false; + } + return true; + }); + this.nodes.push(new NodeContext(type, attrs, applyMarks, solid, null, options)); this.open++; + return marks; } // Make sure all nodes above this.open are finished and added to // their parents @@ -3369,7 +3340,7 @@ let next = depth > 0 || (depth == 0 && useRoot) ? this.nodes[depth].type : option && depth >= minDepth ? option.node(depth - minDepth).type : null; - if (!next || (next.name != part && next.groups.indexOf(part) == -1)) + if (!next || (next.name != part && !next.isInGroup(part))) return false; depth--; } @@ -3392,29 +3363,6 @@ return type; } } - addPendingMark(mark) { - let found = findSameMarkInSet(mark, this.top.pendingMarks); - if (found) - this.top.stashMarks.push(found); - this.top.pendingMarks = mark.addToSet(this.top.pendingMarks); - } - removePendingMark(mark, upto) { - for (let depth = this.open; depth >= 0; depth--) { - let level = this.nodes[depth]; - let found = level.pendingMarks.lastIndexOf(mark); - if (found > -1) { - level.pendingMarks = mark.removeFromSet(level.pendingMarks); - } - else { - level.activeMarks = mark.removeFromSet(level.activeMarks); - let stashMark = level.popFromStashMark(mark); - if (stashMark && level.type && level.type.allowsMarkType(stashMark.type)) - level.activeMarks = stashMark.addToSet(level.activeMarks); - } - if (level == upto) - break; - } - } } // Kludge to work around directly nested list nodes produced by some // tools and allowed by browsers to mean that the nested list is @@ -3467,12 +3415,6 @@ return true; } } - function findSameMarkInSet(mark, set) { - for (let i = 0; i < set.length; i++) { - if (mark.eq(set[i])) - return set[i]; - } - } /** A DOM serializer knows how to convert ProseMirror nodes and @@ -4775,7 +4717,9 @@ throw new RangeError("Type given to setBlockType should be a textblock"); let mapFrom = tr.steps.length; tr.doc.nodesBetween(from, to, (node, pos) => { - if (node.isTextblock && !node.hasMarkup(type, attrs) && canChangeType(tr.doc, tr.mapping.slice(mapFrom).map(pos), type)) { + let attrsHere = typeof attrs == "function" ? attrs(node) : attrs; + if (node.isTextblock && !node.hasMarkup(type, attrsHere) && + canChangeType(tr.doc, tr.mapping.slice(mapFrom).map(pos), type)) { let convertNewlines = null; if (type.schema.linebreakReplacement) { let pre = type.whitespace == "pre", supportLinebreak = !!type.contentMatch.matchType(type.schema.linebreakReplacement); @@ -4790,7 +4734,7 @@ clearIncompatible(tr, tr.mapping.slice(mapFrom).map(pos, 1), type, undefined, convertNewlines === null); let mapping = tr.mapping.slice(mapFrom); let startM = mapping.map(pos, 1), endM = mapping.map(pos + node.nodeSize, 1); - tr.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1, new Slice(Fragment.from(type.create(attrs, null, node.marks)), 0, 0), 1, true)); + tr.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1, new Slice(Fragment.from(type.create(attrsHere, null, node.marks)), 0, 0), 1, true)); if (convertNewlines === true) replaceNewlines$1(tr, node, pos, mapFrom); return false; @@ -4881,8 +4825,24 @@ return joinable($pos.nodeBefore, $pos.nodeAfter) && $pos.parent.canReplace(index, index + 1); } + function canAppendWithSubstitutedLinebreaks(a, b) { + if (!b.content.size) + a.type.compatibleContent(b.type); + let match = a.contentMatchAt(a.childCount); + let { linebreakReplacement } = a.type.schema; + for (let i = 0; i < b.childCount; i++) { + let child = b.child(i); + let type = child.type == linebreakReplacement ? a.type.schema.nodes.text : child.type; + match = match.matchType(type); + if (!match) + return false; + if (!a.type.allowsMarks(child.marks)) + return false; + } + return match.validEnd; + } function joinable(a, b) { - return !!(a && b && !a.isLeaf && a.canAppend(b)); + return !!(a && b && !a.isLeaf && canAppendWithSubstitutedLinebreaks(a, b)); } /** Find an ancestor of the given position that can be joined to the @@ -4915,8 +4875,31 @@ } } function join$2(tr, pos, depth) { - let step = new ReplaceStep(pos - depth, pos + depth, Slice.empty, true); - tr.step(step); + let convertNewlines = null; + let { linebreakReplacement } = tr.doc.type.schema; + let $before = tr.doc.resolve(pos - depth), beforeType = $before.node().type; + if (linebreakReplacement && beforeType.inlineContent) { + let pre = beforeType.whitespace == "pre"; + let supportLinebreak = !!beforeType.contentMatch.matchType(linebreakReplacement); + if (pre && !supportLinebreak) + convertNewlines = false; + else if (!pre && supportLinebreak) + convertNewlines = true; + } + let mapFrom = tr.steps.length; + if (convertNewlines === false) { + let $after = tr.doc.resolve(pos + depth); + replaceLinebreaks(tr, $after.node(), $after.before(), mapFrom); + } + if (beforeType.inlineContent) + clearIncompatible(tr, pos + depth - 1, beforeType, $before.node().contentMatchAt($before.index()), convertNewlines == null); + let mapping = tr.mapping.slice(mapFrom), start = mapping.map(pos - depth); + tr.step(new ReplaceStep(start, mapping.map(pos + depth, -1), Slice.empty, true)); + if (convertNewlines === true) { + let $full = tr.doc.resolve(start); + replaceNewlines$1(tr, $full.node(), $full.before(), tr.steps.length); + } + return tr; } /** Try to find a point where a node of the given type can be inserted @@ -5400,7 +5383,8 @@ return tr.delete($from.before(depth), $to.after(depth)); } for (let d = 1; d <= $from.depth && d <= $to.depth; d++) { - if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d) + if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d && + $from.start(d - 1) == $to.start(d - 1) && $from.node(d - 1).canReplace($from.index(d - 1), $to.index(d - 1))) return tr.delete($from.before(d), to); } tr.delete(from, to); @@ -6940,15 +6924,18 @@ if (doc.caretPositionFromPoint) { try { // Firefox throws for this call in hard-to-predict circumstances (#994) let pos = doc.caretPositionFromPoint(x, y); + // Clip the offset, because Chrome will return a text offset + // into nodes, which can't be treated as a regular DOM + // offset if (pos) - return { node: pos.offsetNode, offset: pos.offset }; + return { node: pos.offsetNode, offset: Math.min(nodeSize(pos.offsetNode), pos.offset) }; } catch (_) { } } if (doc.caretRangeFromPoint) { let range = doc.caretRangeFromPoint(x, y); if (range) - return { node: range.startContainer, offset: range.startOffset }; + return { node: range.startContainer, offset: Math.min(nodeSize(range.startContainer), range.startOffset) }; } } @@ -7460,6 +7447,8 @@ return false; let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size; let sel = view.domSelection(); + if (!sel) + return $head.pos == $head.start() || $head.pos == $head.end(); // 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) @@ -8666,6 +8655,7 @@ return true; } else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) { + this.destroyBetween(this.index, i); this.top.children[this.index] = updated; if (updated.contentDOM) { updated.dirty = CONTENT_DIRTY; @@ -8685,7 +8675,8 @@ // 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)) + !next.node.content.eq(node.content) || + !sameOuterDeco(outerDeco, next.outerDeco) || !innerDeco.eq(next.innerDeco)) return null; let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos); if (wrapper.contentDOM) { @@ -8956,9 +8947,9 @@ let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1); if (head < 0) return null; - let $head = doc.resolve(head), $anchor, selection; + let $head = doc.resolve(head), anchor, selection; if (selectionCollapsed(domSel)) { - $anchor = $head; + anchor = head; while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent; let nearestDescNode = nearestDesc.node; @@ -8969,11 +8960,25 @@ } } else { - let anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1); + if (domSel instanceof view.dom.ownerDocument.defaultView.Selection && domSel.rangeCount > 1) { + let min = head, max = head; + for (let i = 0; i < domSel.rangeCount; i++) { + let range = domSel.getRangeAt(i); + min = Math.min(min, view.docView.posFromDOM(range.startContainer, range.startOffset, 1)); + max = Math.max(max, view.docView.posFromDOM(range.endContainer, range.endOffset, -1)); + } + if (min < 0) + return null; + [anchor, head] = max == view.state.selection.anchor ? [max, min] : [min, max]; + $head = doc.resolve(head); + } + else { + anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1); + } if (anchor < 0) return null; - $anchor = doc.resolve(anchor); } + let $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); @@ -9082,12 +9087,14 @@ } function selectCursorWrapper(view) { let domSel = view.domSelection(), range = document.createRange(); + if (!domSel) + return; let node = view.cursorWrapper.dom, img = node.nodeName == "IMG"; if (img) - range.setEnd(node.parentNode, domIndex(node) + 1); + range.setStart(node.parentNode, domIndex(node) + 1); else - range.setEnd(node, 0); - range.collapse(false); + range.setStart(node, 0); + range.collapse(true); domSel.removeAllRanges(); domSel.addRange(range); // Kludge to kill 'control selection' in IE11 when selecting an @@ -9375,6 +9382,8 @@ } } let sel = view.domSelection(); + if (!sel) + return; if (selectionCollapsed(sel)) { let range = document.createRange(); range.setEnd(node, offset); @@ -9729,6 +9738,15 @@ function detachedDoc() { return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title")); } + function maybeWrapTrusted(html) { + let trustedTypes = window.trustedTypes; + if (!trustedTypes) + return html; + // With the require-trusted-types-for CSP, Chrome will block + // innerHTML, even on a detached document. This wraps the string in + // a way that makes the browser allow us to use its parser again. + return trustedTypes.createPolicy("detachedDocument", { createHTML: (s) => s }).createHTML(html); + } function readHTML(html) { let metas = /^(\s*]*>)*/.exec(html); if (metas) @@ -9737,7 +9755,7 @@ let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap; if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()]) html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "").reverse().join(""); - elt.innerHTML = html; + elt.innerHTML = maybeWrapTrusted(html); if (wrap) for (let i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt; @@ -9878,7 +9896,9 @@ // and handling them eagerly tends to corrupt the input. if (android && chrome && event.keyCode == 13) return; - if (event.keyCode != 229) + if (view.domObserver.selectionChanged(view.domSelectionRange())) + view.domObserver.flush(); + else if (event.keyCode != 229) view.domObserver.forceFlush(); // On iOS, if we preventDefault enter key presses, the virtual // keyboard gets confused. So the hack here is to set a flag that @@ -9941,6 +9961,8 @@ function updateSelection(view, selection, origin) { if (!view.focused) view.focus(); + if (view.state.selection.eq(selection)) + return; let tr = view.state.tr.setSelection(selection); if (origin == "pointer") tr.setMeta("pointer", true); @@ -10195,8 +10217,8 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => { if (!view.composing) { view.domObserver.flush(); - let { state } = view, $pos = state.selection.$from; - if (state.selection.empty && + let { state } = view, $pos = state.selection.$to; + if (state.selection instanceof TextSelection && (state.storedMarks || (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(m => m.type.spec.inclusive === false)))) { // Need to wrap the cursor in mark nodes different from the ones in the DOM context @@ -10205,7 +10227,7 @@ view.markCursor = null; } else { - endComposition(view); + endComposition(view, !state.selection.empty); // In firefox, if the cursor is after but outside a marked node, // the inserted text won't inherit the marks. So this moves it // inside if necessary. @@ -10216,7 +10238,9 @@ if (!before) break; if (before.nodeType == 3) { - view.domSelection().collapse(before, before.nodeValue.length); + let sel = view.domSelection(); + if (sel) + sel.collapse(before, before.nodeValue.length); break; } else { @@ -10284,15 +10308,17 @@ /** @internal */ - function endComposition(view, forceUpdate = false) { + function endComposition(view, restarting = false) { if (android && view.domObserver.flushingSoon >= 0) return; view.domObserver.forceFlush(); clearComposition(view); - if (forceUpdate || view.docView && view.docView.dirty) { + if (restarting || view.docView && view.docView.dirty) { let sel = selectionFromDOM(view); if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel)); + else if ((view.markCursor || restarting) && !view.state.selection.empty) + view.dispatch(view.state.tr.deleteSelection()); else view.updateState(view.state); return true; @@ -10949,6 +10975,7 @@ } return result; } + forEachSet(f) { f(this); } } /** The empty set of decorations. @@ -11024,6 +11051,10 @@ members.reduce((r, m) => r.concat(m instanceof DecorationSet ? m : m.members), [])); } } + forEachSet(f) { + for (let i = 0; i < this.members.length; i++) + this.members[i].forEachSet(f); + } } function mapChildren(oldChildren, newLocal, mapping, node, offset, oldOffset, options) { let children = oldChildren.slice(); @@ -11390,6 +11421,9 @@ this.queue.push(mut); return this.queue; } + selectionChanged(sel) { + return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel); + } flush() { let { view } = this; if (!view.docView || this.flushingSoon > -1) @@ -11397,8 +11431,7 @@ let mutations = this.pendingRecords(); if (mutations.length) this.queue = []; - let sel = view.domSelectionRange(); - let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel); + let sel = view.domSelectionRange(), newSel = this.selectionChanged(sel); let from = -1, to = -1, typeOver = false, added = []; if (view.editable) { for (let i = 0; i < mutations.length; i++) { @@ -11686,6 +11719,8 @@ } view.input.lastKeyCode = null; let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide); + if (change) + view.input.domChangeCount++; if ((ios && view.input.lastIOSEnter > Date.now() - 225 || android) && addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) && (!change || change.endA >= change.endB) && @@ -11711,7 +11746,6 @@ return; } } - view.input.domChangeCount++; // Handle the case where overwriting a selection by typing matches // the start or end of the selected content, creating a change // that's smaller than what was actually overwritten. @@ -12434,6 +12468,8 @@ */ domSelectionRange() { let sel = this.domSelection(); + if (!sel) + return { focusNode: null, focusOffset: 0, anchorNode: null, anchorOffset: 0 }; return safari && this.root.nodeType === 11 && deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel; } @@ -12471,7 +12507,7 @@ dom.className = "ProseMirror-separator"; dom.setAttribute("mark-placeholder", "true"); dom.setAttribute("alt", ""); - view.cursorWrapper = { dom, deco: Decoration.widget(view.state.selection.head, dom, { raw: true, marks: view.markCursor }) }; + view.cursorWrapper = { dom, deco: Decoration.widget(view.state.selection.from, dom, { raw: true, marks: view.markCursor }) }; } else { view.cursorWrapper = null; @@ -12794,21 +12830,26 @@ } let before = $cut.nodeBefore; // Apply the joining algorithm - if (!before.type.spec.isolating && deleteBarrier(state, $cut, dispatch)) + if (deleteBarrier(state, $cut, dispatch, -1)) return true; // If the node below has no content and the node above is // selectable, delete the node below and select the one above. if ($cursor.parent.content.size == 0 && (textblockAt(before, "end") || NodeSelection.isSelectable(before))) { - let delStep = replaceStep(state.doc, $cursor.before(), $cursor.after(), Slice.empty); - if (delStep && delStep.slice.size < delStep.to - delStep.from) { - if (dispatch) { - let tr = state.tr.step(delStep); - tr.setSelection(textblockAt(before, "end") ? Selection.findFrom(tr.doc.resolve(tr.mapping.map($cut.pos, -1)), -1) - : NodeSelection.create(tr.doc, $cut.pos - before.nodeSize)); - dispatch(tr.scrollIntoView()); + for (let depth = $cursor.depth;; depth--) { + let delStep = replaceStep(state.doc, $cursor.before(depth), $cursor.after(depth), Slice.empty); + if (delStep && delStep.slice.size < delStep.to - delStep.from) { + if (dispatch) { + let tr = state.tr.step(delStep); + tr.setSelection(textblockAt(before, "end") + ? Selection.findFrom(tr.doc.resolve(tr.mapping.map($cut.pos, -1)), -1) + : NodeSelection.create(tr.doc, $cut.pos - before.nodeSize)); + dispatch(tr.scrollIntoView()); + } + return true; } - return true; + if (depth == 1 || $cursor.node(depth - 1).childCount > 1) + break; } } // If the node before is an atom, delete it @@ -12940,7 +12981,7 @@ return false; let after = $cut.nodeAfter; // Try the joining algorithm - if (deleteBarrier(state, $cut, dispatch)) + if (deleteBarrier(state, $cut, dispatch, 1)) return true; // If the node above has no content and the node below is // selectable, delete the node above and select the one below. @@ -13166,19 +13207,15 @@ if (!$pos.parent.canReplace(index, index + 1) || !(after.isTextblock || canJoin(state.doc, $pos.pos))) return false; if (dispatch) - dispatch(state.tr - .clearIncompatible($pos.pos, before.type, before.contentMatchAt(before.childCount)) - .join($pos.pos) - .scrollIntoView()); + dispatch(state.tr.join($pos.pos).scrollIntoView()); return true; } - function deleteBarrier(state, $cut, dispatch) { + function deleteBarrier(state, $cut, dispatch, dir) { let before = $cut.nodeBefore, after = $cut.nodeAfter, conn, match; - if (before.type.spec.isolating || after.type.spec.isolating) - return false; - if (joinMaybeClear(state, $cut, dispatch)) + let isolated = before.type.spec.isolating || after.type.spec.isolating; + if (!isolated && joinMaybeClear(state, $cut, dispatch)) return true; - let canDelAfter = $cut.parent.canReplace($cut.index(), $cut.index() + 1); + let canDelAfter = !isolated && $cut.parent.canReplace($cut.index(), $cut.index() + 1); if (canDelAfter && (conn = (match = before.contentMatchAt(before.childCount)).findWrapping(after.type)) && match.matchType(conn[0] || after.type).validEnd) { @@ -13188,14 +13225,15 @@ wrap = Fragment.from(conn[i].create(null, wrap)); wrap = Fragment.from(before.copy(wrap)); let tr = state.tr.step(new ReplaceAroundStep($cut.pos - 1, end, $cut.pos, end, new Slice(wrap, 1, 0), conn.length, true)); - let joinAt = end + 2 * conn.length; - if (canJoin(tr.doc, joinAt)) - tr.join(joinAt); + let $joinAt = tr.doc.resolve(end + 2 * conn.length); + if ($joinAt.nodeAfter && $joinAt.nodeAfter.type == before.type && + canJoin(tr.doc, $joinAt.pos)) + tr.join($joinAt.pos); dispatch(tr.scrollIntoView()); } return true; } - let selAfter = Selection.findFrom($cut, 1); + let selAfter = after.type.spec.isolating || (dir > 0 && isolated) ? null : Selection.findFrom($cut, 1); let range = selAfter && selAfter.$from.blockRange(selAfter.$to), target = range && liftTarget(range); if (target != null && target >= $cut.depth) { if (dispatch) @@ -13757,7 +13795,18 @@ mergedAttributes[key] = [...existingClasses, ...insertClasses].join(' '); } else if (key === 'style') { - mergedAttributes[key] = [mergedAttributes[key], value].join('; '); + const newStyles = value ? value.split(';').map((style) => style.trim()).filter(Boolean) : []; + const existingStyles = mergedAttributes[key] ? mergedAttributes[key].split(';').map((style) => style.trim()).filter(Boolean) : []; + const styleMap = new Map(); + existingStyles.forEach(style => { + const [property, val] = style.split(':').map(part => part.trim()); + styleMap.set(property, val); + }); + newStyles.forEach(style => { + const [property, val] = style.split(':').map(part => part.trim()); + styleMap.set(property, val); + }); + mergedAttributes[key] = Array.from(styleMap.entries()).map(([property, val]) => `${property}: ${val}`).join('; '); } else { mergedAttributes[key] = value; @@ -13769,6 +13818,7 @@ function getRenderedAttributes(nodeOrMark, extensionAttributes) { return extensionAttributes + .filter(attribute => attribute.type === nodeOrMark.type.name) .filter(item => item.attribute.rendered) .map(item => { if (!item.attribute.renderHTML) { @@ -14205,6 +14255,123 @@ return plugin; } + // see: https://github.com/mesqueeb/is-what/blob/88d6e4ca92fb2baab6003c54e02eedf4e729e5ab/src/index.ts + function getType(value) { + return Object.prototype.toString.call(value).slice(8, -1); + } + function isPlainObject$2(value) { + if (getType(value) !== 'Object') { + return false; + } + return value.constructor === Object && Object.getPrototypeOf(value) === Object.prototype; + } + + function mergeDeep(target, source) { + const output = { ...target }; + if (isPlainObject$2(target) && isPlainObject$2(source)) { + Object.keys(source).forEach(key => { + if (isPlainObject$2(source[key]) && isPlainObject$2(target[key])) { + output[key] = mergeDeep(target[key], source[key]); + } + else { + output[key] = source[key]; + } + }); + } + return output; + } + + /** + * The Mark class is used to create custom mark extensions. + * @see https://tiptap.dev/api/extensions#create-a-new-extension + */ + class Mark { + constructor(config = {}) { + this.type = 'mark'; + this.name = 'mark'; + this.parent = null; + this.child = null; + this.config = { + name: this.name, + defaultOptions: {}, + }; + this.config = { + ...this.config, + ...config, + }; + this.name = this.config.name; + if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) { + console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`); + } + // TODO: remove `addOptions` fallback + this.options = this.config.defaultOptions; + if (this.config.addOptions) { + this.options = callOrReturn(getExtensionField(this, 'addOptions', { + name: this.name, + })); + } + this.storage = callOrReturn(getExtensionField(this, 'addStorage', { + name: this.name, + options: this.options, + })) || {}; + } + static create(config = {}) { + return new Mark(config); + } + configure(options = {}) { + // return a new instance so we can use the same extension + // with different calls of `configure` + const extension = this.extend({ + ...this.config, + addOptions: () => { + return mergeDeep(this.options, options); + }, + }); + // Always preserve the current name + extension.name = this.name; + // Set the parent to be our parent + extension.parent = this.parent; + return extension; + } + extend(extendedConfig = {}) { + const extension = new Mark(extendedConfig); + extension.parent = this; + this.child = extension; + extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name; + if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { + console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`); + } + extension.options = callOrReturn(getExtensionField(extension, 'addOptions', { + name: extension.name, + })); + extension.storage = callOrReturn(getExtensionField(extension, 'addStorage', { + name: extension.name, + options: extension.options, + })); + return extension; + } + static handleExit({ editor, mark }) { + const { tr } = editor.state; + const currentPos = editor.state.selection.$from; + const isAtEnd = currentPos.pos === currentPos.end(); + if (isAtEnd) { + const currentMarks = currentPos.marks(); + const isInMark = !!currentMarks.find(m => (m === null || m === void 0 ? void 0 : m.type.name) === mark.name); + if (!isInMark) { + return false; + } + const removeMark = currentMarks.find(m => (m === null || m === void 0 ? void 0 : m.type.name) === mark.name); + if (removeMark) { + tr.removeStoredMark(removeMark); + } + tr.insertText(' ', currentPos.pos); + editor.view.dispatch(tr); + return true; + } + return false; + } + } + function isNumber$1(value) { return typeof value === 'number'; } @@ -14588,15 +14755,19 @@ if (!addNodeView) { return []; } - const nodeview = (node, view, getPos, decorations) => { + const nodeview = (node, view, getPos, decorations, innerDecorations) => { const HTMLAttributes = getRenderedAttributes(node, extensionAttributes); return addNodeView()({ - editor, + // pass-through node, - getPos, + view, + getPos: getPos, decorations, - HTMLAttributes, + innerDecorations, + // tiptap-specific + editor, extension, + HTMLAttributes, }); }; return [extension.name, nodeview]; @@ -14660,32 +14831,6 @@ } } - // see: https://github.com/mesqueeb/is-what/blob/88d6e4ca92fb2baab6003c54e02eedf4e729e5ab/src/index.ts - function getType(value) { - return Object.prototype.toString.call(value).slice(8, -1); - } - function isPlainObject$2(value) { - if (getType(value) !== 'Object') { - return false; - } - return value.constructor === Object && Object.getPrototypeOf(value) === Object.prototype; - } - - function mergeDeep(target, source) { - const output = { ...target }; - if (isPlainObject$2(target) && isPlainObject$2(source)) { - Object.keys(source).forEach(key => { - if (isPlainObject$2(source[key]) && isPlainObject$2(target[key])) { - output[key] = mergeDeep(target[key], source[key]); - } - else { - output[key] = source[key]; - } - }); - } - return output; - } - /** * The Extension class is the base class for all extensions. * @see https://tiptap.dev/api/extensions#create-a-new-extension @@ -15204,7 +15349,11 @@ if (isArrayContent) { return Fragment.fromArray(content.map(item => schema.nodeFromJSON(item))); } - return schema.nodeFromJSON(content); + const node = schema.nodeFromJSON(content); + if (options.errorOnInvalidContent) { + node.check(); + } + return node; } catch (error) { if (options.errorOnInvalidContent) { @@ -16750,10 +16899,17 @@ const type = getNodeType(typeOrName, state.schema); const toggleType = getNodeType(toggleTypeOrName, state.schema); const isActive = isNodeActive(state, type, attributes); + let attributesToCopy; + if (state.selection.$anchor.sameParent(state.selection.$head)) { + // only copy attributes if the selection is pointing to a node of the same type + attributesToCopy = state.selection.$anchor.parent.attrs; + } if (isActive) { - return commands.setNode(toggleType); + return commands.setNode(toggleType, attributesToCopy); } - return commands.setNode(type, attributes); + // If the node is not active, we want to set the new node type with the given attributes + // Copying over the attributes from the current node if the selection is pointing to a node of the same type + return commands.setNode(type, { ...attributesToCopy, ...attributes }); }; const toggleWrap = (typeOrName, attributes = {}) => ({ state, commands }) => { @@ -16958,6 +17114,27 @@ }, }); + const Drop = Extension.create({ + name: 'drop', + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('tiptapDrop'), + props: { + handleDrop: (_, e, slice, moved) => { + this.editor.emit('drop', { + editor: this.editor, + event: e, + slice, + moved, + }); + }, + }, + }), + ]; + }, + }); + const Editable = Extension.create({ name: 'editable', addProseMirrorPlugins() { @@ -17087,7 +17264,8 @@ appendTransaction: (transactions, oldState, newState) => { const docChanges = transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc); - if (!docChanges) { + const ignoreTr = transactions.some(transaction => transaction.getMeta('preventClearDocument')); + if (!docChanges || ignoreTr) { return; } const { empty, from, to } = oldState.selection; @@ -17097,7 +17275,7 @@ if (empty || !allWasSelected) { return; } - const isEmpty = newState.doc.textBetween(0, newState.doc.content.size, ' ', ' ').length === 0; + const isEmpty = isNodeEmpty(newState.doc); if (!isEmpty) { return; } @@ -17121,6 +17299,26 @@ }, }); + const Paste = Extension.create({ + name: 'paste', + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('tiptapPaste'), + props: { + handlePaste: (_view, e, slice) => { + this.editor.emit('paste', { + editor: this.editor, + event: e, + slice, + }); + }, + }, + }), + ]; + }, + }); + const Tabindex = Extension.create({ name: 'tabindex', addProseMirrorPlugins() { @@ -17139,9 +17337,11 @@ __proto__: null, ClipboardTextSerializer: ClipboardTextSerializer, Commands: Commands, + Drop: Drop, Editable: Editable, FocusEvents: FocusEvents, Keymap: Keymap, + Paste: Paste, Tabindex: Tabindex }); @@ -17445,6 +17645,8 @@ img.ProseMirror-separator { onBlur: () => null, onDestroy: () => null, onContentError: ({ error }) => { throw error; }, + onPaste: () => null, + onDrop: () => null, }; this.isCapturingTransaction = false; this.capturedTransaction = null; @@ -17464,6 +17666,8 @@ img.ProseMirror-separator { this.on('focus', this.options.onFocus); this.on('blur', this.options.onBlur); this.on('destroy', this.options.onDestroy); + this.on('drop', ({ event, slice, moved }) => this.options.onDrop(event, slice, moved)); + this.on('paste', ({ event, slice }) => this.options.onPaste(event, slice)); window.setTimeout(() => { if (this.isDestroyed) { return; @@ -17552,6 +17756,7 @@ img.ProseMirror-separator { * * @param plugin A ProseMirror plugin * @param handlePlugins Control how to merge the plugin into the existing plugins. + * @returns The new editor state */ registerPlugin(plugin, handlePlugins) { const plugins = isFunction(handlePlugins) @@ -17559,15 +17764,17 @@ img.ProseMirror-separator { : [...this.state.plugins, plugin]; const state = this.state.reconfigure({ plugins }); this.view.updateState(state); + return state; } /** * Unregister a ProseMirror plugin. * * @param nameOrPluginKey The plugins name + * @returns The new editor state or undefined if the editor is destroyed */ unregisterPlugin(nameOrPluginKey) { if (this.isDestroyed) { - return; + return undefined; } // @ts-ignore const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key; @@ -17576,6 +17783,7 @@ img.ProseMirror-separator { plugins: this.state.plugins.filter(plugin => !plugin.key.startsWith(name)), }); this.view.updateState(state); + return state; } /** * Creates an extension manager. @@ -17591,7 +17799,14 @@ img.ProseMirror-separator { FocusEvents, Keymap, Tabindex, - ] : []; + Drop, + Paste, + ].filter(ext => { + if (typeof this.options.enableCoreExtensions === 'object') { + return this.options.enableCoreExtensions[ext.name] !== false; + } + return true; + }) : []; const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => { return ['extension', 'node', 'mark'].includes(extension === null || extension === void 0 ? void 0 : extension.type); }); @@ -17803,6 +18018,12 @@ img.ProseMirror-separator { destroy() { this.emit('destroy'); if (this.view) { + // Cleanup our reference to prevent circular references which caused memory leaks + // @ts-ignore + const dom = this.view.dom; + if (dom && dom.editor) { + delete dom.editor; + } this.view.destroy(); } this.removeAllListeners(); @@ -18018,104 +18239,13 @@ img.ProseMirror-separator { } /** - * The Mark class is used to create custom mark extensions. + * The Node class is used to create custom node extensions. * @see https://tiptap.dev/api/extensions#create-a-new-extension */ - class Mark { + class Node { constructor(config = {}) { - this.type = 'mark'; - this.name = 'mark'; - this.parent = null; - this.child = null; - this.config = { - name: this.name, - defaultOptions: {}, - }; - this.config = { - ...this.config, - ...config, - }; - this.name = this.config.name; - if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) { - console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`); - } - // TODO: remove `addOptions` fallback - this.options = this.config.defaultOptions; - if (this.config.addOptions) { - this.options = callOrReturn(getExtensionField(this, 'addOptions', { - name: this.name, - })); - } - this.storage = callOrReturn(getExtensionField(this, 'addStorage', { - name: this.name, - options: this.options, - })) || {}; - } - static create(config = {}) { - return new Mark(config); - } - configure(options = {}) { - // return a new instance so we can use the same extension - // with different calls of `configure` - const extension = this.extend({ - ...this.config, - addOptions: () => { - return mergeDeep(this.options, options); - }, - }); - // Always preserve the current name - extension.name = this.name; - // Set the parent to be our parent - extension.parent = this.parent; - return extension; - } - extend(extendedConfig = {}) { - const extension = new Mark(extendedConfig); - extension.parent = this; - this.child = extension; - extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name; - if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { - console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`); - } - extension.options = callOrReturn(getExtensionField(extension, 'addOptions', { - name: extension.name, - })); - extension.storage = callOrReturn(getExtensionField(extension, 'addStorage', { - name: extension.name, - options: extension.options, - })); - return extension; - } - static handleExit({ editor, mark }) { - const { tr } = editor.state; - const currentPos = editor.state.selection.$from; - const isAtEnd = currentPos.pos === currentPos.end(); - if (isAtEnd) { - const currentMarks = currentPos.marks(); - const isInMark = !!currentMarks.find(m => (m === null || m === void 0 ? void 0 : m.type.name) === mark.name); - if (!isInMark) { - return false; - } - const removeMark = currentMarks.find(m => (m === null || m === void 0 ? void 0 : m.type.name) === mark.name); - if (removeMark) { - tr.removeStoredMark(removeMark); - } - tr.insertText(' ', currentPos.pos); - editor.view.dispatch(tr); - return true; - } - return false; - } - } - - /** - * The Node class is used to create custom node extensions. - * @see https://tiptap.dev/api/extensions#create-a-new-extension - */ - class Node { - constructor(config = {}) { - this.type = 'node'; - this.name = 'node'; + this.type = 'node'; + this.name = 'node'; this.parent = null; this.child = null; this.config = { @@ -18200,6 +18330,9 @@ img.ProseMirror-separator { this.extension = props.extension; this.node = props.node; this.decorations = props.decorations; + this.innerDecorations = props.innerDecorations; + this.view = props.view; + this.HTMLAttributes = props.HTMLAttributes; this.getPos = props.getPos; this.mount(); } @@ -18238,9 +18371,13 @@ img.ProseMirror-separator { y = handleBox.y - domBox.y + offsetY; } (_g = event.dataTransfer) === null || _g === void 0 ? void 0 : _g.setDragImage(this.dom, x, y); + const pos = this.getPos(); + if (typeof pos !== 'number') { + return; + } // we need to tell ProseMirror that we want to move the whole node // so we create a NodeSelection - const selection = NodeSelection.create(view.state.doc, this.getPos()); + const selection = NodeSelection.create(view.state.doc, pos); const transaction = view.state.tr.setSelection(selection); view.dispatch(transaction); } @@ -18311,6 +18448,11 @@ img.ProseMirror-separator { } return true; } + /** + * Called when a DOM [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) or a selection change happens within the view. + * @return `false` if the editor should re-read the selection or re-parse the range around the mutation + * @return `true` if it can safely be ignored. + */ ignoreMutation(mutation) { if (!this.dom || !this.contentDOM) { return true; @@ -18357,9 +18499,15 @@ img.ProseMirror-separator { } return true; } + /** + * Update the attributes of the prosemirror node. + */ updateAttributes(attributes) { this.editor.commands.command(({ tr }) => { const pos = this.getPos(); + if (typeof pos !== 'number') { + return false; + } tr.setNodeMarkup(pos, undefined, { ...this.node.attrs, ...attributes, @@ -18367,8 +18515,14 @@ img.ProseMirror-separator { return true; }); } + /** + * Delete the node. + */ deleteNode() { const from = this.getPos(); + if (typeof from !== 'number') { + return; + } const to = from + this.node.nodeSize; this.editor.commands.deleteRange({ from, to }); } @@ -21568,9 +21722,9 @@ img.ProseMirror-separator { const started = !prev.active && next.active; const stopped = prev.active && !next.active; const changed = !started && !stopped && prev.query !== next.query; - const handleStart = started; + const handleStart = started || (moved && changed); const handleChange = changed || moved; - const handleExit = stopped; + const handleExit = stopped || (moved && changed); // Cancel when suggestion isn't active if (!handleStart && !handleChange && !handleExit) { return; @@ -22119,7 +22273,7 @@ img.ProseMirror-separator { * This extension allows you to create list items. * @see https://www.tiptap.dev/api/nodes/list-item */ - const ListItem$2 = Node.create({ + const ListItem = Node.create({ name: 'listItem', addOptions() { return { @@ -22154,7 +22308,7 @@ img.ProseMirror-separator { * for the `textColor` and `backgroundColor` extensions. * @see https://www.tiptap.dev/api/marks/text-style */ - const TextStyle$1 = Mark.create({ + const TextStyle = Mark.create({ name: 'textStyle', priority: 101, addOptions() { @@ -22229,7 +22383,7 @@ img.ProseMirror-separator { return { toggleBulletList: () => ({ commands, chain }) => { if (this.options.keepAttributes) { - return chain().toggleList(this.name, this.options.itemTypeName, this.options.keepMarks).updateAttributes(ListItem$2.name, this.editor.getAttributes(TextStyle$1.name)).run(); + return chain().toggleList(this.name, this.options.itemTypeName, this.options.keepMarks).updateAttributes(ListItem.name, this.editor.getAttributes(TextStyle.name)).run(); } return commands.toggleList(this.name, this.options.itemTypeName, this.options.keepMarks); }, @@ -22251,7 +22405,7 @@ img.ProseMirror-separator { type: this.type, keepMarks: this.options.keepMarks, keepAttributes: this.options.keepAttributes, - getAttributes: () => { return this.editor.getAttributes(TextStyle$1.name); }, + getAttributes: () => { return this.editor.getAttributes(TextStyle.name); }, editor: this.editor, }); } @@ -23541,118 +23695,6 @@ img.ProseMirror-separator { }, }); - /** - * This extension allows you to create list items. - * @see https://www.tiptap.dev/api/nodes/list-item - */ - const ListItem$1 = Node.create({ - name: 'listItem', - addOptions() { - return { - HTMLAttributes: {}, - bulletListTypeName: 'bulletList', - orderedListTypeName: 'orderedList', - }; - }, - content: 'paragraph block*', - defining: true, - parseHTML() { - return [ - { - tag: 'li', - }, - ]; - }, - renderHTML({ HTMLAttributes }) { - return ['li', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; - }, - addKeyboardShortcuts() { - return { - Enter: () => this.editor.commands.splitListItem(this.name), - Tab: () => this.editor.commands.sinkListItem(this.name), - 'Shift-Tab': () => this.editor.commands.liftListItem(this.name), - }; - }, - }); - - /** - * This extension allows you to create list items. - * @see https://www.tiptap.dev/api/nodes/list-item - */ - const ListItem = Node.create({ - name: 'listItem', - addOptions() { - return { - HTMLAttributes: {}, - bulletListTypeName: 'bulletList', - orderedListTypeName: 'orderedList', - }; - }, - content: 'paragraph block*', - defining: true, - parseHTML() { - return [ - { - tag: 'li', - }, - ]; - }, - renderHTML({ HTMLAttributes }) { - return ['li', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; - }, - addKeyboardShortcuts() { - return { - Enter: () => this.editor.commands.splitListItem(this.name), - Tab: () => this.editor.commands.sinkListItem(this.name), - 'Shift-Tab': () => this.editor.commands.liftListItem(this.name), - }; - }, - }); - - /** - * This extension allows you to create text styles. It is required by default - * for the `textColor` and `backgroundColor` extensions. - * @see https://www.tiptap.dev/api/marks/text-style - */ - const TextStyle = Mark.create({ - name: 'textStyle', - priority: 101, - addOptions() { - return { - HTMLAttributes: {}, - }; - }, - parseHTML() { - return [ - { - tag: 'span', - getAttrs: element => { - const hasStyles = element.hasAttribute('style'); - if (!hasStyles) { - return false; - } - return {}; - }, - }, - ]; - }, - renderHTML({ HTMLAttributes }) { - return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; - }, - addCommands() { - return { - removeEmptyTextStyle: () => ({ state, commands }) => { - const attributes = getMarkAttributes(state, this.type); - const hasStyles = Object.entries(attributes).some(([, value]) => !!value); - if (hasStyles) { - return true; - } - return commands.unsetMark(this.name); - }, - }; - }, - }); - /** * Matches an ordered list to a 1. on input (or any number followed by a dot). */ @@ -23870,7 +23912,6 @@ img.ProseMirror-separator { Document, History, Placeholder, - // Basic Formats Text, Paragraph, HardBreak.configure({ @@ -23878,18 +23919,17 @@ img.ProseMirror-separator { class: 'smart', }, }), - // Custom Formats Link, - Blockquote, ]; var articleEditorExtensions = __spreadArray(__spreadArray([], baseEditorExtensions, true), [ + Blockquote, Gapcursor, Bold, Strike, CodeBlock, HorizontalRule, OrderedList, - ListItem$1, + ListItem, BulletList, Heading.configure({ levels: [2, 3], @@ -23899,7 +23939,7 @@ img.ProseMirror-separator { FigureEmbed, FigcaptionKit, ], false); - var commentEditorExtensions = __spreadArray([], baseEditorExtensions, true); + var commentEditorExtensions = __spreadArray(__spreadArray([], baseEditorExtensions, true), [Blockquote], false); var momentEditorExtensions = __spreadArray([], baseEditorExtensions, true); var campaignEditorExtensions = [ Document, @@ -27574,6 +27614,10 @@ img.ProseMirror-separator { }, }); + function getDefaultExportFromCjs$1 (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + var shim = {exports: {}}; var useSyncExternalStoreShim_production_min = {}; @@ -27761,6 +27805,80 @@ img.ProseMirror-separator { }); const EditorContent = React.memo(EditorContentWithKey); + var react = function equal(a, b) { + if (a === b) return true; + + if (a && b && typeof a == 'object' && typeof b == 'object') { + if (a.constructor !== b.constructor) return false; + + var length, i, keys; + if (Array.isArray(a)) { + length = a.length; + if (length != b.length) return false; + for (i = length; i-- !== 0;) + if (!equal(a[i], b[i])) return false; + return true; + } + + + if ((a instanceof Map) && (b instanceof Map)) { + if (a.size !== b.size) return false; + for (i of a.entries()) + if (!b.has(i[0])) return false; + for (i of a.entries()) + if (!equal(i[1], b.get(i[0]))) return false; + return true; + } + + if ((a instanceof Set) && (b instanceof Set)) { + if (a.size !== b.size) return false; + for (i of a.entries()) + if (!b.has(i[0])) return false; + return true; + } + + if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { + length = a.length; + if (length != b.length) return false; + for (i = length; i-- !== 0;) + if (a[i] !== b[i]) return false; + return true; + } + + + if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; + if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); + if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); + + keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) return false; + + for (i = length; i-- !== 0;) + if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; + + for (i = length; i-- !== 0;) { + var key = keys[i]; + + if (key === '_owner' && a.$$typeof) { + // React-specific: avoid traversing React elements' _owner. + // _owner contains circular references + // and is not needed when comparing the actual elements (and not their owners) + continue; + } + + if (!equal(a[key], b[key])) return false; + } + + return true; + } + + // true if both NaN, false otherwise + return a!==a && b!==b; + }; + + var deepEqual = /*@__PURE__*/getDefaultExportFromCjs$1(react); + var withSelector = {exports: {}}; var withSelector_production_min = {}; @@ -27858,13 +27976,25 @@ img.ProseMirror-separator { return undefined; } } + /** + * This hook allows you to watch for changes on the editor instance. + * It will allow you to select a part of the editor state and re-render the component when it changes. + * @example + * ```tsx + * const editor = useEditor({...options}) + * const { currentSelection } = useEditorState({ + * editor, + * selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }), + * }) + */ function useEditorState(options) { - const [editorInstance] = React.useState(() => new EditorStateManager(options.editor)); + var _a; + const [editorStateManager] = React.useState(() => new EditorStateManager(options.editor)); // Using the `useSyncExternalStore` hook to sync the editor instance with the component state - const selectedState = withSelectorExports.useSyncExternalStoreWithSelector(editorInstance.subscribe, editorInstance.getSnapshot, editorInstance.getServerSnapshot, options.selector, options.equalityFn); + const selectedState = withSelectorExports.useSyncExternalStoreWithSelector(editorStateManager.subscribe, editorStateManager.getSnapshot, editorStateManager.getServerSnapshot, options.selector, (_a = options.equalityFn) !== null && _a !== void 0 ? _a : deepEqual); React.useEffect(() => { - return editorInstance.watch(options.editor); - }, [options.editor, editorInstance]); + return editorStateManager.watch(options.editor); + }, [options.editor, editorStateManager]); React.useDebugValue(selectedState); return selectedState; } @@ -27950,6 +28080,8 @@ img.ProseMirror-separator { onTransaction: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); }, onUpdate: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); }, onContentError: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onContentError) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); }, + onDrop: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onDrop) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); }, + onPaste: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onPaste) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); }, }; const editor = new Editor(optionsToApply); // no need to keep track of the event listeners, they will be removed when the editor is destroyed @@ -28102,14 +28234,14 @@ img.ProseMirror-separator { * It allows the editor to be accessible across the entire component tree * with `useCurrentEditor`. */ - function EditorProvider({ children, slotAfter, slotBefore, ...editorOptions }) { + function EditorProvider({ children, slotAfter, slotBefore, editorContainerProps = {}, ...editorOptions }) { const editor = useEditor(editorOptions); if (!editor) { return null; } return (React.createElement(EditorContext.Provider, { value: { editor } }, slotBefore, - React.createElement(EditorConsumer, null, ({ editor: currentEditor }) => (React.createElement(EditorContent, { editor: currentEditor }))), + React.createElement(EditorConsumer, null, ({ editor: currentEditor }) => (React.createElement(EditorContent, { editor: currentEditor, ...editorContainerProps }))), children, slotAfter)); } @@ -28140,7 +28272,7 @@ img.ProseMirror-separator { tippyOptions, }); menuEditor.registerPlugin(plugin); - return () => menuEditor.unregisterPlugin(pluginKey); + return () => { menuEditor.unregisterPlugin(pluginKey); }; }, [props.editor, currentEditor, element]); return (React.createElement("div", { ref: setElement, className: props.className, style: { visibility: 'hidden' } }, props.children)); }; @@ -28170,7 +28302,7 @@ img.ProseMirror-separator { shouldShow, }); menuEditor.registerPlugin(plugin); - return () => menuEditor.unregisterPlugin(pluginKey); + return () => { menuEditor.unregisterPlugin(pluginKey); }; }, [ props.editor, currentEditor, @@ -28238,7 +28370,10 @@ img.ProseMirror-separator { * }) */ class ReactRenderer { - constructor(component, { editor, props = {}, as = 'div', className = '', attrs, }) { + /** + * Immediately creates element and renders the provided React component. + */ + constructor(component, { editor, props = {}, as = 'div', className = '', }) { this.ref = null; this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString(); this.component = component; @@ -28249,11 +28384,6 @@ img.ProseMirror-separator { if (className) { this.element.classList.add(...className.split(' ')); } - if (attrs) { - Object.keys(attrs).forEach(key => { - this.element.setAttribute(key, attrs[key]); - }); - } if (this.editor.isInitialized) { // On first render, we need to flush the render synchronously // Renders afterwards can be async, but this fixes a cursor positioning issue @@ -28265,12 +28395,16 @@ img.ProseMirror-separator { this.render(); } } + /** + * Render the React component. + */ render() { var _a; const Component = this.component; const props = this.props; const editor = this.editor; if (isClassComponent(Component) || isForwardRefComponent(Component)) { + // @ts-ignore This is a hack to make the ref work props.ref = (ref) => { this.ref = ref; }; @@ -28278,6 +28412,9 @@ img.ProseMirror-separator { this.reactElement = React.createElement(Component, props); (_a = editor === null || editor === void 0 ? void 0 : editor.contentComponent) === null || _a === void 0 ? void 0 : _a.setRenderer(this.id, this); } + /** + * Re-renders the React component with new props. + */ updateProps(props = {}) { this.props = { ...this.props, @@ -28285,21 +28422,39 @@ img.ProseMirror-separator { }; this.render(); } + /** + * Destroy the React component. + */ destroy() { var _a; const editor = this.editor; (_a = editor === null || editor === void 0 ? void 0 : editor.contentComponent) === null || _a === void 0 ? void 0 : _a.removeRenderer(this.id); } + /** + * Update the attributes of the element that holds the React component. + */ + updateAttributes(attributes) { + Object.keys(attributes).forEach(key => { + this.element.setAttribute(key, attributes[key]); + }); + } } class ReactNodeView extends NodeView { + /** + * Setup the React component. + * Called on initialization. + */ mount() { const props = { editor: this.editor, node: this.node, decorations: this.decorations, + innerDecorations: this.innerDecorations, + view: this.view, selected: false, extension: this.extension, + HTMLAttributes: this.HTMLAttributes, getPos: () => this.getPos(), updateAttributes: (attributes = {}) => this.updateAttributes(attributes), deleteNode: () => this.deleteNode(), @@ -28334,6 +28489,7 @@ img.ProseMirror-separator { this.contentDOMElement = document.createElement(this.node.isInline ? 'span' : 'div'); } if (this.contentDOMElement) { + this.contentDOMElement.dataset.nodeViewContentReact = ''; // For some reason the whiteSpace prop is not inherited properly in Chrome and Safari // With this fix it seems to work fine // See: https://github.com/ueberdosis/tiptap/issues/1197 @@ -28351,9 +28507,13 @@ img.ProseMirror-separator { props, as, className: `node-${this.node.type.name} ${className}`.trim(), - attrs: this.options.attrs, }); + this.updateElementAttributes(); } + /** + * Return the DOM element. + * This is the element that will be used to display the node view. + */ get dom() { var _a; if (this.renderer.element.firstElementChild @@ -28362,15 +28522,27 @@ img.ProseMirror-separator { } return this.renderer.element; } + /** + * Return the content DOM element. + * This is the element that will be used to display the rich-text content of the node. + */ get contentDOM() { if (this.node.isLeaf) { return null; } return this.contentDOMElement; } + /** + * On editor selection update, check if the node is selected. + * If it is, call `selectNode`, otherwise call `deselectNode`. + */ handleSelectionUpdate() { const { from, to } = this.editor.state.selection; - if (from <= this.getPos() && to >= this.getPos() + this.node.nodeSize) { + const pos = this.getPos(); + if (typeof pos !== 'number') { + return; + } + if (from <= pos && to >= pos + this.node.nodeSize) { if (this.renderer.props.selected) { return; } @@ -28383,9 +28555,16 @@ img.ProseMirror-separator { this.deselectNode(); } } - update(node, decorations) { - const updateProps = (props) => { + /** + * On update, update the React component. + * To prevent unnecessary updates, the `update` option can be used. + */ + update(node, decorations, innerDecorations) { + const rerenderComponent = (props) => { this.renderer.updateProps(props); + if (typeof this.options.attrs === 'function') { + this.updateElementAttributes(); + } }; if (node.type !== this.node.type) { return false; @@ -28393,44 +28572,83 @@ img.ProseMirror-separator { if (typeof this.options.update === 'function') { const oldNode = this.node; const oldDecorations = this.decorations; + const oldInnerDecorations = this.innerDecorations; this.node = node; this.decorations = decorations; + this.innerDecorations = innerDecorations; return this.options.update({ oldNode, oldDecorations, newNode: node, newDecorations: decorations, - updateProps: () => updateProps({ node, decorations }), + oldInnerDecorations, + innerDecorations, + updateProps: () => rerenderComponent({ node, decorations, innerDecorations }), }); } - if (node === this.node && this.decorations === decorations) { + if (node === this.node + && this.decorations === decorations + && this.innerDecorations === innerDecorations) { return true; } this.node = node; this.decorations = decorations; - updateProps({ node, decorations }); + this.innerDecorations = innerDecorations; + rerenderComponent({ node, decorations, innerDecorations }); return true; } + /** + * Select the node. + * Add the `selected` prop and the `ProseMirror-selectednode` class. + */ selectNode() { this.renderer.updateProps({ selected: true, }); this.renderer.element.classList.add('ProseMirror-selectednode'); } + /** + * Deselect the node. + * Remove the `selected` prop and the `ProseMirror-selectednode` class. + */ deselectNode() { this.renderer.updateProps({ selected: false, }); this.renderer.element.classList.remove('ProseMirror-selectednode'); } + /** + * Destroy the React component instance. + */ destroy() { this.renderer.destroy(); this.editor.off('selectionUpdate', this.handleSelectionUpdate); this.contentDOMElement = null; } + /** + * Update the attributes of the top-level element that holds the React component. + * Applying the attributes defined in the `attrs` option. + */ + updateElementAttributes() { + if (this.options.attrs) { + let attrsObj = {}; + if (typeof this.options.attrs === 'function') { + const extensionAttributes = this.editor.extensionManager.attributes; + const HTMLAttributes = getRenderedAttributes(this.node, extensionAttributes); + attrsObj = this.options.attrs({ node: this.node, HTMLAttributes }); + } + else { + attrsObj = this.options.attrs; + } + this.renderer.updateAttributes(attrsObj); + } + } } + /** + * Create a React node view renderer. + */ function ReactNodeViewRenderer(component, options) { - return (props) => { + return props => { // try to get the parent component // this is important for vue devtools to show the component hierarchy correctly // maybe it’s `undefined` because isn’t rendered yet @@ -74497,6 +74715,7 @@ img.ProseMirror-separator { exports.PasteRule = PasteRule; exports.Placeholder = Placeholder; exports.PureEditorContent = PureEditorContent; + exports.ReactNodeView = ReactNodeView; exports.ReactNodeViewContext = ReactNodeViewContext; exports.ReactNodeViewRenderer = ReactNodeViewRenderer; exports.ReactRenderer = ReactRenderer;