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

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)
}