viewdesc.ts (63650B)
1 import {DOMSerializer, Fragment, Mark, Node, TagParseRule} from "prosemirror-model" 2 import {TextSelection} from "prosemirror-state" 3 4 import {domIndex, isEquivalentPosition, DOMNode} from "./dom" 5 import * as browser from "./browser" 6 import {Decoration, DecorationSource, WidgetConstructor, WidgetType, NodeType} from "./decoration" 7 import {EditorView} from "./index" 8 9 declare global { 10 interface Node { 11 /// @internal 12 pmViewDesc?: ViewDesc 13 } 14 } 15 16 /// A ViewMutationRecord represents a DOM 17 /// [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) 18 /// or a selection change happens within the view. When the change is 19 /// a selection change, the record will have a `type` property of 20 /// `"selection"` (which doesn't occur for native mutation records). 21 export type ViewMutationRecord = MutationRecord | { type: "selection", target: DOMNode } 22 23 /// By default, document nodes are rendered using the result of the 24 /// [`toDOM`](#model.NodeSpec.toDOM) method of their spec, and managed 25 /// entirely by the editor. For some use cases, such as embedded 26 /// node-specific editing interfaces, you want more control over 27 /// the behavior of a node's in-editor representation, and need to 28 /// [define](#view.EditorProps.nodeViews) a custom node view. 29 /// 30 /// Objects returned as node views must conform to this interface. 31 export interface NodeView { 32 /// The outer DOM node that represents the document node. 33 dom: DOMNode 34 35 /// The DOM node that should hold the node's content. Only meaningful 36 /// if the node view also defines a `dom` property and if its node 37 /// type is not a leaf node type. When this is present, ProseMirror 38 /// will take care of rendering the node's children into it. When it 39 /// is not present, the node view itself is responsible for rendering 40 /// (or deciding not to render) its child nodes. 41 contentDOM?: HTMLElement | null 42 43 /// When given, this will be called when the view is updating 44 /// itself. It will be given a node, an array of active decorations 45 /// around the node (which are automatically drawn, and the node 46 /// view may ignore if it isn't interested in them), and a 47 /// [decoration source](#view.DecorationSource) that represents any 48 /// decorations that apply to the content of the node (which again 49 /// may be ignored). It should return true if it was able to update 50 /// to that node, and false otherwise. If the node view has a 51 /// `contentDOM` property (or no `dom` property), updating its child 52 /// nodes will be handled by ProseMirror. 53 update?: (node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource) => boolean 54 55 /// By default, `update` will only be called when a node of the same 56 /// node type appears in this view's position. When you set this to 57 /// true, it will be called for any node, making it possible to have 58 /// a node view that representsmultiple types of nodes. You will 59 /// need to check the type of the nodes you get in `update` and 60 /// return `false` for types you cannot handle. 61 multiType?: boolean 62 63 /// Can be used to override the way the node's selected status (as a 64 /// node selection) is displayed. 65 selectNode?: () => void 66 67 /// When defining a `selectNode` method, you should also provide a 68 /// `deselectNode` method to remove the effect again. 69 deselectNode?: () => void 70 71 /// This will be called to handle setting the selection inside the 72 /// node. The `anchor` and `head` positions are relative to the start 73 /// of the node. By default, a DOM selection will be created between 74 /// the DOM positions corresponding to those positions, but if you 75 /// override it you can do something else. 76 setSelection?: (anchor: number, head: number, root: Document | ShadowRoot) => void 77 78 /// Can be used to prevent the editor view from trying to handle some 79 /// or all DOM events that bubble up from the node view. Events for 80 /// which this returns true are not handled by the editor. 81 stopEvent?: (event: Event) => boolean 82 83 /// Called when a [mutation](#view.ViewMutationRecord) happens within the 84 /// view. Return false if the editor should re-read the selection or re-parse 85 /// the range around the mutation, true if it can safely be ignored. 86 ignoreMutation?: (mutation: ViewMutationRecord) => boolean 87 88 /// Called when the node view is removed from the editor or the whole 89 /// editor is destroyed. 90 destroy?: () => void 91 } 92 93 /// By default, document marks are rendered using the result of the 94 /// [`toDOM`](#model.MarkSpec.toDOM) method of their spec, and managed entirely 95 /// by the editor. For some use cases, you want more control over the behavior 96 /// of a mark's in-editor representation, and need to 97 /// [define](#view.EditorProps.markViews) a custom mark view. 98 /// 99 /// Objects returned as mark views must conform to this interface. 100 export interface MarkView { 101 /// The outer DOM node that represents the document node. 102 dom: DOMNode 103 104 /// The DOM node that should hold the mark's content. When this is not 105 /// present, the `dom` property is used as the content DOM. 106 contentDOM?: HTMLElement | null 107 108 /// Called when a [mutation](#view.ViewMutationRecord) happens within the 109 /// view. Return false if the editor should re-read the selection or re-parse 110 /// the range around the mutation, true if it can safely be ignored. 111 ignoreMutation?: (mutation: ViewMutationRecord) => boolean 112 113 114 /// Called when the mark view is removed from the editor or the whole 115 /// editor is destroyed. 116 destroy?: () => void 117 } 118 119 // View descriptions are data structures that describe the DOM that is 120 // used to represent the editor's content. They are used for: 121 // 122 // - Incremental redrawing when the document changes 123 // 124 // - Figuring out what part of the document a given DOM position 125 // corresponds to 126 // 127 // - Wiring in custom implementations of the editing interface for a 128 // given node 129 // 130 // They form a doubly-linked mutable tree, starting at `view.docView`. 131 132 const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3 133 134 // Superclass for the various kinds of descriptions. Defines their 135 // basic structure and shared methods. 136 export class ViewDesc { 137 dirty = NOT_DIRTY 138 declare node: Node | null 139 140 constructor( 141 public parent: ViewDesc | undefined, 142 public children: ViewDesc[], 143 public dom: DOMNode, 144 // This is the node that holds the child views. It may be null for 145 // descs that don't have children. 146 public contentDOM: HTMLElement | null 147 ) { 148 // An expando property on the DOM node provides a link back to its 149 // description. 150 dom.pmViewDesc = this 151 } 152 153 // Used to check whether a given description corresponds to a 154 // widget/mark/node. 155 matchesWidget(widget: Decoration) { return false } 156 matchesMark(mark: Mark) { return false } 157 matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource) { return false } 158 matchesHack(nodeName: string) { return false } 159 160 // When parsing in-editor content (in domchange.js), we allow 161 // descriptions to determine the parse rules that should be used to 162 // parse them. 163 parseRule(): Omit<TagParseRule, "tag"> | null { return null } 164 165 // Used by the editor's event handler to ignore events that come 166 // from certain descs. 167 stopEvent(event: Event) { return false } 168 169 // The size of the content represented by this desc. 170 get size() { 171 let size = 0 172 for (let i = 0; i < this.children.length; i++) size += this.children[i].size 173 return size 174 } 175 176 // For block nodes, this represents the space taken up by their 177 // start/end tokens. 178 get border() { return 0 } 179 180 destroy() { 181 this.parent = undefined 182 if (this.dom.pmViewDesc == this) this.dom.pmViewDesc = undefined 183 for (let i = 0; i < this.children.length; i++) 184 this.children[i].destroy() 185 } 186 187 posBeforeChild(child: ViewDesc): number { 188 for (let i = 0, pos = this.posAtStart;; i++) { 189 let cur = this.children[i] 190 if (cur == child) return pos 191 pos += cur.size 192 } 193 } 194 195 get posBefore() { 196 return this.parent!.posBeforeChild(this) 197 } 198 199 get posAtStart() { 200 return this.parent ? this.parent.posBeforeChild(this) + this.border : 0 201 } 202 203 get posAfter() { 204 return this.posBefore + this.size 205 } 206 207 get posAtEnd() { 208 return this.posAtStart + this.size - 2 * this.border 209 } 210 211 localPosFromDOM(dom: DOMNode, offset: number, bias: number): number { 212 // If the DOM position is in the content, use the child desc after 213 // it to figure out a position. 214 if (this.contentDOM && this.contentDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode)) { 215 if (bias < 0) { 216 let domBefore, desc: ViewDesc | undefined 217 if (dom == this.contentDOM) { 218 domBefore = dom.childNodes[offset - 1] 219 } else { 220 while (dom.parentNode != this.contentDOM) dom = dom.parentNode! 221 domBefore = dom.previousSibling 222 } 223 while (domBefore && !((desc = domBefore.pmViewDesc) && desc.parent == this)) domBefore = domBefore.previousSibling 224 return domBefore ? this.posBeforeChild(desc!) + desc!.size : this.posAtStart 225 } else { 226 let domAfter, desc: ViewDesc | undefined 227 if (dom == this.contentDOM) { 228 domAfter = dom.childNodes[offset] 229 } else { 230 while (dom.parentNode != this.contentDOM) dom = dom.parentNode! 231 domAfter = dom.nextSibling 232 } 233 while (domAfter && !((desc = domAfter.pmViewDesc) && desc.parent == this)) domAfter = domAfter.nextSibling 234 return domAfter ? this.posBeforeChild(desc!) : this.posAtEnd 235 } 236 } 237 // Otherwise, use various heuristics, falling back on the bias 238 // parameter, to determine whether to return the position at the 239 // start or at the end of this view desc. 240 let atEnd 241 if (dom == this.dom && this.contentDOM) { 242 atEnd = offset > domIndex(this.contentDOM) 243 } else if (this.contentDOM && this.contentDOM != this.dom && this.dom.contains(this.contentDOM)) { 244 atEnd = dom.compareDocumentPosition(this.contentDOM) & 2 245 } else if (this.dom.firstChild) { 246 if (offset == 0) for (let search = dom;; search = search.parentNode!) { 247 if (search == this.dom) { atEnd = false; break } 248 if (search.previousSibling) break 249 } 250 if (atEnd == null && offset == dom.childNodes.length) for (let search = dom;; search = search.parentNode!) { 251 if (search == this.dom) { atEnd = true; break } 252 if (search.nextSibling) break 253 } 254 } 255 return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart 256 } 257 258 // Scan up the dom finding the first desc that is a descendant of 259 // this one. 260 nearestDesc(dom: DOMNode): ViewDesc | undefined 261 nearestDesc(dom: DOMNode, onlyNodes: true): NodeViewDesc | undefined 262 nearestDesc(dom: DOMNode, onlyNodes: boolean = false) { 263 for (let first = true, cur: DOMNode | null = dom; cur; cur = cur.parentNode) { 264 let desc = this.getDesc(cur), nodeDOM 265 if (desc && (!onlyNodes || desc.node)) { 266 // If dom is outside of this desc's nodeDOM, don't count it. 267 if (first && (nodeDOM = (desc as NodeViewDesc).nodeDOM) && 268 !(nodeDOM.nodeType == 1 ? nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : nodeDOM == dom)) 269 first = false 270 else 271 return desc 272 } 273 } 274 } 275 276 getDesc(dom: DOMNode) { 277 let desc = dom.pmViewDesc 278 for (let cur: ViewDesc | undefined = desc; cur; cur = cur.parent) if (cur == this) return desc 279 } 280 281 posFromDOM(dom: DOMNode, offset: number, bias: number) { 282 for (let scan: DOMNode | null = dom; scan; scan = scan.parentNode) { 283 let desc = this.getDesc(scan) 284 if (desc) return desc.localPosFromDOM(dom, offset, bias) 285 } 286 return -1 287 } 288 289 // Find the desc for the node after the given pos, if any. (When a 290 // parent node overrode rendering, there might not be one.) 291 descAt(pos: number): ViewDesc | undefined { 292 for (let i = 0, offset = 0; i < this.children.length; i++) { 293 let child = this.children[i], end = offset + child.size 294 if (offset == pos && end != offset) { 295 while (!child.border && child.children.length) { 296 for (let i = 0; i < child.children.length; i++) { 297 let inner = child.children[i] 298 if (inner.size) { child = inner; break } 299 } 300 } 301 return child 302 } 303 if (pos < end) return child.descAt(pos - offset - child.border) 304 offset = end 305 } 306 } 307 308 domFromPos(pos: number, side: number): {node: DOMNode, offset: number, atom?: number} { 309 if (!this.contentDOM) return {node: this.dom, offset: 0, atom: pos + 1} 310 // First find the position in the child array 311 let i = 0, offset = 0 312 for (let curPos = 0; i < this.children.length; i++) { 313 let child = this.children[i], end = curPos + child.size 314 if (end > pos || child instanceof TrailingHackViewDesc) { offset = pos - curPos; break } 315 curPos = end 316 } 317 // If this points into the middle of a child, call through 318 if (offset) return this.children[i].domFromPos(offset - this.children[i].border, side) 319 // Go back if there were any zero-length widgets with side >= 0 before this point 320 for (let prev; i && !(prev = this.children[i - 1]).size && prev instanceof WidgetViewDesc && prev.side >= 0; i--) {} 321 // Scan towards the first useable node 322 if (side <= 0) { 323 let prev, enter = true 324 for (;; i--, enter = false) { 325 prev = i ? this.children[i - 1] : null 326 if (!prev || prev.dom.parentNode == this.contentDOM) break 327 } 328 if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side) 329 return {node: this.contentDOM, offset: prev ? domIndex(prev.dom) + 1 : 0} 330 } else { 331 let next, enter = true 332 for (;; i++, enter = false) { 333 next = i < this.children.length ? this.children[i] : null 334 if (!next || next.dom.parentNode == this.contentDOM) break 335 } 336 if (next && enter && !next.border && !next.domAtom) return next.domFromPos(0, side) 337 return {node: this.contentDOM, offset: next ? domIndex(next.dom) : this.contentDOM.childNodes.length} 338 } 339 } 340 341 // Used to find a DOM range in a single parent for a given changed 342 // range. 343 parseRange( 344 from: number, to: number, base = 0 345 ): {node: DOMNode, from: number, to: number, fromOffset: number, toOffset: number} { 346 if (this.children.length == 0) 347 return {node: this.contentDOM!, from, to, fromOffset: 0, toOffset: this.contentDOM!.childNodes.length} 348 349 let fromOffset = -1, toOffset = -1 350 for (let offset = base, i = 0;; i++) { 351 let child = this.children[i], end = offset + child.size 352 if (fromOffset == -1 && from <= end) { 353 let childBase = offset + child.border 354 // FIXME maybe descend mark views to parse a narrower range? 355 if (from >= childBase && to <= end - child.border && child.node && 356 child.contentDOM && this.contentDOM!.contains(child.contentDOM)) 357 return child.parseRange(from, to, childBase) 358 359 from = offset 360 for (let j = i; j > 0; j--) { 361 let prev = this.children[j - 1] 362 if (prev.size && prev.dom.parentNode == this.contentDOM && !prev.emptyChildAt(1)) { 363 fromOffset = domIndex(prev.dom) + 1 364 break 365 } 366 from -= prev.size 367 } 368 if (fromOffset == -1) fromOffset = 0 369 } 370 if (fromOffset > -1 && (end > to || i == this.children.length - 1)) { 371 to = end 372 for (let j = i + 1; j < this.children.length; j++) { 373 let next = this.children[j] 374 if (next.size && next.dom.parentNode == this.contentDOM && !next.emptyChildAt(-1)) { 375 toOffset = domIndex(next.dom) 376 break 377 } 378 to += next.size 379 } 380 if (toOffset == -1) toOffset = this.contentDOM!.childNodes.length 381 break 382 } 383 offset = end 384 } 385 return {node: this.contentDOM!, from, to, fromOffset, toOffset} 386 } 387 388 emptyChildAt(side: number): boolean { 389 if (this.border || !this.contentDOM || !this.children.length) return false 390 let child = this.children[side < 0 ? 0 : this.children.length - 1] 391 return child.size == 0 || child.emptyChildAt(side) 392 } 393 394 domAfterPos(pos: number): DOMNode { 395 let {node, offset} = this.domFromPos(pos, 0) 396 if (node.nodeType != 1 || offset == node.childNodes.length) 397 throw new RangeError("No node after pos " + pos) 398 return node.childNodes[offset] 399 } 400 401 // View descs are responsible for setting any selection that falls 402 // entirely inside of them, so that custom implementations can do 403 // custom things with the selection. Note that this falls apart when 404 // a selection starts in such a node and ends in another, in which 405 // case we just use whatever domFromPos produces as a best effort. 406 setSelection(anchor: number, head: number, view: EditorView, force = false): void { 407 // If the selection falls entirely in a child, give it to that child 408 let from = Math.min(anchor, head), to = Math.max(anchor, head) 409 for (let i = 0, offset = 0; i < this.children.length; i++) { 410 let child = this.children[i], end = offset + child.size 411 if (from > offset && to < end) 412 return child.setSelection(anchor - offset - child.border, head - offset - child.border, view, force) 413 offset = end 414 } 415 416 let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1) 417 let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1) 418 let domSel = (view.root as Document).getSelection()! 419 let selRange = view.domSelectionRange() 420 421 let brKludge = false 422 // On Firefox, using Selection.collapse to put the cursor after a 423 // BR node for some reason doesn't always work (#1073). On Safari, 424 // the cursor sometimes inexplicable visually lags behind its 425 // reported position in such situations (#1092). 426 if ((browser.gecko || browser.safari) && anchor == head) { 427 let {node, offset} = anchorDOM 428 if (node.nodeType == 3) { 429 brKludge = !!(offset && node.nodeValue![offset - 1] == "\n") 430 // Issue #1128 431 if (brKludge && offset == node.nodeValue!.length) { 432 for (let scan: DOMNode | null = node, after; scan; scan = scan.parentNode) { 433 if (after = scan.nextSibling) { 434 if (after.nodeName == "BR") 435 anchorDOM = headDOM = {node: after.parentNode!, offset: domIndex(after) + 1} 436 break 437 } 438 let desc = scan.pmViewDesc 439 if (desc && desc.node && desc.node.isBlock) break 440 } 441 } 442 } else { 443 let prev = node.childNodes[offset - 1] 444 brKludge = prev && (prev.nodeName == "BR" || (prev as HTMLElement).contentEditable == "false") 445 } 446 } 447 // Firefox can act strangely when the selection is in front of an 448 // uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536 449 if (browser.gecko && selRange.focusNode && selRange.focusNode != headDOM.node && selRange.focusNode.nodeType == 1) { 450 let after = selRange.focusNode.childNodes[selRange.focusOffset] 451 if (after && (after as HTMLElement).contentEditable == "false") force = true 452 } 453 454 if (!(force || brKludge && browser.safari) && 455 isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode!, selRange.anchorOffset) && 456 isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode!, selRange.focusOffset)) 457 return 458 459 // Selection.extend can be used to create an 'inverted' selection 460 // (one where the focus is before the anchor), but not all 461 // browsers support it yet. 462 let domSelExtended = false 463 if ((domSel.extend || anchor == head) && !(brKludge && browser.gecko)) { 464 domSel.collapse(anchorDOM.node, anchorDOM.offset) 465 try { 466 if (anchor != head) 467 domSel.extend(headDOM.node, headDOM.offset) 468 domSelExtended = true 469 } catch (_) { 470 // In some cases with Chrome the selection is empty after calling 471 // collapse, even when it should be valid. This appears to be a bug, but 472 // it is difficult to isolate. If this happens fallback to the old path 473 // without using extend. 474 // Similarly, this could crash on Safari if the editor is hidden, and 475 // there was no selection. 476 } 477 } 478 if (!domSelExtended) { 479 if (anchor > head) { let tmp = anchorDOM; anchorDOM = headDOM; headDOM = tmp } 480 let range = document.createRange() 481 range.setEnd(headDOM.node, headDOM.offset) 482 range.setStart(anchorDOM.node, anchorDOM.offset) 483 domSel.removeAllRanges() 484 domSel.addRange(range) 485 } 486 } 487 488 ignoreMutation(mutation: ViewMutationRecord): boolean { 489 return !this.contentDOM && mutation.type != "selection" 490 } 491 492 get contentLost() { 493 return this.contentDOM && this.contentDOM != this.dom && !this.dom.contains(this.contentDOM) 494 } 495 496 // Remove a subtree of the element tree that has been touched 497 // by a DOM change, so that the next update will redraw it. 498 markDirty(from: number, to: number) { 499 for (let offset = 0, i = 0; i < this.children.length; i++) { 500 let child = this.children[i], end = offset + child.size 501 if (offset == end ? from <= end && to >= offset : from < end && to > offset) { 502 let startInside = offset + child.border, endInside = end - child.border 503 if (from >= startInside && to <= endInside) { 504 this.dirty = from == offset || to == end ? CONTENT_DIRTY : CHILD_DIRTY 505 if (from == startInside && to == endInside && 506 (child.contentLost || child.dom.parentNode != this.contentDOM)) child.dirty = NODE_DIRTY 507 else child.markDirty(from - startInside, to - startInside) 508 return 509 } else { 510 child.dirty = child.dom == child.contentDOM && child.dom.parentNode == this.contentDOM && !child.children.length 511 ? CONTENT_DIRTY : NODE_DIRTY 512 } 513 } 514 offset = end 515 } 516 this.dirty = CONTENT_DIRTY 517 } 518 519 markParentsDirty() { 520 let level = 1 521 for (let node = this.parent; node; node = node.parent, level++) { 522 let dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY 523 if (node.dirty < dirty) node.dirty = dirty 524 } 525 } 526 527 get domAtom() { return false } 528 529 get ignoreForCoords() { return false } 530 531 get ignoreForSelection() { return false } 532 533 isText(text: string) { return false } 534 } 535 536 // A widget desc represents a widget decoration, which is a DOM node 537 // drawn between the document nodes. 538 class WidgetViewDesc extends ViewDesc { 539 constructor(parent: ViewDesc, readonly widget: Decoration, view: EditorView, pos: number) { 540 let self: WidgetViewDesc, dom = (widget.type as any).toDOM as WidgetConstructor 541 if (typeof dom == "function") dom = dom(view, () => { 542 if (!self) return pos 543 if (self.parent) return self.parent.posBeforeChild(self) 544 }) 545 if (!widget.type.spec.raw) { 546 if (dom.nodeType != 1) { 547 let wrap = document.createElement("span") 548 wrap.appendChild(dom) 549 dom = wrap 550 } 551 ;(dom as HTMLElement).contentEditable = "false" 552 ;(dom as HTMLElement).classList.add("ProseMirror-widget") 553 } 554 super(parent, [], dom, null) 555 this.widget = widget 556 self = this 557 } 558 559 matchesWidget(widget: Decoration) { 560 return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type) 561 } 562 563 parseRule() { return {ignore: true} } 564 565 stopEvent(event: Event) { 566 let stop = this.widget.spec.stopEvent 567 return stop ? stop(event) : false 568 } 569 570 ignoreMutation(mutation: ViewMutationRecord) { 571 return mutation.type != "selection" || this.widget.spec.ignoreSelection 572 } 573 574 destroy() { 575 this.widget.type.destroy(this.dom) 576 super.destroy() 577 } 578 579 get domAtom() { return true } 580 581 get ignoreForSelection() { return !!this.widget.type.spec.relaxedSide } 582 583 get side() { return (this.widget.type as any).side as number } 584 } 585 586 class CompositionViewDesc extends ViewDesc { 587 constructor(parent: ViewDesc, dom: DOMNode, readonly textDOM: Text, readonly text: string) { 588 super(parent, [], dom, null) 589 } 590 591 get size() { return this.text.length } 592 593 localPosFromDOM(dom: DOMNode, offset: number) { 594 if (dom != this.textDOM) return this.posAtStart + (offset ? this.size : 0) 595 return this.posAtStart + offset 596 } 597 598 domFromPos(pos: number) { 599 return {node: this.textDOM, offset: pos} 600 } 601 602 ignoreMutation(mut: ViewMutationRecord) { 603 return mut.type === 'characterData' && mut.target.nodeValue == mut.oldValue 604 } 605 } 606 607 // A mark desc represents a mark. May have multiple children, 608 // depending on how the mark is split. Note that marks are drawn using 609 // a fixed nesting order, for simplicity and predictability, so in 610 // some cases they will be split more often than would appear 611 // necessary. 612 class MarkViewDesc extends ViewDesc { 613 constructor(parent: ViewDesc, readonly mark: Mark, dom: DOMNode, contentDOM: HTMLElement, readonly spec: MarkView) { 614 super(parent, [], dom, contentDOM) 615 } 616 617 static create(parent: ViewDesc, mark: Mark, inline: boolean, view: EditorView) { 618 let custom = view.nodeViews[mark.type.name] 619 let spec: {dom: HTMLElement, contentDOM?: HTMLElement} = custom && (custom as any)(mark, view, inline) 620 if (!spec || !spec.dom) 621 spec = (DOMSerializer.renderSpec as any)(document, mark.type.spec.toDOM!(mark, inline), null, mark.attrs) as any 622 return new MarkViewDesc(parent, mark, spec.dom, spec.contentDOM || spec.dom as HTMLElement, spec) 623 } 624 625 parseRule() { 626 if ((this.dirty & NODE_DIRTY) || this.mark.type.spec.reparseInView) return null 627 return {mark: this.mark.type.name, attrs: this.mark.attrs, contentElement: this.contentDOM!} 628 } 629 630 matchesMark(mark: Mark) { return this.dirty != NODE_DIRTY && this.mark.eq(mark) } 631 632 markDirty(from: number, to: number) { 633 super.markDirty(from, to) 634 // Move dirty info to nearest node view 635 if (this.dirty != NOT_DIRTY) { 636 let parent = this.parent! 637 while (!parent.node) parent = parent.parent! 638 if (parent.dirty < this.dirty) parent.dirty = this.dirty 639 this.dirty = NOT_DIRTY 640 } 641 } 642 643 slice(from: number, to: number, view: EditorView) { 644 let copy = MarkViewDesc.create(this.parent!, this.mark, true, view) 645 let nodes = this.children, size = this.size 646 if (to < size) nodes = replaceNodes(nodes, to, size, view) 647 if (from > 0) nodes = replaceNodes(nodes, 0, from, view) 648 for (let i = 0; i < nodes.length; i++) nodes[i].parent = copy 649 copy.children = nodes 650 return copy 651 } 652 653 ignoreMutation(mutation: ViewMutationRecord) { 654 return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation) 655 } 656 657 destroy() { 658 if (this.spec.destroy) this.spec.destroy() 659 super.destroy() 660 } 661 } 662 663 // Node view descs are the main, most common type of view desc, and 664 // correspond to an actual node in the document. Unlike mark descs, 665 // they populate their child array themselves. 666 export class NodeViewDesc extends ViewDesc { 667 constructor( 668 parent: ViewDesc | undefined, 669 public node: Node, 670 public outerDeco: readonly Decoration[], 671 public innerDeco: DecorationSource, 672 dom: DOMNode, 673 contentDOM: HTMLElement | null, 674 readonly nodeDOM: DOMNode, 675 view: EditorView, 676 pos: number 677 ) { 678 super(parent, [], dom, contentDOM) 679 } 680 681 // By default, a node is rendered using the `toDOM` method from the 682 // node type spec. But client code can use the `nodeViews` spec to 683 // supply a custom node view, which can influence various aspects of 684 // the way the node works. 685 // 686 // (Using subclassing for this was intentionally decided against, 687 // since it'd require exposing a whole slew of finicky 688 // implementation details to the user code that they probably will 689 // never need.) 690 static create(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], 691 innerDeco: DecorationSource, view: EditorView, pos: number) { 692 let custom = view.nodeViews[node.type.name], descObj: ViewDesc 693 let spec: NodeView | undefined = custom && (custom as any)(node, view, () => { 694 // (This is a function that allows the custom view to find its 695 // own position) 696 if (!descObj) return pos 697 if (descObj.parent) return descObj.parent.posBeforeChild(descObj) 698 }, outerDeco, innerDeco) 699 700 let dom = spec && spec.dom, contentDOM = spec && spec.contentDOM 701 if (node.isText) { 702 if (!dom) dom = document.createTextNode(node.text!) 703 else if (dom.nodeType != 3) throw new RangeError("Text must be rendered as a DOM text node") 704 } else if (!dom) { 705 let spec = (DOMSerializer.renderSpec as any)(document, node.type.spec.toDOM!(node), null, node.attrs) 706 ;({dom, contentDOM} = spec as {dom: DOMNode, contentDOM?: HTMLElement}) 707 } 708 if (!contentDOM && !node.isText && dom.nodeName != "BR") { // Chrome gets confused by <br contenteditable=false> 709 if (!(dom as HTMLElement).hasAttribute("contenteditable")) (dom as HTMLElement).contentEditable = "false" 710 if (node.type.spec.draggable) (dom as HTMLElement).draggable = true 711 } 712 713 let nodeDOM = dom 714 dom = applyOuterDeco(dom, outerDeco, node) 715 716 if (spec) 717 return descObj = new CustomNodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, 718 spec, view, pos + 1) 719 else if (node.isText) 720 return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM, view) 721 else 722 return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, view, pos + 1) 723 } 724 725 parseRule() { 726 // Experimental kludge to allow opt-in re-parsing of nodes 727 if (this.node.type.spec.reparseInView) return null 728 // FIXME the assumption that this can always return the current 729 // attrs means that if the user somehow manages to change the 730 // attrs in the dom, that won't be picked up. Not entirely sure 731 // whether this is a problem 732 let rule: Omit<TagParseRule, "tag"> = {node: this.node.type.name, attrs: this.node.attrs} 733 if (this.node.type.whitespace == "pre") rule.preserveWhitespace = "full" 734 if (!this.contentDOM) { 735 rule.getContent = () => this.node.content 736 } else if (!this.contentLost) { 737 rule.contentElement = this.contentDOM 738 } else { 739 // Chrome likes to randomly recreate parent nodes when 740 // backspacing things. When that happens, this tries to find the 741 // new parent. 742 for (let i = this.children.length - 1; i >= 0; i--) { 743 let child = this.children[i] 744 if (this.dom.contains(child.dom.parentNode)) { 745 rule.contentElement = child.dom.parentNode as HTMLElement 746 break 747 } 748 } 749 if (!rule.contentElement) rule.getContent = () => Fragment.empty 750 } 751 return rule 752 } 753 754 matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource) { 755 return this.dirty == NOT_DIRTY && node.eq(this.node) && 756 sameOuterDeco(outerDeco, this.outerDeco) && innerDeco.eq(this.innerDeco) 757 } 758 759 get size() { return this.node.nodeSize } 760 761 get border() { return this.node.isLeaf ? 0 : 1 } 762 763 // Syncs `this.children` to match `this.node.content` and the local 764 // decorations, possibly introducing nesting for marks. Then, in a 765 // separate step, syncs the DOM inside `this.contentDOM` to 766 // `this.children`. 767 updateChildren(view: EditorView, pos: number) { 768 let inline = this.node.inlineContent, off = pos 769 let composition = view.composing ? this.localCompositionInfo(view, pos) : null 770 let localComposition = composition && composition.pos > -1 ? composition : null 771 let compositionInChild = composition && composition.pos < 0 772 let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view) 773 iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => { 774 if (widget.spec.marks) 775 updater.syncToMarks(widget.spec.marks, inline, view) 776 else if ((widget.type as WidgetType).side >= 0 && !insideNode) 777 updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view) 778 // If the next node is a desc matching this widget, reuse it, 779 // otherwise insert the widget as a new view desc. 780 updater.placeWidget(widget, view, off) 781 }, (child, outerDeco, innerDeco, i) => { 782 // Make sure the wrapping mark descs match the node's marks. 783 updater.syncToMarks(child.marks, inline, view) 784 // Try several strategies for drawing this node 785 let compIndex 786 if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) { 787 // Found precise match with existing node view 788 } else if (compositionInChild && view.state.selection.from > off && 789 view.state.selection.to < off + child.nodeSize && 790 (compIndex = updater.findIndexWithChild(composition!.node)) > -1 && 791 updater.updateNodeAt(child, outerDeco, innerDeco, compIndex, view)) { 792 // Updated the specific node that holds the composition 793 } else if (updater.updateNextNode(child, outerDeco, innerDeco, view, i, off)) { 794 // Could update an existing node to reflect this node 795 } else { 796 // Add it as a new view 797 updater.addNode(child, outerDeco, innerDeco, view, off) 798 } 799 off += child.nodeSize 800 }) 801 // Drop all remaining descs after the current position. 802 updater.syncToMarks([], inline, view) 803 if (this.node.isTextblock) updater.addTextblockHacks() 804 updater.destroyRest() 805 806 // Sync the DOM if anything changed 807 if (updater.changed || this.dirty == CONTENT_DIRTY) { 808 // May have to protect focused DOM from being changed if a composition is active 809 if (localComposition) this.protectLocalComposition(view, localComposition) 810 renderDescs(this.contentDOM!, this.children, view) 811 if (browser.ios) iosHacks(this.dom as HTMLElement) 812 } 813 } 814 815 localCompositionInfo(view: EditorView, pos: number): {node: Text, pos: number, text: string} | null { 816 // Only do something if both the selection and a focused text node 817 // are inside of this node 818 let {from, to} = view.state.selection 819 if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size) return null 820 let textNode = view.input.compositionNode 821 if (!textNode || !this.dom.contains(textNode.parentNode)) return null 822 823 if (this.node.inlineContent) { 824 // Find the text in the focused node in the node, stop if it's not 825 // there (may have been modified through other means, in which 826 // case it should overwritten) 827 let text = textNode.nodeValue! 828 let textPos = findTextInFragment(this.node.content, text, from - pos, to - pos) 829 return textPos < 0 ? null : {node: textNode, pos: textPos, text} 830 } else { 831 return {node: textNode, pos: -1, text: ""} 832 } 833 } 834 835 protectLocalComposition(view: EditorView, {node, pos, text}: {node: Text, pos: number, text: string}) { 836 // The node is already part of a local view desc, leave it there 837 if (this.getDesc(node)) return 838 839 // Create a composition view for the orphaned nodes 840 let topNode: DOMNode = node 841 for (;; topNode = topNode.parentNode!) { 842 if (topNode.parentNode == this.contentDOM) break 843 while (topNode.previousSibling) topNode.parentNode!.removeChild(topNode.previousSibling) 844 while (topNode.nextSibling) topNode.parentNode!.removeChild(topNode.nextSibling) 845 if (topNode.pmViewDesc) topNode.pmViewDesc = undefined 846 } 847 let desc = new CompositionViewDesc(this, topNode, node, text) 848 view.input.compositionNodes.push(desc) 849 850 // Patch up this.children to contain the composition view 851 this.children = replaceNodes(this.children, pos, pos + text.length, view, desc) 852 } 853 854 // If this desc must be updated to match the given node decoration, 855 // do so and return true. 856 update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { 857 if (this.dirty == NODE_DIRTY || 858 !node.sameMarkup(this.node)) return false 859 this.updateInner(node, outerDeco, innerDeco, view) 860 return true 861 } 862 863 updateInner(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { 864 this.updateOuterDeco(outerDeco) 865 this.node = node 866 this.innerDeco = innerDeco 867 if (this.contentDOM) this.updateChildren(view, this.posAtStart) 868 this.dirty = NOT_DIRTY 869 } 870 871 updateOuterDeco(outerDeco: readonly Decoration[]) { 872 if (sameOuterDeco(outerDeco, this.outerDeco)) return 873 let needsWrap = this.nodeDOM.nodeType != 1 874 let oldDOM = this.dom 875 this.dom = patchOuterDeco(this.dom, this.nodeDOM, 876 computeOuterDeco(this.outerDeco, this.node, needsWrap), 877 computeOuterDeco(outerDeco, this.node, needsWrap)) 878 if (this.dom != oldDOM) { 879 oldDOM.pmViewDesc = undefined 880 this.dom.pmViewDesc = this 881 } 882 this.outerDeco = outerDeco 883 } 884 885 // Mark this node as being the selected node. 886 selectNode() { 887 if (this.nodeDOM.nodeType == 1) { 888 ;(this.nodeDOM as HTMLElement).classList.add("ProseMirror-selectednode") 889 if (this.contentDOM || !this.node.type.spec.draggable) (this.nodeDOM as HTMLElement).draggable = true 890 } 891 } 892 893 // Remove selected node marking from this node. 894 deselectNode() { 895 if (this.nodeDOM.nodeType == 1) { 896 ;(this.nodeDOM as HTMLElement).classList.remove("ProseMirror-selectednode") 897 if (this.contentDOM || !this.node.type.spec.draggable) (this.nodeDOM as HTMLElement).removeAttribute("draggable") 898 } 899 } 900 901 get domAtom() { return this.node.isAtom } 902 } 903 904 // Create a view desc for the top-level document node, to be exported 905 // and used by the view class. 906 export function docViewDesc(doc: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, 907 dom: HTMLElement, view: EditorView): NodeViewDesc { 908 applyOuterDeco(dom, outerDeco, doc) 909 let docView = new NodeViewDesc(undefined, doc, outerDeco, innerDeco, dom, dom, dom, view, 0) 910 if (docView.contentDOM) docView.updateChildren(view, 0) 911 return docView 912 } 913 914 class TextViewDesc extends NodeViewDesc { 915 constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], 916 innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode, view: EditorView) { 917 super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM, view, 0) 918 } 919 920 parseRule() { 921 let skip = this.nodeDOM.parentNode 922 while (skip && skip != this.dom && !(skip as any).pmIsDeco) skip = skip.parentNode 923 return {skip: (skip || true) as any} 924 } 925 926 update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { 927 if (this.dirty == NODE_DIRTY || (this.dirty != NOT_DIRTY && !this.inParent()) || 928 !node.sameMarkup(this.node)) return false 929 this.updateOuterDeco(outerDeco) 930 if ((this.dirty != NOT_DIRTY || node.text != this.node.text) && node.text != this.nodeDOM.nodeValue) { 931 this.nodeDOM.nodeValue = node.text! 932 if (view.trackWrites == this.nodeDOM) view.trackWrites = null 933 } 934 this.node = node 935 this.dirty = NOT_DIRTY 936 return true 937 } 938 939 inParent() { 940 let parentDOM = this.parent!.contentDOM 941 for (let n: DOMNode | null = this.nodeDOM; n; n = n.parentNode) if (n == parentDOM) return true 942 return false 943 } 944 945 domFromPos(pos: number) { 946 return {node: this.nodeDOM, offset: pos} 947 } 948 949 localPosFromDOM(dom: DOMNode, offset: number, bias: number) { 950 if (dom == this.nodeDOM) return this.posAtStart + Math.min(offset, this.node.text!.length) 951 return super.localPosFromDOM(dom, offset, bias) 952 } 953 954 ignoreMutation(mutation: ViewMutationRecord) { 955 return mutation.type != "characterData" && mutation.type != "selection" 956 } 957 958 slice(from: number, to: number, view: EditorView) { 959 let node = this.node.cut(from, to), dom = document.createTextNode(node.text!) 960 return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom, view) 961 } 962 963 markDirty(from: number, to: number) { 964 super.markDirty(from, to) 965 if (this.dom != this.nodeDOM && (from == 0 || to == this.nodeDOM.nodeValue!.length)) 966 this.dirty = NODE_DIRTY 967 } 968 969 get domAtom() { return false } 970 971 isText(text: string) { return this.node.text == text } 972 } 973 974 // A dummy desc used to tag trailing BR or IMG nodes created to work 975 // around contentEditable terribleness. 976 class TrailingHackViewDesc extends ViewDesc { 977 parseRule() { return {ignore: true} } 978 matchesHack(nodeName: string) { return this.dirty == NOT_DIRTY && this.dom.nodeName == nodeName } 979 get domAtom() { return true } 980 get ignoreForCoords() { return this.dom.nodeName == "IMG" } 981 } 982 983 // A separate subclass is used for customized node views, so that the 984 // extra checks only have to be made for nodes that are actually 985 // customized. 986 class CustomNodeViewDesc extends NodeViewDesc { 987 constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, 988 dom: DOMNode, contentDOM: HTMLElement | null, nodeDOM: DOMNode, readonly spec: NodeView, 989 view: EditorView, pos: number) { 990 super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos) 991 } 992 993 // A custom `update` method gets to decide whether the update goes 994 // through. If it does, and there's a `contentDOM` node, our logic 995 // updates the children. 996 update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { 997 if (this.dirty == NODE_DIRTY) return false 998 if (this.spec.update && (this.node.type == node.type || this.spec.multiType)) { 999 let result = this.spec.update(node, outerDeco, innerDeco) 1000 if (result) this.updateInner(node, outerDeco, innerDeco, view) 1001 return result 1002 } else if (!this.contentDOM && !node.isLeaf) { 1003 return false 1004 } else { 1005 return super.update(node, outerDeco, innerDeco, view) 1006 } 1007 } 1008 1009 selectNode() { 1010 this.spec.selectNode ? this.spec.selectNode() : super.selectNode() 1011 } 1012 1013 deselectNode() { 1014 this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode() 1015 } 1016 1017 setSelection(anchor: number, head: number, view: EditorView, force: boolean) { 1018 this.spec.setSelection ? this.spec.setSelection(anchor, head, view.root) 1019 : super.setSelection(anchor, head, view, force) 1020 } 1021 1022 destroy() { 1023 if (this.spec.destroy) this.spec.destroy() 1024 super.destroy() 1025 } 1026 1027 stopEvent(event: Event) { 1028 return this.spec.stopEvent ? this.spec.stopEvent(event) : false 1029 } 1030 1031 ignoreMutation(mutation: ViewMutationRecord) { 1032 return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation) 1033 } 1034 } 1035 1036 // Sync the content of the given DOM node with the nodes associated 1037 // with the given array of view descs, recursing into mark descs 1038 // because this should sync the subtree for a whole node at a time. 1039 function renderDescs(parentDOM: HTMLElement, descs: readonly ViewDesc[], view: EditorView) { 1040 let dom = parentDOM.firstChild, written = false 1041 for (let i = 0; i < descs.length; i++) { 1042 let desc = descs[i], childDOM = desc.dom 1043 if (childDOM.parentNode == parentDOM) { 1044 while (childDOM != dom) { dom = rm(dom!); written = true } 1045 dom = dom.nextSibling 1046 } else { 1047 written = true 1048 parentDOM.insertBefore(childDOM, dom) 1049 } 1050 if (desc instanceof MarkViewDesc) { 1051 let pos = dom ? dom.previousSibling : parentDOM.lastChild 1052 renderDescs(desc.contentDOM!, desc.children, view) 1053 dom = pos ? pos.nextSibling : parentDOM.firstChild 1054 } 1055 } 1056 while (dom) { dom = rm(dom); written = true } 1057 if (written && view.trackWrites == parentDOM) view.trackWrites = null 1058 } 1059 1060 type OuterDecoLevel = {[attr: string]: string} 1061 1062 const OuterDecoLevel: {new (nodeName?: string): OuterDecoLevel} = function(this: any, nodeName?: string) { 1063 if (nodeName) this.nodeName = nodeName 1064 } as any 1065 OuterDecoLevel.prototype = Object.create(null) 1066 1067 const noDeco = [new OuterDecoLevel] 1068 1069 function computeOuterDeco(outerDeco: readonly Decoration[], node: Node, needsWrap: boolean) { 1070 if (outerDeco.length == 0) return noDeco 1071 1072 let top = needsWrap ? noDeco[0] : new OuterDecoLevel, result = [top] 1073 1074 for (let i = 0; i < outerDeco.length; i++) { 1075 let attrs = (outerDeco[i].type as NodeType).attrs 1076 if (!attrs) continue 1077 if (attrs.nodeName) 1078 result.push(top = new OuterDecoLevel(attrs.nodeName)) 1079 1080 for (let name in attrs) { 1081 let val = attrs[name] 1082 if (val == null) continue 1083 if (needsWrap && result.length == 1) 1084 result.push(top = new OuterDecoLevel(node.isInline ? "span" : "div")) 1085 if (name == "class") top.class = (top.class ? top.class + " " : "") + val 1086 else if (name == "style") top.style = (top.style ? top.style + ";" : "") + val 1087 else if (name != "nodeName") top[name] = val 1088 } 1089 } 1090 1091 return result 1092 } 1093 1094 function patchOuterDeco(outerDOM: DOMNode, nodeDOM: DOMNode, 1095 prevComputed: readonly OuterDecoLevel[], curComputed: readonly OuterDecoLevel[]) { 1096 // Shortcut for trivial case 1097 if (prevComputed == noDeco && curComputed == noDeco) return nodeDOM 1098 1099 let curDOM = nodeDOM 1100 for (let i = 0; i < curComputed.length; i++) { 1101 let deco = curComputed[i], prev = prevComputed[i] 1102 if (i) { 1103 let parent: DOMNode | null 1104 if (prev && prev.nodeName == deco.nodeName && curDOM != outerDOM && 1105 (parent = curDOM.parentNode) && parent.nodeName!.toLowerCase() == deco.nodeName) { 1106 curDOM = parent 1107 } else { 1108 parent = document.createElement(deco.nodeName) 1109 ;(parent as any).pmIsDeco = true 1110 parent.appendChild(curDOM) 1111 prev = noDeco[0] 1112 curDOM = parent 1113 } 1114 } 1115 patchAttributes(curDOM as HTMLElement, prev || noDeco[0], deco) 1116 } 1117 return curDOM 1118 } 1119 1120 function patchAttributes(dom: HTMLElement, prev: {[name: string]: string}, cur: {[name: string]: string}) { 1121 for (let name in prev) 1122 if (name != "class" && name != "style" && name != "nodeName" && !(name in cur)) 1123 dom.removeAttribute(name) 1124 for (let name in cur) 1125 if (name != "class" && name != "style" && name != "nodeName" && cur[name] != prev[name]) 1126 dom.setAttribute(name, cur[name]) 1127 if (prev.class != cur.class) { 1128 let prevList = prev.class ? prev.class.split(" ").filter(Boolean) : [] 1129 let curList = cur.class ? cur.class.split(" ").filter(Boolean) : [] 1130 for (let i = 0; i < prevList.length; i++) if (curList.indexOf(prevList[i]) == -1) 1131 dom.classList.remove(prevList[i]) 1132 for (let i = 0; i < curList.length; i++) if (prevList.indexOf(curList[i]) == -1) 1133 dom.classList.add(curList[i]) 1134 if (dom.classList.length == 0) 1135 dom.removeAttribute("class") 1136 } 1137 if (prev.style != cur.style) { 1138 if (prev.style) { 1139 let prop = /\s*([\w\-\xa1-\uffff]+)\s*:(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\(.*?\)|[^;])*/g, m 1140 while (m = prop.exec(prev.style)) 1141 dom.style.removeProperty(m[1]) 1142 } 1143 if (cur.style) 1144 dom.style.cssText += cur.style 1145 } 1146 } 1147 1148 function applyOuterDeco(dom: DOMNode, deco: readonly Decoration[], node: Node) { 1149 return patchOuterDeco(dom, dom, noDeco, computeOuterDeco(deco, node, dom.nodeType != 1)) 1150 } 1151 1152 function sameOuterDeco(a: readonly Decoration[], b: readonly Decoration[]) { 1153 if (a.length != b.length) return false 1154 for (let i = 0; i < a.length; i++) if (!a[i].type.eq(b[i].type)) return false 1155 return true 1156 } 1157 1158 // Remove a DOM node and return its next sibling. 1159 function rm(dom: DOMNode) { 1160 let next = dom.nextSibling 1161 dom.parentNode!.removeChild(dom) 1162 return next 1163 } 1164 1165 // Helper class for incrementally updating a tree of mark descs and 1166 // the widget and node descs inside of them. 1167 class ViewTreeUpdater { 1168 // Index into `this.top`'s child array, represents the current 1169 // update position. 1170 index = 0 1171 // When entering a mark, the current top and index are pushed 1172 // onto this. 1173 stack: (ViewDesc | number)[] = [] 1174 // Tracks whether anything was changed 1175 changed = false 1176 preMatch: {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]} 1177 top: ViewDesc 1178 1179 constructor(top: NodeViewDesc, readonly lock: DOMNode | null, private readonly view: EditorView) { 1180 this.top = top 1181 this.preMatch = preMatch(top.node.content, top) 1182 } 1183 1184 // Destroy and remove the children between the given indices in 1185 // `this.top`. 1186 destroyBetween(start: number, end: number) { 1187 if (start == end) return 1188 for (let i = start; i < end; i++) this.top.children[i].destroy() 1189 this.top.children.splice(start, end - start) 1190 this.changed = true 1191 } 1192 1193 // Destroy all remaining children in `this.top`. 1194 destroyRest() { 1195 this.destroyBetween(this.index, this.top.children.length) 1196 } 1197 1198 // Sync the current stack of mark descs with the given array of 1199 // marks, reusing existing mark descs when possible. 1200 syncToMarks(marks: readonly Mark[], inline: boolean, view: EditorView) { 1201 let keep = 0, depth = this.stack.length >> 1 1202 let maxKeep = Math.min(depth, marks.length) 1203 while (keep < maxKeep && 1204 (keep == depth - 1 ? this.top : this.stack[(keep + 1) << 1] as ViewDesc) 1205 .matchesMark(marks[keep]) && marks[keep].type.spec.spanning !== false) 1206 keep++ 1207 1208 while (keep < depth) { 1209 this.destroyRest() 1210 this.top.dirty = NOT_DIRTY 1211 this.index = this.stack.pop() as number 1212 this.top = this.stack.pop() as ViewDesc 1213 depth-- 1214 } 1215 while (depth < marks.length) { 1216 this.stack.push(this.top, this.index + 1) 1217 let found = -1 1218 for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) { 1219 let next = this.top.children[i] 1220 if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) { found = i; break } 1221 } 1222 if (found > -1) { 1223 if (found > this.index) { 1224 this.changed = true 1225 this.destroyBetween(this.index, found) 1226 } 1227 this.top = this.top.children[this.index] 1228 } else { 1229 let markDesc = MarkViewDesc.create(this.top, marks[depth], inline, view) 1230 this.top.children.splice(this.index, 0, markDesc) 1231 this.top = markDesc 1232 this.changed = true 1233 } 1234 this.index = 0 1235 depth++ 1236 } 1237 } 1238 1239 // Try to find a node desc matching the given data. Skip over it and 1240 // return true when successful. 1241 findNodeMatch(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number): boolean { 1242 let found = -1, targetDesc 1243 if (index >= this.preMatch.index && 1244 (targetDesc = this.preMatch.matches[index - this.preMatch.index]).parent == this.top && 1245 targetDesc.matchesNode(node, outerDeco, innerDeco)) { 1246 found = this.top.children.indexOf(targetDesc, this.index) 1247 } else { 1248 for (let i = this.index, e = Math.min(this.top.children.length, i + 5); i < e; i++) { 1249 let child = this.top.children[i] 1250 if (child.matchesNode(node, outerDeco, innerDeco) && !this.preMatch.matched.has(child)) { 1251 found = i 1252 break 1253 } 1254 } 1255 } 1256 if (found < 0) return false 1257 this.destroyBetween(this.index, found) 1258 this.index++ 1259 return true 1260 } 1261 1262 updateNodeAt(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number, view: EditorView) { 1263 let child = this.top.children[index] as NodeViewDesc 1264 if (child.dirty == NODE_DIRTY && child.dom == child.contentDOM) child.dirty = CONTENT_DIRTY 1265 if (!child.update(node, outerDeco, innerDeco, view)) return false 1266 this.destroyBetween(this.index, index) 1267 this.index++ 1268 return true 1269 } 1270 1271 findIndexWithChild(domNode: DOMNode) { 1272 for (;;) { 1273 let parent = domNode.parentNode 1274 if (!parent) return -1 1275 if (parent == this.top.contentDOM) { 1276 let desc = domNode.pmViewDesc 1277 if (desc) for (let i = this.index; i < this.top.children.length; i++) { 1278 if (this.top.children[i] == desc) return i 1279 } 1280 return -1 1281 } 1282 domNode = parent 1283 } 1284 } 1285 1286 // Try to update the next node, if any, to the given data. Checks 1287 // pre-matches to avoid overwriting nodes that could still be used. 1288 updateNextNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, 1289 view: EditorView, index: number, pos: number): boolean { 1290 for (let i = this.index; i < this.top.children.length; i++) { 1291 let next = this.top.children[i] 1292 if (next instanceof NodeViewDesc) { 1293 let preMatch = this.preMatch.matched.get(next) 1294 if (preMatch != null && preMatch != index) return false 1295 let nextDOM = next.dom, updated 1296 1297 // Can't update if nextDOM is or contains this.lock, except if 1298 // it's a text node whose content already matches the new text 1299 // and whose decorations match the new ones. 1300 let locked = this.isLocked(nextDOM) && 1301 !(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text && 1302 next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco)) 1303 if (!locked && next.update(node, outerDeco, innerDeco, view)) { 1304 this.destroyBetween(this.index, i) 1305 if (next.dom != nextDOM) this.changed = true 1306 this.index++ 1307 return true 1308 } else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) { 1309 this.destroyBetween(this.index, i) 1310 this.top.children[this.index] = updated 1311 if (updated.contentDOM) { 1312 updated.dirty = CONTENT_DIRTY 1313 updated.updateChildren(view, pos + 1) 1314 updated.dirty = NOT_DIRTY 1315 } 1316 this.changed = true 1317 this.index++ 1318 return true 1319 } 1320 break 1321 } 1322 } 1323 return false 1324 } 1325 1326 // When a node with content is replaced by a different node with 1327 // identical content, move over its children. 1328 recreateWrapper(next: NodeViewDesc, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, 1329 view: EditorView, pos: number) { 1330 if (next.dirty || node.isAtom || !next.children.length || 1331 !next.node.content.eq(node.content) || 1332 !sameOuterDeco(outerDeco, next.outerDeco) || !innerDeco.eq(next.innerDeco)) return null 1333 let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos) 1334 if (wrapper.contentDOM) { 1335 wrapper.children = next.children 1336 next.children = [] 1337 for (let ch of wrapper.children) ch.parent = wrapper 1338 } 1339 next.destroy() 1340 return wrapper 1341 } 1342 1343 // Insert the node as a newly created node desc. 1344 addNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView, pos: number) { 1345 let desc = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos) 1346 if (desc.contentDOM) desc.updateChildren(view, pos + 1) 1347 this.top.children.splice(this.index++, 0, desc) 1348 this.changed = true 1349 } 1350 1351 placeWidget(widget: Decoration, view: EditorView, pos: number) { 1352 let next = this.index < this.top.children.length ? this.top.children[this.index] : null 1353 if (next && next.matchesWidget(widget) && 1354 (widget == (next as WidgetViewDesc).widget || !(next as any).widget.type.toDOM.parentNode)) { 1355 this.index++ 1356 } else { 1357 let desc = new WidgetViewDesc(this.top, widget, view, pos) 1358 this.top.children.splice(this.index++, 0, desc) 1359 this.changed = true 1360 } 1361 } 1362 1363 // Make sure a textblock looks and behaves correctly in 1364 // contentEditable. 1365 addTextblockHacks() { 1366 let lastChild = this.top.children[this.index - 1], parent = this.top 1367 while (lastChild instanceof MarkViewDesc) { 1368 parent = lastChild 1369 lastChild = parent.children[parent.children.length - 1] 1370 } 1371 1372 if (!lastChild || // Empty textblock 1373 !(lastChild instanceof TextViewDesc) || 1374 /\n$/.test(lastChild.node.text!) || 1375 (this.view.requiresGeckoHackNode && /\s$/.test(lastChild.node.text!))) { 1376 // Avoid bugs in Safari's cursor drawing (#1165) and Chrome's mouse selection (#1152) 1377 if ((browser.safari || browser.chrome) && lastChild && (lastChild.dom as HTMLElement).contentEditable == "false") 1378 this.addHackNode("IMG", parent) 1379 this.addHackNode("BR", this.top) 1380 } 1381 } 1382 1383 addHackNode(nodeName: string, parent: ViewDesc) { 1384 if (parent == this.top && this.index < parent.children.length && parent.children[this.index].matchesHack(nodeName)) { 1385 this.index++ 1386 } else { 1387 let dom = document.createElement(nodeName) 1388 if (nodeName == "IMG") { 1389 dom.className = "ProseMirror-separator" 1390 ;(dom as HTMLImageElement).alt = "" 1391 } 1392 if (nodeName == "BR") dom.className = "ProseMirror-trailingBreak" 1393 let hack = new TrailingHackViewDesc(this.top, [], dom, null) 1394 if (parent != this.top) parent.children.push(hack) 1395 else parent.children.splice(this.index++, 0, hack) 1396 this.changed = true 1397 } 1398 } 1399 1400 isLocked(node: DOMNode) { 1401 return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode)) 1402 } 1403 } 1404 1405 // Iterate from the end of the fragment and array of descs to find 1406 // directly matching ones, in order to avoid overeagerly reusing those 1407 // for other nodes. Returns the fragment index of the first node that 1408 // is part of the sequence of matched nodes at the end of the 1409 // fragment. 1410 function preMatch( 1411 frag: Fragment, parentDesc: ViewDesc 1412 ): {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]} { 1413 let curDesc = parentDesc, descI = curDesc.children.length 1414 let fI = frag.childCount, matched = new Map, matches = [] 1415 outer: while (fI > 0) { 1416 let desc 1417 for (;;) { 1418 if (descI) { 1419 let next = curDesc.children[descI - 1] 1420 if (next instanceof MarkViewDesc) { 1421 curDesc = next 1422 descI = next.children.length 1423 } else { 1424 desc = next 1425 descI-- 1426 break 1427 } 1428 } else if (curDesc == parentDesc) { 1429 break outer 1430 } else { 1431 // FIXME 1432 descI = curDesc.parent!.children.indexOf(curDesc) 1433 curDesc = curDesc.parent! 1434 } 1435 } 1436 let node = desc.node 1437 if (!node) continue 1438 if (node != frag.child(fI - 1)) break 1439 --fI 1440 matched.set(desc, fI) 1441 matches.push(desc) 1442 } 1443 return {index: fI, matched, matches: matches.reverse()} 1444 } 1445 1446 function compareSide(a: Decoration, b: Decoration) { 1447 return (a.type as WidgetType).side - (b.type as WidgetType).side 1448 } 1449 1450 // This function abstracts iterating over the nodes and decorations in 1451 // a fragment. Calls `onNode` for each node, with its local and child 1452 // decorations. Splits text nodes when there is a decoration starting 1453 // or ending inside of them. Calls `onWidget` for each widget. 1454 function iterDeco( 1455 parent: Node, 1456 deco: DecorationSource, 1457 onWidget: (widget: Decoration, index: number, insideNode: boolean) => void, 1458 onNode: (node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number) => void 1459 ) { 1460 let locals = deco.locals(parent), offset = 0 1461 // Simple, cheap variant for when there are no local decorations 1462 if (locals.length == 0) { 1463 for (let i = 0; i < parent.childCount; i++) { 1464 let child = parent.child(i) 1465 onNode(child, locals, deco.forChild(offset, child), i) 1466 offset += child.nodeSize 1467 } 1468 return 1469 } 1470 1471 let decoIndex = 0, active = [], restNode = null 1472 for (let parentIndex = 0;;) { 1473 let widget, widgets 1474 while (decoIndex < locals.length && locals[decoIndex].to == offset) { 1475 let next = locals[decoIndex++] 1476 if (next.widget) { 1477 if (!widget) widget = next 1478 else (widgets || (widgets = [widget])).push(next) 1479 } 1480 } 1481 if (widget) { 1482 if (widgets) { 1483 widgets.sort(compareSide) 1484 for (let i = 0; i < widgets.length; i++) onWidget(widgets[i], parentIndex, !!restNode) 1485 } else { 1486 onWidget(widget, parentIndex, !!restNode) 1487 } 1488 } 1489 1490 let child, index 1491 if (restNode) { 1492 index = -1 1493 child = restNode 1494 restNode = null 1495 } else if (parentIndex < parent.childCount) { 1496 index = parentIndex 1497 child = parent.child(parentIndex++) 1498 } else { 1499 break 1500 } 1501 1502 for (let i = 0; i < active.length; i++) if (active[i].to <= offset) active.splice(i--, 1) 1503 while (decoIndex < locals.length && locals[decoIndex].from <= offset && locals[decoIndex].to > offset) 1504 active.push(locals[decoIndex++]) 1505 1506 let end = offset + child.nodeSize 1507 if (child.isText) { 1508 let cutAt = end 1509 if (decoIndex < locals.length && locals[decoIndex].from < cutAt) cutAt = locals[decoIndex].from 1510 for (let i = 0; i < active.length; i++) if (active[i].to < cutAt) cutAt = active[i].to 1511 if (cutAt < end) { 1512 restNode = child.cut(cutAt - offset) 1513 child = child.cut(0, cutAt - offset) 1514 end = cutAt 1515 index = -1 1516 } 1517 } else { 1518 while (decoIndex < locals.length && locals[decoIndex].to < end) decoIndex++ 1519 } 1520 1521 let outerDeco = child.isInline && !child.isLeaf ? active.filter(d => !d.inline) : active.slice() 1522 onNode(child, outerDeco, deco.forChild(offset, child), index) 1523 offset = end 1524 } 1525 } 1526 1527 // List markers in Mobile Safari will mysteriously disappear 1528 // sometimes. This works around that. 1529 function iosHacks(dom: HTMLElement) { 1530 if (dom.nodeName == "UL" || dom.nodeName == "OL") { 1531 let oldCSS = dom.style.cssText 1532 dom.style.cssText = oldCSS + "; list-style: square !important" 1533 window.getComputedStyle(dom).listStyle 1534 dom.style.cssText = oldCSS 1535 } 1536 } 1537 1538 // Find a piece of text in an inline fragment, overlapping from-to 1539 function findTextInFragment(frag: Fragment, text: string, from: number, to: number) { 1540 for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) { 1541 let child = frag.child(i++), childStart = pos 1542 pos += child.nodeSize 1543 if (!child.isText) continue 1544 let str = child.text! 1545 while (i < frag.childCount) { 1546 let next = frag.child(i++) 1547 pos += next.nodeSize 1548 if (!next.isText) break 1549 str += next.text 1550 } 1551 if (pos >= from) { 1552 if (pos >= to && str.slice(to - text.length - childStart, to - childStart) == text) 1553 return to - text.length 1554 let found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1 1555 if (found >= 0 && found + text.length + childStart >= from) 1556 return childStart + found 1557 if (from == to && str.length >= (to + text.length) - childStart && 1558 str.slice(to - childStart, to - childStart + text.length) == text) 1559 return to 1560 } 1561 } 1562 return -1 1563 } 1564 1565 // Replace range from-to in an array of view descs with replacement 1566 // (may be null to just delete). This goes very much against the grain 1567 // of the rest of this code, which tends to create nodes with the 1568 // right shape in one go, rather than messing with them after 1569 // creation, but is necessary in the composition hack. 1570 function replaceNodes(nodes: readonly ViewDesc[], from: number, to: number, view: EditorView, replacement?: ViewDesc) { 1571 let result = [] 1572 for (let i = 0, off = 0; i < nodes.length; i++) { 1573 let child = nodes[i], start = off, end = off += child.size 1574 if (start >= to || end <= from) { 1575 result.push(child) 1576 } else { 1577 if (start < from) result.push((child as MarkViewDesc | TextViewDesc).slice(0, from - start, view)) 1578 if (replacement) { 1579 result.push(replacement) 1580 replacement = undefined 1581 } 1582 if (end > to) result.push((child as MarkViewDesc | TextViewDesc).slice(to - start, child.size, view)) 1583 } 1584 } 1585 return result 1586 }