gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
+383
@@ -0,0 +1,383 @@
|
||||
import {Fragment, DOMParser, TagParseRule, Node, Mark, ResolvedPos} from "prosemirror-model"
|
||||
import {TextSelection, Transaction} from "prosemirror-state"
|
||||
|
||||
import {selectionBetween, selectionFromDOM, selectionToDOM} from "./selection"
|
||||
import {selectionCollapsed, keyEvent, DOMNode} from "./dom"
|
||||
import * as browser from "./browser"
|
||||
import {EditorView} from "./index"
|
||||
|
||||
// Note that all referencing and parsing is done with the
|
||||
// start-of-operation selection and document, since that's the one
|
||||
// that the DOM represents. If any changes came in in the meantime,
|
||||
// the modification is mapped over those before it is applied, in
|
||||
// readDOMChange.
|
||||
|
||||
function parseBetween(view: EditorView, from_: number, to_: number) {
|
||||
let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_)
|
||||
|
||||
let domSel = view.domSelectionRange()
|
||||
let find: {node: DOMNode, offset: number, pos?: number}[] | undefined
|
||||
let anchor = domSel.anchorNode
|
||||
if (anchor && view.dom.contains(anchor.nodeType == 1 ? anchor : anchor.parentNode)) {
|
||||
find = [{node: anchor, offset: domSel.anchorOffset}]
|
||||
if (!selectionCollapsed(domSel))
|
||||
find.push({node: domSel.focusNode!, offset: domSel.focusOffset})
|
||||
}
|
||||
// Work around issue in Chrome where backspacing sometimes replaces
|
||||
// the deleted content with a random BR node (issues #799, #831)
|
||||
if (browser.chrome && view.input.lastKeyCode === 8) {
|
||||
for (let off = toOffset; off > fromOffset; off--) {
|
||||
let node = parent.childNodes[off - 1], desc = node.pmViewDesc
|
||||
if (node.nodeName == "BR" && !desc) { toOffset = off; break }
|
||||
if (!desc || desc.size) break
|
||||
}
|
||||
}
|
||||
let startDoc = view.state.doc
|
||||
let parser = view.someProp("domParser") || DOMParser.fromSchema(view.state.schema)
|
||||
let $from = startDoc.resolve(from)
|
||||
|
||||
let sel = null, doc = parser.parse(parent, {
|
||||
topNode: $from.parent,
|
||||
topMatch: $from.parent.contentMatchAt($from.index()),
|
||||
topOpen: true,
|
||||
from: fromOffset,
|
||||
to: toOffset,
|
||||
preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
|
||||
findPositions: find,
|
||||
ruleFromNode,
|
||||
context: $from
|
||||
})
|
||||
if (find && find[0].pos != null) {
|
||||
let anchor = find[0].pos, head = find[1] && find[1].pos
|
||||
if (head == null) head = anchor
|
||||
sel = {anchor: anchor + from, head: head + from}
|
||||
}
|
||||
return {doc, sel, from, to}
|
||||
}
|
||||
|
||||
function ruleFromNode(dom: DOMNode): Omit<TagParseRule, "tag"> | null {
|
||||
let desc = dom.pmViewDesc
|
||||
if (desc) {
|
||||
return desc.parseRule()
|
||||
} else if (dom.nodeName == "BR" && dom.parentNode) {
|
||||
// Safari replaces the list item or table cell with a BR
|
||||
// directly in the list node (?!) if you delete the last
|
||||
// character in a list item or table cell (#708, #862)
|
||||
if (browser.safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
|
||||
let skip = document.createElement("div")
|
||||
skip.appendChild(document.createElement("li"))
|
||||
return {skip} as any
|
||||
} else if (dom.parentNode.lastChild == dom || browser.safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
|
||||
return {ignore: true}
|
||||
}
|
||||
} else if (dom.nodeName == "IMG" && (dom as HTMLElement).getAttribute("mark-placeholder")) {
|
||||
return {ignore: true}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i
|
||||
|
||||
export function readDOMChange(view: EditorView, from: number, to: number, typeOver: boolean, addedNodes: readonly DOMNode[]) {
|
||||
let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0)
|
||||
view.input.compositionPendingChanges = 0
|
||||
|
||||
if (from < 0) {
|
||||
let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null
|
||||
let newSel = selectionFromDOM(view, origin)
|
||||
if (newSel && !view.state.selection.eq(newSel)) {
|
||||
if (browser.chrome && browser.android &&
|
||||
view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime &&
|
||||
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter"))))
|
||||
return
|
||||
let tr = view.state.tr.setSelection(newSel)
|
||||
if (origin == "pointer") tr.setMeta("pointer", true)
|
||||
else if (origin == "key") tr.scrollIntoView()
|
||||
if (compositionID) tr.setMeta("composition", compositionID)
|
||||
view.dispatch(tr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let $before = view.state.doc.resolve(from)
|
||||
let shared = $before.sharedDepth(to)
|
||||
from = $before.before(shared + 1)
|
||||
to = view.state.doc.resolve(to).after(shared + 1)
|
||||
|
||||
let sel = view.state.selection
|
||||
let parse = parseBetween(view, from, to)
|
||||
|
||||
let doc = view.state.doc, compare = doc.slice(parse.from, parse.to)
|
||||
let preferredPos, preferredSide: "start" | "end"
|
||||
// Prefer anchoring to end when Backspace is pressed
|
||||
if (view.input.lastKeyCode === 8 && Date.now() - 100 < view.input.lastKeyCodeTime) {
|
||||
preferredPos = view.state.selection.to
|
||||
preferredSide = "end"
|
||||
} else {
|
||||
preferredPos = view.state.selection.from
|
||||
preferredSide = "start"
|
||||
}
|
||||
view.input.lastKeyCode = null
|
||||
|
||||
let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide)
|
||||
if (change) view.input.domChangeCount++
|
||||
if ((browser.ios && view.input.lastIOSEnter > Date.now() - 225 || browser.android) &&
|
||||
addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) &&
|
||||
(!change || change.endA >= change.endB) &&
|
||||
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
|
||||
view.input.lastIOSEnter = 0
|
||||
return
|
||||
}
|
||||
if (!change) {
|
||||
if (typeOver && sel instanceof TextSelection && !sel.empty && sel.$head.sameParent(sel.$anchor) &&
|
||||
!view.composing && !(parse.sel && parse.sel.anchor != parse.sel.head)) {
|
||||
change = {start: sel.from, endA: sel.to, endB: sel.to}
|
||||
} else {
|
||||
if (parse.sel) {
|
||||
let sel = resolveSelection(view, view.state.doc, parse.sel)
|
||||
if (sel && !sel.eq(view.state.selection)) {
|
||||
let tr = view.state.tr.setSelection(sel)
|
||||
if (compositionID) tr.setMeta("composition", compositionID)
|
||||
view.dispatch(tr)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (view.state.selection.from < view.state.selection.to &&
|
||||
change.start == change.endB &&
|
||||
view.state.selection instanceof TextSelection) {
|
||||
if (change.start > view.state.selection.from && change.start <= view.state.selection.from + 2 &&
|
||||
view.state.selection.from >= parse.from) {
|
||||
change.start = view.state.selection.from
|
||||
} else if (change.endA < view.state.selection.to && change.endA >= view.state.selection.to - 2 &&
|
||||
view.state.selection.to <= parse.to) {
|
||||
change.endB += (view.state.selection.to - change.endA)
|
||||
change.endA = view.state.selection.to
|
||||
}
|
||||
}
|
||||
|
||||
// IE11 will insert a non-breaking space _ahead_ of the space after
|
||||
// the cursor space when adding a space before another space. When
|
||||
// that happened, adjust the change to cover the space instead.
|
||||
if (browser.ie && browser.ie_version <= 11 && change.endB == change.start + 1 &&
|
||||
change.endA == change.start && change.start > parse.from &&
|
||||
parse.doc.textBetween(change.start - parse.from - 1, change.start - parse.from + 1) == " \u00a0") {
|
||||
change.start--
|
||||
change.endA--
|
||||
change.endB--
|
||||
}
|
||||
|
||||
let $from = parse.doc.resolveNoCache(change.start - parse.from)
|
||||
let $to = parse.doc.resolveNoCache(change.endB - parse.from)
|
||||
let $fromA = doc.resolve(change.start)
|
||||
let inlineChange = $from.sameParent($to) && $from.parent.inlineContent && $fromA.end() >= change.endA
|
||||
// If this looks like the effect of pressing Enter (or was recorded
|
||||
// as being an iOS enter press), just dispatch an Enter key instead.
|
||||
if (((browser.ios && view.input.lastIOSEnter > Date.now() - 225 &&
|
||||
(!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) ||
|
||||
(!inlineChange && $from.pos < parse.doc.content.size &&
|
||||
(!$from.sameParent($to) || !$from.parent.inlineContent) &&
|
||||
$from.pos < $to.pos && !/\S/.test(parse.doc.textBetween($from.pos, $to.pos, "", "")))) &&
|
||||
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
|
||||
view.input.lastIOSEnter = 0
|
||||
return
|
||||
}
|
||||
// Same for backspace
|
||||
if (view.state.selection.anchor > change.start &&
|
||||
looksLikeBackspace(doc, change.start, change.endA, $from, $to) &&
|
||||
view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) {
|
||||
if (browser.android && browser.chrome) view.domObserver.suppressSelectionUpdates() // #820
|
||||
return
|
||||
}
|
||||
|
||||
// Chrome will occasionally, during composition, delete the
|
||||
// entire composition and then immediately insert it again. This is
|
||||
// used to detect that situation.
|
||||
if (browser.chrome && change.endB == change.start)
|
||||
view.input.lastChromeDelete = Date.now()
|
||||
|
||||
// This tries to detect Android virtual keyboard
|
||||
// enter-and-pick-suggestion action. That sometimes (see issue
|
||||
// #1059) first fires a DOM mutation, before moving the selection to
|
||||
// the newly created block. And then, because ProseMirror cleans up
|
||||
// the DOM selection, it gives up moving the selection entirely,
|
||||
// leaving the cursor in the wrong place. When that happens, we drop
|
||||
// the new paragraph from the initial change, and fire a simulated
|
||||
// enter key afterwards.
|
||||
if (browser.android && !inlineChange && $from.start() != $to.start() && $to.parentOffset == 0 && $from.depth == $to.depth &&
|
||||
parse.sel && parse.sel.anchor == parse.sel.head && parse.sel.head == change.endA) {
|
||||
change.endB -= 2
|
||||
$to = parse.doc.resolveNoCache(change.endB - parse.from)
|
||||
setTimeout(() => {
|
||||
view.someProp("handleKeyDown", function (f) { return f(view, keyEvent(13, "Enter")); })
|
||||
}, 20)
|
||||
}
|
||||
|
||||
let chFrom = change.start, chTo = change.endA
|
||||
|
||||
let mkTr = (base?: Transaction) => {
|
||||
let tr = base || view.state.tr.replace(chFrom, chTo, parse.doc.slice(change!.start - parse.from,
|
||||
change!.endB - parse.from))
|
||||
if (parse.sel) {
|
||||
let sel = resolveSelection(view, tr.doc, parse.sel)
|
||||
// Chrome will sometimes, during composition, report the
|
||||
// selection in the wrong place. If it looks like that is
|
||||
// happening, don't update the selection.
|
||||
// Edge just doesn't move the cursor forward when you start typing
|
||||
// in an empty block or between br nodes.
|
||||
if (sel && !(browser.chrome && view.composing && sel.empty &&
|
||||
(change!.start != change!.endB || view.input.lastChromeDelete < Date.now() - 100) &&
|
||||
(sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
|
||||
browser.ie && sel.empty && sel.head == chFrom))
|
||||
tr.setSelection(sel)
|
||||
}
|
||||
if (compositionID) tr.setMeta("composition", compositionID)
|
||||
return tr.scrollIntoView()
|
||||
}
|
||||
|
||||
let markChange
|
||||
if (inlineChange) {
|
||||
if ($from.pos == $to.pos) { // Deletion
|
||||
// IE11 sometimes weirdly moves the DOM selection around after
|
||||
// backspacing out the first element in a textblock
|
||||
if (browser.ie && browser.ie_version <= 11 && $from.parentOffset == 0) {
|
||||
view.domObserver.suppressSelectionUpdates()
|
||||
setTimeout(() => selectionToDOM(view), 20)
|
||||
}
|
||||
let tr = mkTr(view.state.tr.delete(chFrom, chTo))
|
||||
let marks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA))
|
||||
if (marks) tr.ensureMarks(marks)
|
||||
view.dispatch(tr)
|
||||
} else if ( // Adding or removing a mark
|
||||
change.endA == change.endB &&
|
||||
(markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset),
|
||||
$fromA.parent.content.cut($fromA.parentOffset, change.endA - $fromA.start())))
|
||||
) {
|
||||
let tr = mkTr(view.state.tr)
|
||||
if (markChange.type == "add") tr.addMark(chFrom, chTo, markChange.mark)
|
||||
else tr.removeMark(chFrom, chTo, markChange.mark)
|
||||
view.dispatch(tr)
|
||||
} else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) {
|
||||
// Both positions in the same text node -- simply insert text
|
||||
let text = $from.parent.textBetween($from.parentOffset, $to.parentOffset)
|
||||
let deflt = () => mkTr(view.state.tr.insertText(text, chFrom, chTo))
|
||||
if (!view.someProp("handleTextInput", f => f(view, chFrom, chTo, text, deflt)))
|
||||
view.dispatch(deflt())
|
||||
} else {
|
||||
view.dispatch(mkTr())
|
||||
}
|
||||
} else {
|
||||
view.dispatch(mkTr())
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSelection(view: EditorView, doc: Node, parsedSel: {anchor: number, head: number}) {
|
||||
if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size) return null
|
||||
return selectionBetween(view, doc.resolve(parsedSel.anchor), doc.resolve(parsedSel.head))
|
||||
}
|
||||
|
||||
// Given two same-length, non-empty fragments of inline content,
|
||||
// determine whether the first could be created from the second by
|
||||
// removing or adding a single mark type.
|
||||
function isMarkChange(cur: Fragment, prev: Fragment) {
|
||||
let curMarks = cur.firstChild!.marks, prevMarks = prev.firstChild!.marks
|
||||
let added = curMarks, removed = prevMarks, type, mark: Mark | undefined, update
|
||||
for (let i = 0; i < prevMarks.length; i++) added = prevMarks[i].removeFromSet(added)
|
||||
for (let i = 0; i < curMarks.length; i++) removed = curMarks[i].removeFromSet(removed)
|
||||
if (added.length == 1 && removed.length == 0) {
|
||||
mark = added[0]
|
||||
type = "add"
|
||||
update = (node: Node) => node.mark(mark!.addToSet(node.marks))
|
||||
} else if (added.length == 0 && removed.length == 1) {
|
||||
mark = removed[0]
|
||||
type = "remove"
|
||||
update = (node: Node) => node.mark(mark!.removeFromSet(node.marks))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
let updated = []
|
||||
for (let i = 0; i < prev.childCount; i++) updated.push(update(prev.child(i)))
|
||||
if (Fragment.from(updated).eq(cur)) return {mark, type}
|
||||
}
|
||||
|
||||
function looksLikeBackspace(old: Node, start: number, end: number, $newStart: ResolvedPos, $newEnd: ResolvedPos) {
|
||||
if (// The content must have shrunk
|
||||
end - start <= $newEnd.pos - $newStart.pos ||
|
||||
// newEnd must point directly at or after the end of the block that newStart points into
|
||||
skipClosingAndOpening($newStart, true, false) < $newEnd.pos)
|
||||
return false
|
||||
|
||||
let $start = old.resolve(start)
|
||||
|
||||
// Handle the case where, rather than joining blocks, the change just removed an entire block
|
||||
if (!$newStart.parent.isTextblock) {
|
||||
let after = $start.nodeAfter
|
||||
return after != null && end == start + after.nodeSize
|
||||
}
|
||||
|
||||
// Start must be at the end of a block
|
||||
if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock)
|
||||
return false
|
||||
let $next = old.resolve(skipClosingAndOpening($start, true, true))
|
||||
// The next textblock must start before end and end near it
|
||||
if (!$next.parent.isTextblock || $next.pos > end ||
|
||||
skipClosingAndOpening($next, true, false) < end)
|
||||
return false
|
||||
|
||||
// The fragments after the join point must match
|
||||
return $newStart.parent.content.cut($newStart.parentOffset).eq($next.parent.content)
|
||||
}
|
||||
|
||||
function skipClosingAndOpening($pos: ResolvedPos, fromEnd: boolean, mayOpen: boolean) {
|
||||
let depth = $pos.depth, end = fromEnd ? $pos.end() : $pos.pos
|
||||
while (depth > 0 && (fromEnd || $pos.indexAfter(depth) == $pos.node(depth).childCount)) {
|
||||
depth--
|
||||
end++
|
||||
fromEnd = false
|
||||
}
|
||||
if (mayOpen) {
|
||||
let next = $pos.node(depth).maybeChild($pos.indexAfter(depth))
|
||||
while (next && !next.isLeaf) {
|
||||
next = next.firstChild
|
||||
end++
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
function findDiff(a: Fragment, b: Fragment, pos: number, preferredPos: number, preferredSide: "start" | "end") {
|
||||
let start = a.findDiffStart(b, pos)
|
||||
if (start == null) return null
|
||||
let {a: endA, b: endB} = a.findDiffEnd(b, pos + a.size, pos + b.size)!
|
||||
if (preferredSide == "end") {
|
||||
let adjust = Math.max(0, start - Math.min(endA, endB))
|
||||
preferredPos -= endA + adjust - start
|
||||
}
|
||||
if (endA < start && a.size < b.size) {
|
||||
let move = preferredPos <= start && preferredPos >= endA ? start - preferredPos : 0
|
||||
start -= move
|
||||
if (start && start < b.size && isSurrogatePair(b.textBetween(start - 1, start + 1)))
|
||||
start += move ? 1 : -1
|
||||
endB = start + (endB - endA)
|
||||
endA = start
|
||||
} else if (endB < start) {
|
||||
let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0
|
||||
start -= move
|
||||
if (start && start < a.size && isSurrogatePair(a.textBetween(start - 1, start + 1)))
|
||||
start += move ? 1 : -1
|
||||
endA = start + (endA - endB)
|
||||
endB = start
|
||||
}
|
||||
return {start, endA, endB}
|
||||
}
|
||||
|
||||
function isSurrogatePair(str: string) {
|
||||
if (str.length != 2) return false
|
||||
let a = str.charCodeAt(0), b = str.charCodeAt(1)
|
||||
return a >= 0xDC00 && a <= 0xDFFF && b >= 0xD800 && b <= 0xDBFF
|
||||
}
|
||||
Reference in New Issue
Block a user