You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
769 lines
29 KiB
769 lines
29 KiB
import {Node, Mark} from "prosemirror-model"
|
|
import {Mappable, Mapping} from "prosemirror-transform"
|
|
import {EditorView} from "./index"
|
|
import {DOMNode} from "./dom"
|
|
|
|
function compareObjs(a: {[prop: string]: any}, b: {[prop: string]: any}) {
|
|
if (a == b) return true
|
|
for (let p in a) if (a[p] !== b[p]) return false
|
|
for (let p in b) if (!(p in a)) return false
|
|
return true
|
|
}
|
|
|
|
export interface DecorationType {
|
|
spec: any
|
|
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null
|
|
valid(node: Node, span: Decoration): boolean
|
|
eq(other: DecorationType): boolean
|
|
destroy(dom: DOMNode): void
|
|
}
|
|
|
|
export type WidgetConstructor = ((view: EditorView, getPos: () => number | undefined) => DOMNode) | DOMNode
|
|
|
|
export class WidgetType implements DecorationType {
|
|
spec: any
|
|
side: number
|
|
|
|
constructor(readonly toDOM: WidgetConstructor, spec: any) {
|
|
this.spec = spec || noSpec
|
|
this.side = this.spec.side || 0
|
|
}
|
|
|
|
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null {
|
|
let {pos, deleted} = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1)
|
|
return deleted ? null : new Decoration(pos - offset, pos - offset, this)
|
|
}
|
|
|
|
valid() { return true }
|
|
|
|
eq(other: WidgetType) {
|
|
return this == other ||
|
|
(other instanceof WidgetType &&
|
|
(this.spec.key && this.spec.key == other.spec.key ||
|
|
this.toDOM == other.toDOM && compareObjs(this.spec, other.spec)))
|
|
}
|
|
|
|
destroy(node: DOMNode) {
|
|
if (this.spec.destroy) this.spec.destroy(node)
|
|
}
|
|
}
|
|
|
|
export class InlineType implements DecorationType {
|
|
spec: any
|
|
|
|
constructor(readonly attrs: DecorationAttrs, spec: any) {
|
|
this.spec = spec || noSpec
|
|
}
|
|
|
|
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null {
|
|
let from = mapping.map(span.from + oldOffset, this.spec.inclusiveStart ? -1 : 1) - offset
|
|
let to = mapping.map(span.to + oldOffset, this.spec.inclusiveEnd ? 1 : -1) - offset
|
|
return from >= to ? null : new Decoration(from, to, this)
|
|
}
|
|
|
|
valid(_: Node, span: Decoration) { return span.from < span.to }
|
|
|
|
eq(other: DecorationType): boolean {
|
|
return this == other ||
|
|
(other instanceof InlineType && compareObjs(this.attrs, other.attrs) &&
|
|
compareObjs(this.spec, other.spec))
|
|
}
|
|
|
|
static is(span: Decoration) { return span.type instanceof InlineType }
|
|
|
|
destroy() {}
|
|
}
|
|
|
|
export class NodeType implements DecorationType {
|
|
spec: any
|
|
constructor(readonly attrs: DecorationAttrs, spec: any) {
|
|
this.spec = spec || noSpec
|
|
}
|
|
|
|
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null {
|
|
let from = mapping.mapResult(span.from + oldOffset, 1)
|
|
if (from.deleted) return null
|
|
let to = mapping.mapResult(span.to + oldOffset, -1)
|
|
if (to.deleted || to.pos <= from.pos) return null
|
|
return new Decoration(from.pos - offset, to.pos - offset, this)
|
|
}
|
|
|
|
valid(node: Node, span: Decoration): boolean {
|
|
let {index, offset} = node.content.findIndex(span.from), child
|
|
return offset == span.from && !(child = node.child(index)).isText && offset + child.nodeSize == span.to
|
|
}
|
|
|
|
eq(other: DecorationType): boolean {
|
|
return this == other ||
|
|
(other instanceof NodeType && compareObjs(this.attrs, other.attrs) &&
|
|
compareObjs(this.spec, other.spec))
|
|
}
|
|
|
|
destroy() {}
|
|
}
|
|
|
|
/// Decoration objects can be provided to the view through the
|
|
/// [`decorations` prop](#view.EditorProps.decorations). They come in
|
|
/// several variants—see the static members of this class for details.
|
|
export class Decoration {
|
|
/// @internal
|
|
constructor(
|
|
/// The start position of the decoration.
|
|
readonly from: number,
|
|
/// The end position. Will be the same as `from` for [widget
|
|
/// decorations](#view.Decoration^widget).
|
|
readonly to: number,
|
|
/// @internal
|
|
readonly type: DecorationType
|
|
) {}
|
|
|
|
/// @internal
|
|
copy(from: number, to: number) {
|
|
return new Decoration(from, to, this.type)
|
|
}
|
|
|
|
/// @internal
|
|
eq(other: Decoration, offset = 0) {
|
|
return this.type.eq(other.type) && this.from + offset == other.from && this.to + offset == other.to
|
|
}
|
|
|
|
/// @internal
|
|
map(mapping: Mappable, offset: number, oldOffset: number) {
|
|
return this.type.map(mapping, this, offset, oldOffset)
|
|
}
|
|
|
|
/// Creates a widget decoration, which is a DOM node that's shown in
|
|
/// the document at the given position. It is recommended that you
|
|
/// delay rendering the widget by passing a function that will be
|
|
/// called when the widget is actually drawn in a view, but you can
|
|
/// also directly pass a DOM node. `getPos` can be used to find the
|
|
/// widget's current document position.
|
|
static widget(pos: number, toDOM: WidgetConstructor, spec?: {
|
|
/// Controls which side of the document position this widget is
|
|
/// associated with. When negative, it is drawn before a cursor
|
|
/// at its position, and content inserted at that position ends
|
|
/// up after the widget. When zero (the default) or positive, the
|
|
/// widget is drawn after the cursor and content inserted there
|
|
/// ends up before the widget.
|
|
///
|
|
/// When there are multiple widgets at a given position, their
|
|
/// `side` values determine the order in which they appear. Those
|
|
/// with lower values appear first. The ordering of widgets with
|
|
/// the same `side` value is unspecified.
|
|
///
|
|
/// When `marks` is null, `side` also determines the marks that
|
|
/// the widget is wrapped in—those of the node before when
|
|
/// negative, those of the node after when positive.
|
|
side?: number
|
|
|
|
/// The precise set of marks to draw around the widget.
|
|
marks?: readonly Mark[]
|
|
|
|
/// Can be used to control which DOM events, when they bubble out
|
|
/// of this widget, the editor view should ignore.
|
|
stopEvent?: (event: Event) => boolean
|
|
|
|
/// When set (defaults to false), selection changes inside the
|
|
/// widget are ignored, and don't cause ProseMirror to try and
|
|
/// re-sync the selection with its selection state.
|
|
ignoreSelection?: boolean
|
|
|
|
/// When comparing decorations of this type (in order to decide
|
|
/// whether it needs to be redrawn), ProseMirror will by default
|
|
/// compare the widget DOM node by identity. If you pass a key,
|
|
/// that key will be compared instead, which can be useful when
|
|
/// you generate decorations on the fly and don't want to store
|
|
/// and reuse DOM nodes. Make sure that any widgets with the same
|
|
/// key are interchangeable—if widgets differ in, for example,
|
|
/// the behavior of some event handler, they should get
|
|
/// different keys.
|
|
key?: string
|
|
|
|
/// Called when the widget decoration is removed as a result of
|
|
/// mapping
|
|
destroy?: (node: DOMNode) => void
|
|
|
|
/// Specs allow arbitrary additional properties.
|
|
[key: string]: any
|
|
}): Decoration {
|
|
return new Decoration(pos, pos, new WidgetType(toDOM, spec))
|
|
}
|
|
|
|
/// Creates an inline decoration, which adds the given attributes to
|
|
/// each inline node between `from` and `to`.
|
|
static inline(from: number, to: number, attrs: DecorationAttrs, spec?: {
|
|
/// Determines how the left side of the decoration is
|
|
/// [mapped](#transform.Position_Mapping) when content is
|
|
/// inserted directly at that position. By default, the decoration
|
|
/// won't include the new content, but you can set this to `true`
|
|
/// to make it inclusive.
|
|
inclusiveStart?: boolean
|
|
|
|
/// Determines how the right side of the decoration is mapped.
|
|
/// See
|
|
/// [`inclusiveStart`](#view.Decoration^inline^spec.inclusiveStart).
|
|
inclusiveEnd?: boolean
|
|
|
|
/// Specs may have arbitrary additional properties.
|
|
[key: string]: any
|
|
}) {
|
|
return new Decoration(from, to, new InlineType(attrs, spec))
|
|
}
|
|
|
|
/// Creates a node decoration. `from` and `to` should point precisely
|
|
/// before and after a node in the document. That node, and only that
|
|
/// node, will receive the given attributes.
|
|
static node(from: number, to: number, attrs: DecorationAttrs, spec?: any) {
|
|
return new Decoration(from, to, new NodeType(attrs, spec))
|
|
}
|
|
|
|
/// The spec provided when creating this decoration. Can be useful
|
|
/// if you've stored extra information in that object.
|
|
get spec() { return this.type.spec }
|
|
|
|
/// @internal
|
|
get inline() { return this.type instanceof InlineType }
|
|
}
|
|
|
|
/// A set of attributes to add to a decorated node. Most properties
|
|
/// simply directly correspond to DOM attributes of the same name,
|
|
/// which will be set to the property's value. These are exceptions:
|
|
export type DecorationAttrs = {
|
|
/// When non-null, the target node is wrapped in a DOM element of
|
|
/// this type (and the other attributes are applied to this element).
|
|
nodeName?: string
|
|
|
|
/// A CSS class name or a space-separated set of class names to be
|
|
/// _added_ to the classes that the node already had.
|
|
class?: string
|
|
|
|
/// A string of CSS to be _added_ to the node's existing `style` property.
|
|
style?: string
|
|
|
|
/// Any other properties are treated as regular DOM attributes.
|
|
[attribute: string]: string | undefined
|
|
}
|
|
|
|
const none: readonly any[] = [], noSpec = {}
|
|
|
|
/// An object that can [provide](#view.EditorProps.decorations)
|
|
/// decorations. Implemented by [`DecorationSet`](#view.DecorationSet),
|
|
/// and passed to [node views](#view.EditorProps.nodeViews).
|
|
export interface DecorationSource {
|
|
/// Map the set of decorations in response to a change in the
|
|
/// document.
|
|
map: (mapping: Mapping, node: Node) => DecorationSource
|
|
/// @internal
|
|
locals(node: Node): readonly Decoration[]
|
|
/// @internal
|
|
forChild(offset: number, child: Node): DecorationSource
|
|
/// @internal
|
|
eq(other: DecorationSource): boolean
|
|
}
|
|
|
|
/// A collection of [decorations](#view.Decoration), organized in such
|
|
/// a way that the drawing algorithm can efficiently use and compare
|
|
/// them. This is a persistent data structure—it is not modified,
|
|
/// updates create a new value.
|
|
export class DecorationSet implements DecorationSource {
|
|
/// @internal
|
|
local: readonly Decoration[]
|
|
/// @internal
|
|
children: readonly (number | DecorationSet)[]
|
|
|
|
/// @internal
|
|
constructor(local: readonly Decoration[], children: readonly (number | DecorationSet)[]) {
|
|
this.local = local.length ? local : none
|
|
this.children = children.length ? children : none
|
|
}
|
|
|
|
/// Create a set of decorations, using the structure of the given
|
|
/// document.
|
|
static create(doc: Node, decorations: Decoration[]) {
|
|
return decorations.length ? buildTree(decorations, doc, 0, noSpec) : empty
|
|
}
|
|
|
|
/// Find all decorations in this set which touch the given range
|
|
/// (including decorations that start or end directly at the
|
|
/// boundaries) and match the given predicate on their spec. When
|
|
/// `start` and `end` are omitted, all decorations in the set are
|
|
/// considered. When `predicate` isn't given, all decorations are
|
|
/// assumed to match.
|
|
find(start?: number, end?: number, predicate?: (spec: any) => boolean): Decoration[] {
|
|
let result: Decoration[] = []
|
|
this.findInner(start == null ? 0 : start, end == null ? 1e9 : end, result, 0, predicate)
|
|
return result
|
|
}
|
|
|
|
private findInner(start: number, end: number, result: Decoration[], offset: number, predicate?: (spec: any) => boolean) {
|
|
for (let i = 0; i < this.local.length; i++) {
|
|
let span = this.local[i]
|
|
if (span.from <= end && span.to >= start && (!predicate || predicate(span.spec)))
|
|
result.push(span.copy(span.from + offset, span.to + offset))
|
|
}
|
|
for (let i = 0; i < this.children.length; i += 3) {
|
|
if (this.children[i] < end && this.children[i + 1] > start) {
|
|
let childOff = (this.children[i] as number) + 1
|
|
;(this.children[i + 2] as DecorationSet).findInner(start - childOff, end - childOff,
|
|
result, offset + childOff, predicate)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Map the set of decorations in response to a change in the
|
|
/// document.
|
|
map(mapping: Mapping, doc: Node, options?: {
|
|
/// When given, this function will be called for each decoration
|
|
/// that gets dropped as a result of the mapping, passing the
|
|
/// spec of that decoration.
|
|
onRemove?: (decorationSpec: any) => void
|
|
}) {
|
|
if (this == empty || mapping.maps.length == 0) return this
|
|
return this.mapInner(mapping, doc, 0, 0, options || noSpec)
|
|
}
|
|
|
|
/// @internal
|
|
mapInner(mapping: Mapping, node: Node, offset: number, oldOffset: number, options: {
|
|
onRemove?: (decorationSpec: any) => void
|
|
}) {
|
|
let newLocal: Decoration[] | undefined
|
|
for (let i = 0; i < this.local.length; i++) {
|
|
let mapped = this.local[i].map(mapping, offset, oldOffset)
|
|
if (mapped && mapped.type.valid(node, mapped)) (newLocal || (newLocal = [])).push(mapped)
|
|
else if (options.onRemove) options.onRemove(this.local[i].spec)
|
|
}
|
|
|
|
if (this.children.length)
|
|
return mapChildren(this.children, newLocal || [], mapping, node, offset, oldOffset, options)
|
|
else
|
|
return newLocal ? new DecorationSet(newLocal.sort(byPos), none) : empty
|
|
}
|
|
|
|
/// Add the given array of decorations to the ones in the set,
|
|
/// producing a new set. Needs access to the current document to
|
|
/// create the appropriate tree structure.
|
|
add(doc: Node, decorations: Decoration[]) {
|
|
if (!decorations.length) return this
|
|
if (this == empty) return DecorationSet.create(doc, decorations)
|
|
return this.addInner(doc, decorations, 0)
|
|
}
|
|
|
|
private addInner(doc: Node, decorations: Decoration[], offset: number) {
|
|
let children: (number | DecorationSet)[] | undefined, childIndex = 0
|
|
doc.forEach((childNode, childOffset) => {
|
|
let baseOffset = childOffset + offset, found
|
|
if (!(found = takeSpansForNode(decorations, childNode, baseOffset))) return
|
|
|
|
if (!children) children = this.children.slice()
|
|
while (childIndex < children.length && children[childIndex] < childOffset) childIndex += 3
|
|
if (children[childIndex] == childOffset)
|
|
children[childIndex + 2] = (children[childIndex + 2] as DecorationSet).addInner(childNode, found, baseOffset + 1)
|
|
else
|
|
children.splice(childIndex, 0, childOffset, childOffset + childNode.nodeSize, buildTree(found, childNode, baseOffset + 1, noSpec))
|
|
childIndex += 3
|
|
})
|
|
|
|
let local = moveSpans(childIndex ? withoutNulls(decorations) : decorations, -offset)
|
|
for (let i = 0; i < local.length; i++) if (!local[i].type.valid(doc, local[i])) local.splice(i--, 1)
|
|
|
|
return new DecorationSet(local.length ? this.local.concat(local).sort(byPos) : this.local,
|
|
children || this.children)
|
|
}
|
|
|
|
/// Create a new set that contains the decorations in this set, minus
|
|
/// the ones in the given array.
|
|
remove(decorations: Decoration[]) {
|
|
if (decorations.length == 0 || this == empty) return this
|
|
return this.removeInner(decorations, 0)
|
|
}
|
|
|
|
private removeInner(decorations: (Decoration | null)[], offset: number) {
|
|
let children = this.children as (number | DecorationSet)[], local = this.local as Decoration[]
|
|
for (let i = 0; i < children.length; i += 3) {
|
|
let found: Decoration[] | undefined
|
|
let from = (children[i] as number) + offset, to = (children[i + 1] as number) + offset
|
|
for (let j = 0, span; j < decorations.length; j++) if (span = decorations[j]) {
|
|
if (span.from > from && span.to < to) {
|
|
decorations[j] = null
|
|
;(found || (found = [])).push(span)
|
|
}
|
|
}
|
|
if (!found) continue
|
|
if (children == this.children) children = this.children.slice()
|
|
let removed = (children[i + 2] as DecorationSet).removeInner(found, from + 1)
|
|
if (removed != empty) {
|
|
children[i + 2] = removed
|
|
} else {
|
|
children.splice(i, 3)
|
|
i -= 3
|
|
}
|
|
}
|
|
if (local.length) for (let i = 0, span; i < decorations.length; i++) if (span = decorations[i]) {
|
|
for (let j = 0; j < local.length; j++) if (local[j].eq(span, offset)) {
|
|
if (local == this.local) local = this.local.slice()
|
|
local.splice(j--, 1)
|
|
}
|
|
}
|
|
if (children == this.children && local == this.local) return this
|
|
return local.length || children.length ? new DecorationSet(local, children) : empty
|
|
}
|
|
|
|
/// @internal
|
|
forChild(offset: number, node: Node): DecorationSet | DecorationGroup {
|
|
if (this == empty) return this
|
|
if (node.isLeaf) return DecorationSet.empty
|
|
|
|
let child, local: Decoration[] | undefined
|
|
for (let i = 0; i < this.children.length; i += 3) if (this.children[i] >= offset) {
|
|
if (this.children[i] == offset) child = this.children[i + 2] as DecorationSet
|
|
break
|
|
}
|
|
let start = offset + 1, end = start + node.content.size
|
|
for (let i = 0; i < this.local.length; i++) {
|
|
let dec = this.local[i]
|
|
if (dec.from < end && dec.to > start && (dec.type instanceof InlineType)) {
|
|
let from = Math.max(start, dec.from) - start, to = Math.min(end, dec.to) - start
|
|
if (from < to) (local || (local = [])).push(dec.copy(from, to))
|
|
}
|
|
}
|
|
if (local) {
|
|
let localSet = new DecorationSet(local.sort(byPos), none)
|
|
return child ? new DecorationGroup([localSet, child]) : localSet
|
|
}
|
|
return child || empty
|
|
}
|
|
|
|
/// @internal
|
|
eq(other: DecorationSet) {
|
|
if (this == other) return true
|
|
if (!(other instanceof DecorationSet) ||
|
|
this.local.length != other.local.length ||
|
|
this.children.length != other.children.length) return false
|
|
for (let i = 0; i < this.local.length; i++)
|
|
if (!this.local[i].eq(other.local[i])) return false
|
|
for (let i = 0; i < this.children.length; i += 3)
|
|
if (this.children[i] != other.children[i] ||
|
|
this.children[i + 1] != other.children[i + 1] ||
|
|
!(this.children[i + 2] as DecorationSet).eq(other.children[i + 2] as DecorationSet))
|
|
return false
|
|
return true
|
|
}
|
|
|
|
/// @internal
|
|
locals(node: Node) {
|
|
return removeOverlap(this.localsInner(node))
|
|
}
|
|
|
|
/// @internal
|
|
localsInner(node: Node): readonly Decoration[] {
|
|
if (this == empty) return none
|
|
if (node.inlineContent || !this.local.some(InlineType.is)) return this.local
|
|
let result = []
|
|
for (let i = 0; i < this.local.length; i++) {
|
|
if (!(this.local[i].type instanceof InlineType))
|
|
result.push(this.local[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// The empty set of decorations.
|
|
static empty: DecorationSet = new DecorationSet([], [])
|
|
|
|
/// @internal
|
|
static removeOverlap = removeOverlap
|
|
}
|
|
|
|
const empty = DecorationSet.empty
|
|
|
|
// An abstraction that allows the code dealing with decorations to
|
|
// treat multiple DecorationSet objects as if it were a single object
|
|
// with (a subset of) the same interface.
|
|
class DecorationGroup implements DecorationSource {
|
|
constructor(readonly members: readonly DecorationSet[]) {}
|
|
|
|
map(mapping: Mapping, doc: Node) {
|
|
const mappedDecos = this.members.map(
|
|
member => member.map(mapping, doc, noSpec)
|
|
)
|
|
return DecorationGroup.from(mappedDecos)
|
|
}
|
|
|
|
forChild(offset: number, child: Node) {
|
|
if (child.isLeaf) return DecorationSet.empty
|
|
let found: DecorationSet[] = []
|
|
for (let i = 0; i < this.members.length; i++) {
|
|
let result = this.members[i].forChild(offset, child)
|
|
if (result == empty) continue
|
|
if (result instanceof DecorationGroup) found = found.concat(result.members)
|
|
else found.push(result)
|
|
}
|
|
return DecorationGroup.from(found)
|
|
}
|
|
|
|
eq(other: DecorationGroup) {
|
|
if (!(other instanceof DecorationGroup) ||
|
|
other.members.length != this.members.length) return false
|
|
for (let i = 0; i < this.members.length; i++)
|
|
if (!this.members[i].eq(other.members[i])) return false
|
|
return true
|
|
}
|
|
|
|
locals(node: Node) {
|
|
let result: Decoration[] | undefined, sorted = true
|
|
for (let i = 0; i < this.members.length; i++) {
|
|
let locals = this.members[i].localsInner(node)
|
|
if (!locals.length) continue
|
|
if (!result) {
|
|
result = locals as Decoration[]
|
|
} else {
|
|
if (sorted) {
|
|
result = result.slice()
|
|
sorted = false
|
|
}
|
|
for (let j = 0; j < locals.length; j++) result.push(locals[j])
|
|
}
|
|
}
|
|
return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none
|
|
}
|
|
|
|
// Create a group for the given array of decoration sets, or return
|
|
// a single set when possible.
|
|
static from(members: readonly DecorationSource[]): DecorationSource {
|
|
switch (members.length) {
|
|
case 0: return empty
|
|
case 1: return members[0]
|
|
default: return new DecorationGroup(
|
|
members.every(m => m instanceof DecorationSet) ? members as DecorationSet[] :
|
|
members.reduce((r, m) => r.concat(m instanceof DecorationSet ? m : (m as DecorationGroup).members),
|
|
[] as DecorationSet[]))
|
|
}
|
|
}
|
|
}
|
|
|
|
function mapChildren(
|
|
oldChildren: readonly (number | DecorationSet)[],
|
|
newLocal: Decoration[],
|
|
mapping: Mapping,
|
|
node: Node,
|
|
offset: number,
|
|
oldOffset: number,
|
|
options: {onRemove?: (decorationSpec: any) => void}
|
|
) {
|
|
let children = oldChildren.slice() as (number | DecorationSet)[]
|
|
|
|
// Mark the children that are directly touched by changes, and
|
|
// move those that are after the changes.
|
|
for (let i = 0, baseOffset = oldOffset; i < mapping.maps.length; i++) {
|
|
let moved = 0
|
|
mapping.maps[i].forEach((oldStart: number, oldEnd: number, newStart: number, newEnd: number) => {
|
|
let dSize = (newEnd - newStart) - (oldEnd - oldStart)
|
|
for (let i = 0; i < children.length; i += 3) {
|
|
let end = children[i + 1] as number
|
|
if (end < 0 || oldStart > end + baseOffset - moved) continue
|
|
let start = (children[i] as number) + baseOffset - moved
|
|
if (oldEnd >= start) {
|
|
children[i + 1] = oldStart <= start ? -2 : -1
|
|
} else if (newStart >= offset && dSize) {
|
|
;(children[i] as number) += dSize
|
|
;(children[i + 1] as number) += dSize
|
|
}
|
|
}
|
|
moved += dSize
|
|
})
|
|
baseOffset = mapping.maps[i].map(baseOffset, -1)
|
|
}
|
|
|
|
// Find the child nodes that still correspond to a single node,
|
|
// recursively call mapInner on them and update their positions.
|
|
let mustRebuild = false
|
|
for (let i = 0; i < children.length; i += 3) if (children[i + 1] < 0) { // Touched nodes
|
|
if (children[i + 1] == -2) {
|
|
mustRebuild = true
|
|
children[i + 1] = -1
|
|
continue
|
|
}
|
|
let from = mapping.map((oldChildren[i] as number) + oldOffset), fromLocal = from - offset
|
|
if (fromLocal < 0 || fromLocal >= node.content.size) {
|
|
mustRebuild = true
|
|
continue
|
|
}
|
|
// Must read oldChildren because children was tagged with -1
|
|
let to = mapping.map((oldChildren[i + 1] as number) + oldOffset, -1), toLocal = to - offset
|
|
let {index, offset: childOffset} = node.content.findIndex(fromLocal)
|
|
let childNode = node.maybeChild(index)
|
|
if (childNode && childOffset == fromLocal && childOffset + childNode.nodeSize == toLocal) {
|
|
let mapped = (children[i + 2] as DecorationSet)
|
|
.mapInner(mapping, childNode, from + 1, (oldChildren[i] as number) + oldOffset + 1, options)
|
|
if (mapped != empty) {
|
|
children[i] = fromLocal
|
|
children[i + 1] = toLocal
|
|
children[i + 2] = mapped
|
|
} else {
|
|
children[i + 1] = -2
|
|
mustRebuild = true
|
|
}
|
|
} else {
|
|
mustRebuild = true
|
|
}
|
|
}
|
|
|
|
// Remaining children must be collected and rebuilt into the appropriate structure
|
|
if (mustRebuild) {
|
|
let decorations = mapAndGatherRemainingDecorations(children, oldChildren, newLocal, mapping,
|
|
offset, oldOffset, options)
|
|
let built = buildTree(decorations, node, 0, options)
|
|
newLocal = built.local as Decoration[]
|
|
for (let i = 0; i < children.length; i += 3) if (children[i + 1] < 0) {
|
|
children.splice(i, 3)
|
|
i -= 3
|
|
}
|
|
for (let i = 0, j = 0; i < built.children.length; i += 3) {
|
|
let from = built.children[i]
|
|
while (j < children.length && children[j] < from) j += 3
|
|
children.splice(j, 0, built.children[i], built.children[i + 1], built.children[i + 2])
|
|
}
|
|
}
|
|
|
|
return new DecorationSet(newLocal.sort(byPos), children)
|
|
}
|
|
|
|
function moveSpans(spans: Decoration[], offset: number) {
|
|
if (!offset || !spans.length) return spans
|
|
let result = []
|
|
for (let i = 0; i < spans.length; i++) {
|
|
let span = spans[i]
|
|
result.push(new Decoration(span.from + offset, span.to + offset, span.type))
|
|
}
|
|
return result
|
|
}
|
|
|
|
function mapAndGatherRemainingDecorations(
|
|
children: (number | DecorationSet)[],
|
|
oldChildren: readonly (number | DecorationSet)[],
|
|
decorations: Decoration[],
|
|
mapping: Mapping,
|
|
offset: number,
|
|
oldOffset: number,
|
|
options: {onRemove?: (decorationSpec: any) => void}
|
|
) {
|
|
// Gather all decorations from the remaining marked children
|
|
function gather(set: DecorationSet, oldOffset: number) {
|
|
for (let i = 0; i < set.local.length; i++) {
|
|
let mapped = set.local[i].map(mapping, offset, oldOffset)
|
|
if (mapped) decorations.push(mapped)
|
|
else if (options.onRemove) options.onRemove(set.local[i].spec)
|
|
}
|
|
for (let i = 0; i < set.children.length; i += 3)
|
|
gather(set.children[i + 2] as DecorationSet, set.children[i] as number + oldOffset + 1)
|
|
}
|
|
for (let i = 0; i < children.length; i += 3) if (children[i + 1] == -1)
|
|
gather(children[i + 2] as DecorationSet, oldChildren[i] as number + oldOffset + 1)
|
|
|
|
return decorations
|
|
}
|
|
|
|
function takeSpansForNode(spans: (Decoration | null)[], node: Node, offset: number): Decoration[] | null {
|
|
if (node.isLeaf) return null
|
|
let end = offset + node.nodeSize, found = null
|
|
for (let i = 0, span; i < spans.length; i++) {
|
|
if ((span = spans[i]) && span.from > offset && span.to < end) {
|
|
;(found || (found = [])).push(span)
|
|
spans[i] = null
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
function withoutNulls<T>(array: readonly (T | null)[]): T[] {
|
|
let result: T[] = []
|
|
for (let i = 0; i < array.length; i++)
|
|
if (array[i] != null) result.push(array[i]!)
|
|
return result
|
|
}
|
|
|
|
// Build up a tree that corresponds to a set of decorations. `offset`
|
|
// is a base offset that should be subtracted from the `from` and `to`
|
|
// positions in the spans (so that we don't have to allocate new spans
|
|
// for recursive calls).
|
|
function buildTree(
|
|
spans: Decoration[],
|
|
node: Node,
|
|
offset: number,
|
|
options: {onRemove?: (decorationSpec: any) => void}
|
|
) {
|
|
let children: (DecorationSet | number)[] = [], hasNulls = false
|
|
node.forEach((childNode, localStart) => {
|
|
let found = takeSpansForNode(spans, childNode, localStart + offset)
|
|
if (found) {
|
|
hasNulls = true
|
|
let subtree = buildTree(found, childNode, offset + localStart + 1, options)
|
|
if (subtree != empty)
|
|
children.push(localStart, localStart + childNode.nodeSize, subtree)
|
|
}
|
|
})
|
|
let locals = moveSpans(hasNulls ? withoutNulls(spans) : spans, -offset).sort(byPos)
|
|
for (let i = 0; i < locals.length; i++) if (!locals[i].type.valid(node, locals[i])) {
|
|
if (options.onRemove) options.onRemove(locals[i].spec)
|
|
locals.splice(i--, 1)
|
|
}
|
|
return locals.length || children.length ? new DecorationSet(locals, children) : empty
|
|
}
|
|
|
|
// Used to sort decorations so that ones with a low start position
|
|
// come first, and within a set with the same start position, those
|
|
// with an smaller end position come first.
|
|
function byPos(a: Decoration, b: Decoration) {
|
|
return a.from - b.from || a.to - b.to
|
|
}
|
|
|
|
// Scan a sorted array of decorations for partially overlapping spans,
|
|
// and split those so that only fully overlapping spans are left (to
|
|
// make subsequent rendering easier). Will return the input array if
|
|
// no partially overlapping spans are found (the common case).
|
|
function removeOverlap(spans: readonly Decoration[]): Decoration[] {
|
|
let working: Decoration[] = spans as Decoration[]
|
|
for (let i = 0; i < working.length - 1; i++) {
|
|
let span = working[i]
|
|
if (span.from != span.to) for (let j = i + 1; j < working.length; j++) {
|
|
let next = working[j]
|
|
if (next.from == span.from) {
|
|
if (next.to != span.to) {
|
|
if (working == spans) working = spans.slice()
|
|
// Followed by a partially overlapping larger span. Split that
|
|
// span.
|
|
working[j] = next.copy(next.from, span.to)
|
|
insertAhead(working, j + 1, next.copy(span.to, next.to))
|
|
}
|
|
continue
|
|
} else {
|
|
if (next.from < span.to) {
|
|
if (working == spans) working = spans.slice()
|
|
// The end of this one overlaps with a subsequent span. Split
|
|
// this one.
|
|
working[i] = span.copy(span.from, next.from)
|
|
insertAhead(working, j, span.copy(next.from, span.to))
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return working
|
|
}
|
|
|
|
function insertAhead(array: Decoration[], i: number, deco: Decoration) {
|
|
while (i < array.length && byPos(deco, array[i]) > 0) i++
|
|
array.splice(i, 0, deco)
|
|
}
|
|
|
|
// Get the decorations associated with the current props of a view.
|
|
export function viewDecorations(view: EditorView): DecorationSource {
|
|
let found: DecorationSource[] = []
|
|
view.someProp("decorations", f => {
|
|
let result = f(view.state)
|
|
if (result && result != empty) found.push(result)
|
|
})
|
|
if (view.cursorWrapper)
|
|
found.push(DecorationSet.create(view.state.doc, [view.cursorWrapper.deco]))
|
|
return DecorationGroup.from(found)
|
|
}
|