prosemirror-state
Advanced tools
Comparing version 0.19.1 to 0.20.0
;var assign; | ||
((assign = require("./selection"), exports.Selection = assign.Selection, exports.TextSelection = assign.TextSelection, exports.NodeSelection = assign.NodeSelection)) | ||
((assign = require("./selection"), exports.Selection = assign.Selection, exports.SelectionRange = assign.SelectionRange, exports.TextSelection = assign.TextSelection, exports.NodeSelection = assign.NodeSelection, exports.AllSelection = assign.AllSelection, assign)) | ||
@@ -9,2 +9,2 @@ exports.Transaction = require("./transaction").Transaction | ||
;var assign$1; | ||
((assign$1 = require("./plugin"), exports.Plugin = assign$1.Plugin, exports.PluginKey = assign$1.PluginKey)) | ||
((assign$1 = require("./plugin"), exports.Plugin = assign$1.Plugin, exports.PluginKey = assign$1.PluginKey, assign$1)) |
@@ -51,2 +51,12 @@ // PluginSpec:: Object | ||
function bindProps(obj, self, target) { | ||
for (var prop in obj) { | ||
var val = obj[prop] | ||
if (val instanceof Function) { val = val.bind(self) } | ||
else if (prop == "handleDOMEvents") { val = bindProps(val, self, {}) } | ||
target[prop] = val | ||
} | ||
return target | ||
} | ||
// ::- Plugins wrap extra functionality that can be added to an | ||
@@ -56,12 +66,6 @@ // editor. They can define new [state fields](#state.StateField), and | ||
var Plugin = function Plugin(spec) { | ||
var this$1 = this; | ||
// :: EditorProps | ||
// The props exported by this plugin. | ||
this.props = {} | ||
if (spec.props) { for (var prop in spec.props) { | ||
var val = spec.props[prop] | ||
if (val instanceof Function) { val = val.bind(this$1) } | ||
this$1.props[prop] = val | ||
} } | ||
if (spec.props) { bindProps(spec.props, this, this.props) } | ||
// :: Object | ||
@@ -68,0 +72,0 @@ // The plugin's configuration object. |
@@ -1,2 +0,4 @@ | ||
var warnedAboutBetween = false | ||
var ref = require("prosemirror-model"); | ||
var Slice = ref.Slice; | ||
var Fragment = ref.Fragment; | ||
@@ -6,5 +8,6 @@ var classesById = Object.create(null) | ||
// ::- Superclass for editor selections. | ||
var Selection = function Selection($anchor, $head) { | ||
if ( $head === void 0 ) $head = $anchor; | ||
var Selection = function Selection($anchor, $head, ranges) { | ||
// :: [SelectionRange] | ||
// The ranges covered by the selection. | ||
this.ranges = ranges || [new SelectionRange($anchor.min($head), $anchor.max($head))] | ||
// :: ResolvedPos | ||
@@ -20,3 +23,3 @@ // The resolved anchor of the selection (the side that stays in | ||
var prototypeAccessors = { anchor: {},head: {},$from: {},$to: {},from: {},to: {},empty: {} }; | ||
var prototypeAccessors = { anchor: {},head: {},from: {},to: {},$from: {},$to: {},empty: {} }; | ||
@@ -33,26 +36,29 @@ // :: number | ||
// :: number | ||
// The lower bound of the selection's first range. | ||
prototypeAccessors.from.get = function () { return this.$from.pos }; | ||
// :: number | ||
// The upper bound of the selection's first range. | ||
prototypeAccessors.to.get = function () { return this.$to.pos }; | ||
// :: ResolvedPos | ||
// The resolved lower bound of the selection. | ||
// The resolved lowerbound of the selection's main range. | ||
prototypeAccessors.$from.get = function () { | ||
return this.$head.pos < this.$anchor.pos ? this.$head : this.$anchor | ||
return this.ranges[0].$from | ||
}; | ||
// :: ResolvedPos | ||
// The resolved upper bound of the selection. | ||
// The resolved upper bound of the selection's main range. | ||
prototypeAccessors.$to.get = function () { | ||
return this.$head.pos < this.$anchor.pos ? this.$anchor : this.$head | ||
return this.ranges[0].$to | ||
}; | ||
// :: number | ||
// The lower bound of the selection. | ||
prototypeAccessors.from.get = function () { return this.$from.pos }; | ||
// :: number | ||
// The upper bound of the selection. | ||
prototypeAccessors.to.get = function () { return this.$to.pos }; | ||
// :: bool | ||
// True if the selection is empty (head and anchor are the same). | ||
// Indicates whether the selection contains any content. | ||
prototypeAccessors.empty.get = function () { | ||
return this.head == this.anchor | ||
var ranges = this.ranges | ||
for (var i = 0; i < ranges.length; i++) | ||
{ if (ranges[i].$from.pos != ranges[i].$to.pos) { return false } } | ||
return true | ||
}; | ||
@@ -64,5 +70,2 @@ | ||
// head, and anchor. | ||
Selection.prototype.eq = function eq (other) { | ||
return other instanceof this.constructor && other.anchor == this.anchor && other.head == this.head | ||
}; | ||
@@ -73,2 +76,55 @@ // map:: (doc: Node, mapping: Mappable) → Selection | ||
// :: Slice | ||
// Get the content of this selection as a slice. | ||
Selection.prototype.content = function content () { | ||
return this.$from.node(0).slice(this.from, this.to, true) | ||
}; | ||
// :: (Transaction, ?Slice) | ||
// Replace the selection with a slice or, if no slice is given, | ||
// delete the selection. Will append to the given transaction. | ||
Selection.prototype.replace = function replace (tr, content) { | ||
if ( content === void 0 ) content = Slice.empty; | ||
// Put the new selection at the position after the inserted | ||
// content. When that ended in an inline node, search backwards, | ||
// to get the position after that node. If not, search forward. | ||
var lastNode = content.content.lastChild, lastParent = null | ||
for (var i = 0; i < content.openRight; i++) { | ||
lastParent = lastNode | ||
lastNode = lastNode.lastChild | ||
} | ||
var mapFrom = tr.steps.length, ranges = this.ranges | ||
for (var i$1 = 0; i$1 < ranges.length; i$1++) { | ||
var ref = ranges[i$1]; | ||
var $from = ref.$from; | ||
var $to = ref.$to; | ||
var mapping = tr.mapping.slice(mapFrom) | ||
tr.replaceRange(mapping.map($from.pos), mapping.map($to.pos), i$1 ? Slice.empty : content) | ||
if (i$1 == 0) | ||
{ selectionToInsertionEnd(tr, mapFrom, (lastNode ? lastNode.isInline : lastParent && lastParent.isTextblock) ? -1 : 1) } | ||
} | ||
}; | ||
// :: (Transaction, Node) | ||
// Replace the selection with the given node, appending the changes | ||
// to the given transaction. | ||
Selection.prototype.replaceWith = function replaceWith (tr, node) { | ||
var mapFrom = tr.steps.length, ranges = this.ranges | ||
for (var i = 0; i < ranges.length; i++) { | ||
var ref = ranges[i]; | ||
var $from = ref.$from; | ||
var $to = ref.$to; | ||
var mapping = tr.mapping.slice(mapFrom) | ||
var from = mapping.map($from.pos), to = mapping.map($to.pos) | ||
if (i) { | ||
tr.deleteRange(from, to) | ||
} else { | ||
tr.replaceRangeWith(from, to, node) | ||
selectionToInsertionEnd(tr, mapFrom, node.isInline ? -1 : 1) | ||
} | ||
} | ||
}; | ||
// toJSON:: () → Object | ||
@@ -80,5 +136,2 @@ // Convert the selection to a JSON representation. When implementing | ||
// implementation adds `type`, `head`, and `anchor` properties. | ||
Selection.prototype.toJSON = function toJSON () { | ||
return {type: this.jsonID, anchor: this.anchor, head: this.head} | ||
}; | ||
@@ -103,11 +156,10 @@ // :: (ResolvedPos, number, ?bool) → ?Selection | ||
// :: (ResolvedPos, ?number, ?bool) → Selection | ||
// :: (ResolvedPos, ?number) → Selection | ||
// Find a valid cursor or leaf node selection near the given | ||
// position. Searches forward first by default, but if `bias` is | ||
// negative, it will search backwards first. | ||
Selection.near = function near ($pos, bias, textOnly) { | ||
Selection.near = function near ($pos, bias) { | ||
if ( bias === void 0 ) bias = 1; | ||
if ( textOnly === void 0 ) textOnly = false; | ||
var result = this.findFrom($pos, bias, textOnly) || this.findFrom($pos, -bias, textOnly) | ||
var result = this.findFrom($pos, bias) || this.findFrom($pos, -bias) | ||
if (!result) { throw new RangeError("Searching for selection in invalid document " + $pos.node(0)) } | ||
@@ -133,23 +185,2 @@ return result | ||
Selection.between = function between ($anchor, $head, bias) { | ||
if (!warnedAboutBetween && typeof console != "undefined" && console.warn) { | ||
warnedAboutBetween = true | ||
console.warn("Selection.between is now called TextSelection.between") | ||
} | ||
return TextSelection.between($anchor, $head, bias) | ||
}; | ||
// : (Object, Mapping) → Object | ||
// Map a JSON object representing this selection through a mapping. | ||
Selection.mapJSON = function mapJSON (json, mapping) { | ||
var result = {} | ||
for (var prop in json) { | ||
var value = json[prop] | ||
if (prop == "anchor" || prop == "head") | ||
{ value = mapping.map(value, json.type == "node" && prop == "head" ? -1 : 1) } | ||
result[prop] = value | ||
} | ||
return result | ||
}; | ||
// :: (Node, Object) → Selection | ||
@@ -182,2 +213,14 @@ // Deserialize a JSON representation of a selection. Must be | ||
// :: () → SelectionBookmark | ||
// Get a [bookmark](#state.SelectionBookmark) for this selection, | ||
// which is a value that can be mapped without having access to a | ||
// current document, and later resolved to a real selection for a | ||
// given document again. (This is used mostly by the history to | ||
// track and restore old selections.) The default implementation of | ||
// this method just converts the selection to a text selection and | ||
// returns the bookmark for that. | ||
Selection.prototype.getBookmark = function getBookmark () { | ||
return TextSelection.between(this.anchor, this.head).getBookmark() | ||
}; | ||
Object.defineProperties( Selection.prototype, prototypeAccessors ); | ||
@@ -192,9 +235,36 @@ exports.Selection = Selection | ||
// ::- A text selection represents a classical editor | ||
// selection, with a head (the moving side) and anchor (immobile | ||
// side), both of which point into textblock nodes. It can be empty (a | ||
// regular cursor position). | ||
// SelectionBookmark:: interface | ||
// A lightweight, document-independent representation of a selection. | ||
// You can define a custom bookmark type for a custom selection class | ||
// to make the history handle it well. | ||
// | ||
// map:: (mapping: Mapping) → SelectionBookmark | ||
// Map the bookmark through a set of changes. | ||
// | ||
// resolve:: (doc: Node) → Selection | ||
// Resolve the bookmark to a real selection again. This may need to | ||
// do some error checking and may fall back to a default (usually | ||
// [`TextSelection.between`](#state.TextSelection.between) if | ||
// mapping made the bookmark invalid. | ||
// ::- Represents a selected range in a document. | ||
var SelectionRange = function SelectionRange($from, $to) { | ||
// :: ResolvedPos | ||
// The lower bound of the range. | ||
this.$from = $from | ||
// :: ResolvedPos | ||
// The upper bound of the range. | ||
this.$to = $to | ||
}; | ||
exports.SelectionRange = SelectionRange | ||
// ::- A text selection represents a classical editor selection, with | ||
// a head (the moving side) and anchor (immobile side), both of which | ||
// point into textblock nodes. It can be empty (a regular cursor | ||
// position). | ||
var TextSelection = (function (Selection) { | ||
function TextSelection () { | ||
Selection.apply(this, arguments); | ||
function TextSelection($anchor, $head) { | ||
if ( $head === void 0 ) $head = $anchor; | ||
Selection.call(this, $anchor, $head) | ||
} | ||
@@ -208,3 +278,6 @@ | ||
prototypeAccessors$1.$cursor.get = function () { return this.empty ? this.$head : null }; | ||
// :: ?ResolvedPos | ||
// Returns a resolved position if this is a cursor selection (an | ||
// empty text selection), and null otherwise. | ||
prototypeAccessors$1.$cursor.get = function () { return this.$anchor.pos == this.$head.pos ? this.$head : null }; | ||
@@ -218,2 +291,28 @@ TextSelection.prototype.map = function map (doc, mapping) { | ||
TextSelection.prototype.replace = function replace (tr, content) { | ||
if ( content === void 0 ) content = Slice.empty; | ||
Selection.prototype.replace.call(this, tr, content) | ||
if (content == Slice.empty) { | ||
if (this.$from.parentOffset < this.$from.parent.content.size) | ||
{ tr.ensureMarks(this.$from.marks(true)) } | ||
} | ||
}; | ||
TextSelection.prototype.eq = function eq (other) { | ||
return other instanceof TextSelection && other.anchor == this.anchor && other.head == this.head | ||
}; | ||
TextSelection.prototype.getBookmark = function getBookmark () { | ||
return new TextBookmark(this.anchor, this.head) | ||
}; | ||
TextSelection.prototype.toJSON = function toJSON () { | ||
return {type: "text", anchor: this.anchor, head: this.head} | ||
}; | ||
TextSelection.fromJSON = function fromJSON (doc, json) { | ||
return new TextSelection(doc.resolve(json.anchor), doc.resolve(json.head)) | ||
}; | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -228,14 +327,24 @@ // Create a text selection from non-resolved positions. | ||
// :: (ResolvedPos, ResolvedPos, ?number) → TextSelection | ||
// :: (ResolvedPos, ResolvedPos, ?number) → Selection | ||
// Return a text selection that spans the given positions or, if | ||
// they aren't text positions, find a text selection near them. | ||
// `bias` determines whether the method searches forward (default) | ||
// or backwards (negative number) first. | ||
// or backwards (negative number) first. Will fall back to returning | ||
// a node selection when the document doesn't contain a valid text | ||
// position. | ||
TextSelection.between = function between ($anchor, $head, bias) { | ||
var dir = $anchor.pos > $head.pos ? -1 : 1 | ||
if (!$head.parent.inlineContent) | ||
{ $head = Selection.near($head, bias || -dir, true).$head } | ||
var dPos = $anchor.pos - $head.pos | ||
if (!bias || dPos) { bias = dPos >= 0 ? 1 : -1 } | ||
if (!$head.parent.inlineContent) { | ||
var found = Selection.findFrom($head, bias, true) || Selection.findFrom($head, -bias, true) | ||
if (found) { $head = found.$head } | ||
else { return Selection.near($head, bias) } | ||
} | ||
if (!$anchor.parent.inlineContent) { | ||
$anchor = Selection.near($anchor, dir, true).$anchor | ||
if (($anchor.pos > $head.pos) != (dir < 0)) { $anchor = $head } | ||
if (dPos == 0) { | ||
$anchor = $head | ||
} else { | ||
$anchor = (Selection.findFrom($anchor, -bias, true) || Selection.findFrom($anchor, bias, true)).$anchor | ||
if (($anchor.pos < $head.pos) != (dPos < 0)) { $anchor = $head } | ||
} | ||
} | ||
@@ -245,10 +354,2 @@ return new TextSelection($anchor, $head) | ||
TextSelection.fromJSON = function fromJSON (doc, json) { | ||
// This is cautious, because the history will blindly map | ||
// selections and then try to deserialize them, and the endpoints | ||
// might not point at appropriate positions anymore (though they | ||
// are guaranteed to be inside of the document's range). | ||
return TextSelection.between(doc.resolve(json.anchor), doc.resolve(json.head)) | ||
}; | ||
Object.defineProperties( TextSelection.prototype, prototypeAccessors$1 ); | ||
@@ -262,2 +363,13 @@ | ||
var TextBookmark = function TextBookmark(anchor, head) { | ||
this.anchor = anchor | ||
this.head = head | ||
}; | ||
TextBookmark.prototype.map = function map (mapping) { | ||
return new TextBookmark(mapping.map(this.anchor), mapping.map(this.head)) | ||
}; | ||
TextBookmark.prototype.resolve = function resolve (doc) { | ||
return TextSelection.between(doc.resolve(this.anchor), doc.resolve(this.head)) | ||
}; | ||
// ::- A node selection is a selection that points at a | ||
@@ -268,7 +380,8 @@ // single node. All nodes marked [selectable](#model.NodeSpec.selectable) | ||
var NodeSelection = (function (Selection) { | ||
function NodeSelection($from) { | ||
var $to = $from.node(0).resolve($from.pos + $from.nodeAfter.nodeSize) | ||
Selection.call(this, $from, $to) | ||
function NodeSelection($pos) { | ||
var node = $pos.nodeAfter | ||
var $end = $pos.node(0).resolve($pos.pos + node.nodeSize) | ||
Selection.call(this, $pos, $end) | ||
// :: Node The selected node. | ||
this.node = $from.nodeAfter | ||
this.node = node | ||
} | ||
@@ -281,9 +394,28 @@ | ||
NodeSelection.prototype.map = function map (doc, mapping) { | ||
var from = mapping.mapResult(this.anchor, 1), to = mapping.mapResult(this.head, -1) | ||
var $from = doc.resolve(from.pos), node = $from.nodeAfter | ||
if (!from.deleted && !to.deleted && node && to.pos == from.pos + node.nodeSize && NodeSelection.isSelectable(node)) | ||
{ return new NodeSelection($from) } | ||
return Selection.near($from) | ||
var ref = mapping.mapResult(this.anchor); | ||
var deleted = ref.deleted; | ||
var pos = ref.pos; | ||
var $pos = doc.resolve(pos) | ||
if (deleted) { return Selection.near($pos) } | ||
return new NodeSelection($pos) | ||
}; | ||
NodeSelection.prototype.content = function content () { | ||
return new Slice(Fragment.from(this.node), 0, 0) | ||
}; | ||
NodeSelection.prototype.eq = function eq (other) { | ||
return other instanceof NodeSelection && other.anchor == this.anchor | ||
}; | ||
NodeSelection.prototype.toJSON = function toJSON () { | ||
return {type: "node", anchor: this.anchor} | ||
}; | ||
NodeSelection.prototype.getBookmark = function getBookmark () { return new NodeBookmark(this.anchor) }; | ||
NodeSelection.fromJSON = function fromJSON (doc, json) { | ||
return new NodeSelection(doc.resolve(json.anchor)) | ||
}; | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -302,8 +434,2 @@ // Create a node selection from non-resolved positions. | ||
NodeSelection.fromJSON = function fromJSON (doc, json) { | ||
var $from = doc.resolve(json.anchor), node = $from.nodeAfter | ||
if (node && json.head == json.anchor + node.nodeSize && NodeSelection.isSelectable(node)) { return new NodeSelection($from) } | ||
else { return Selection.near($from) } | ||
}; | ||
return NodeSelection; | ||
@@ -317,2 +443,51 @@ }(Selection)); | ||
var NodeBookmark = function NodeBookmark(anchor) { | ||
this.anchor = anchor | ||
}; | ||
NodeBookmark.prototype.map = function map (mapping) { | ||
var ref = mapping.mapResult(this.anchor); | ||
var deleted = ref.deleted; | ||
var pos = ref.pos; | ||
return deleted ? new TextBookmark(pos, pos) : new NodeBookmark(pos) | ||
}; | ||
NodeBookmark.prototype.resolve = function resolve (doc) { | ||
var $pos = doc.resolve(this.anchor), node = $pos.nodeAfter | ||
if (node && NodeSelection.isSelectable(node)) { return new NodeSelection($pos) } | ||
return Selection.near($pos) | ||
}; | ||
// ::- A selection type that represents selecting the whole document | ||
// (which can not necessarily be expressed with a text selection, when | ||
// there are for example leaf block nodes at the start or end of the | ||
// document). | ||
var AllSelection = (function (Selection) { | ||
function AllSelection(doc) { | ||
Selection.call(this, doc.resolve(0), doc.resolve(doc.content.size)) | ||
} | ||
if ( Selection ) AllSelection.__proto__ = Selection; | ||
AllSelection.prototype = Object.create( Selection && Selection.prototype ); | ||
AllSelection.prototype.constructor = AllSelection; | ||
AllSelection.prototype.toJSON = function toJSON () { return {type: "all"} }; | ||
AllSelection.fromJSON = function fromJSON (doc) { return new AllSelection(doc) }; | ||
AllSelection.prototype.map = function map (doc) { return new AllSelection(doc) }; | ||
AllSelection.prototype.eq = function eq (other) { return other instanceof AllSelection }; | ||
AllSelection.prototype.getBookmark = function getBookmark () { return AllBookmark }; | ||
return AllSelection; | ||
}(Selection)); | ||
exports.AllSelection = AllSelection | ||
Selection.jsonID("all", AllSelection) | ||
var AllBookmark = { | ||
map: function map() { return this }, | ||
resolve: function resolve(doc) { return new AllSelection(doc) } | ||
} | ||
// FIXME we'll need some awareness of text direction when scanning for selections | ||
@@ -336,1 +511,8 @@ | ||
} | ||
function selectionToInsertionEnd(tr, startLen, bias) { | ||
if (tr.steps.length == startLen) { return } | ||
var map = tr.mapping.maps[tr.mapping.maps.length - 1], end | ||
map.forEach(function (_from, _to, _newFrom, newTo) { return end = newTo; }) | ||
if (end != null) { tr.setSelection(Selection.near(tr.doc.resolve(end), bias)) } | ||
} |
@@ -32,3 +32,3 @@ var ref = require("prosemirror-model"); | ||
init: function init() { return null }, | ||
apply: function apply(tr, _marks, _old, state) { return state.selection.empty ? tr.storedMarks : null } | ||
apply: function apply(tr, _marks, _old, state) { return state.selection.$cursor ? tr.storedMarks : null } | ||
}), | ||
@@ -139,2 +139,3 @@ | ||
if (tr$1 && newState.filterTransaction(tr$1, i)) { | ||
tr$1.setMeta("appendedTransaction", tr$1) | ||
if (!seen) { | ||
@@ -141,0 +142,0 @@ seen = [] |
@@ -5,4 +5,2 @@ var ref = require("prosemirror-transform"); | ||
var Mark = ref$1.Mark; | ||
var ref$2 = require("./selection"); | ||
var Selection = ref$2.Selection; | ||
@@ -123,16 +121,3 @@ var UPDATED_SEL = 1, UPDATED_MARKS = 2, UPDATED_SCROLL = 4 | ||
Transaction.prototype.replaceSelection = function replaceSelection (slice) { | ||
var ref = this.selection; | ||
var from = ref.from; | ||
var to = ref.to; | ||
var startLen = this.steps.length | ||
this.replaceRange(from, to, slice) | ||
// Move the selection to the position after the inserted content. | ||
// When that ended in an inline node, search backwards, to get the | ||
// position after that node. If not, search forward. | ||
var lastNode = slice.content.lastChild, lastParent = null | ||
for (var i = 0; i < slice.openRight; i++) { | ||
lastParent = lastNode | ||
lastNode = lastNode.lastChild | ||
} | ||
selectionToInsertionEnd(this, startLen, (lastNode ? lastNode.isInline : lastParent && lastParent.isTextblock) ? -1 : 1) | ||
this.selection.replace(this, slice) | ||
return this | ||
@@ -147,11 +132,6 @@ }; | ||
Transaction.prototype.replaceSelectionWith = function replaceSelectionWith (node, inheritMarks) { | ||
var ref = this.selection; | ||
var $from = ref.$from; | ||
var from = ref.from; | ||
var to = ref.to; | ||
var startLen = this.steps.length | ||
var selection = this.selection | ||
if (inheritMarks !== false) | ||
{ node = node.mark(this.storedMarks || $from.marks(to > from)) } | ||
this.replaceRangeWith(from, to, node) | ||
selectionToInsertionEnd(this, startLen, node.isInline ? -1 : 1) | ||
{ node = node.mark(this.storedMarks || selection.$from.marks(selection.to > selection.from)) } | ||
selection.replaceWith(this, node) | ||
return this | ||
@@ -163,8 +143,3 @@ }; | ||
Transaction.prototype.deleteSelection = function deleteSelection () { | ||
var ref = this.selection; | ||
var from = ref.from; | ||
var to = ref.to; | ||
var $from = ref.$from; | ||
this.deleteRange(from, to) | ||
if ($from.parentOffset < $from.parent.content.size) { this.ensureMarks($from.marks(true)) } | ||
this.selection.replace(this) | ||
return this | ||
@@ -243,8 +218,1 @@ }; | ||
exports.Transaction = Transaction | ||
function selectionToInsertionEnd(tr, startLen, bias) { | ||
if (tr.steps.length == startLen) { return } | ||
var map = tr.mapping.maps[tr.mapping.maps.length - 1], end | ||
map.forEach(function (_from, _to, _newFrom, newTo) { return end = newTo; }) | ||
if (end != null) { tr.setSelection(Selection.near(tr.doc.resolve(end), bias)) } | ||
} |
{ | ||
"name": "prosemirror-state", | ||
"version": "0.19.1", | ||
"version": "0.20.0", | ||
"description": "ProseMirror editor state", | ||
@@ -19,4 +19,4 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"prosemirror-model": "^0.19.0", | ||
"prosemirror-transform": "^0.19.0" | ||
"prosemirror-model": "^0.20.0", | ||
"prosemirror-transform": "^0.20.0" | ||
}, | ||
@@ -27,3 +27,4 @@ "devDependencies": { | ||
"ist": "^1.0.0", | ||
"rimraf": "^2.5.4" | ||
"rimraf": "^2.5.4", | ||
"prosemirror-test-builder": "^0.20.0" | ||
}, | ||
@@ -30,0 +31,0 @@ "scripts": { |
;({Selection: exports.Selection, | ||
SelectionRange: exports.SelectionRange, | ||
TextSelection: exports.TextSelection, | ||
NodeSelection: exports.NodeSelection} = require("./selection")) | ||
NodeSelection: exports.NodeSelection, | ||
AllSelection: exports.AllSelection} = require("./selection")) | ||
@@ -5,0 +7,0 @@ exports.Transaction = require("./transaction").Transaction |
@@ -51,2 +51,12 @@ // PluginSpec:: Object | ||
function bindProps(obj, self, target) { | ||
for (let prop in obj) { | ||
let val = obj[prop] | ||
if (val instanceof Function) val = val.bind(self) | ||
else if (prop == "handleDOMEvents") val = bindProps(val, self, {}) | ||
target[prop] = val | ||
} | ||
return target | ||
} | ||
// ::- Plugins wrap extra functionality that can be added to an | ||
@@ -62,7 +72,3 @@ // editor. They can define new [state fields](#state.StateField), and | ||
this.props = {} | ||
if (spec.props) for (let prop in spec.props) { | ||
let val = spec.props[prop] | ||
if (val instanceof Function) val = val.bind(this) | ||
this.props[prop] = val | ||
} | ||
if (spec.props) bindProps(spec.props, this, this.props) | ||
// :: Object | ||
@@ -69,0 +75,0 @@ // The plugin's configuration object. |
@@ -24,3 +24,7 @@ This module implements the state object of a ProseMirror editor, along | ||
@NodeSelection | ||
@AllSelection | ||
@SelectionRange | ||
@SelectionBookmark | ||
### Plugin System | ||
@@ -27,0 +31,0 @@ |
@@ -1,2 +0,2 @@ | ||
let warnedAboutBetween = false | ||
const {Slice, Fragment} = require("prosemirror-model") | ||
@@ -7,3 +7,10 @@ const classesById = Object.create(null) | ||
class Selection { | ||
constructor($anchor, $head = $anchor) { | ||
// :: (ResolvedPos, ResolvedPos, ?[SelectionRange]) | ||
// Initialize a selection with the head and anchor and ranges. If no | ||
// ranges are given, constructs a single range across `$anchor` and | ||
// `$head`. | ||
constructor($anchor, $head, ranges) { | ||
// :: [SelectionRange] | ||
// The ranges covered by the selection. | ||
this.ranges = ranges || [new SelectionRange($anchor.min($head), $anchor.max($head))] | ||
// :: ResolvedPos | ||
@@ -29,26 +36,29 @@ // The resolved anchor of the selection (the side that stays in | ||
// :: number | ||
// The lower bound of the selection's first range. | ||
get from() { return this.$from.pos } | ||
// :: number | ||
// The upper bound of the selection's first range. | ||
get to() { return this.$to.pos } | ||
// :: ResolvedPos | ||
// The resolved lower bound of the selection. | ||
// The resolved lower bound of the selection's main range. | ||
get $from() { | ||
return this.$head.pos < this.$anchor.pos ? this.$head : this.$anchor | ||
return this.ranges[0].$from | ||
} | ||
// :: ResolvedPos | ||
// The resolved upper bound of the selection. | ||
// The resolved upper bound of the selection's main range. | ||
get $to() { | ||
return this.$head.pos < this.$anchor.pos ? this.$anchor : this.$head | ||
return this.ranges[0].$to | ||
} | ||
// :: number | ||
// The lower bound of the selection. | ||
get from() { return this.$from.pos } | ||
// :: number | ||
// The upper bound of the selection. | ||
get to() { return this.$to.pos } | ||
// :: bool | ||
// True if the selection is empty (head and anchor are the same). | ||
// Indicates whether the selection contains any content. | ||
get empty() { | ||
return this.head == this.anchor | ||
let ranges = this.ranges | ||
for (let i = 0; i < ranges.length; i++) | ||
if (ranges[i].$from.pos != ranges[i].$to.pos) return false | ||
return true | ||
} | ||
@@ -60,5 +70,2 @@ | ||
// head, and anchor. | ||
eq(other) { | ||
return other instanceof this.constructor && other.anchor == this.anchor && other.head == this.head | ||
} | ||
@@ -69,2 +76,47 @@ // map:: (doc: Node, mapping: Mappable) → Selection | ||
// :: Slice | ||
// Get the content of this selection as a slice. | ||
content() { | ||
return this.$from.node(0).slice(this.from, this.to, true) | ||
} | ||
// :: (Transaction, ?Slice) | ||
// Replace the selection with a slice or, if no slice is given, | ||
// delete the selection. Will append to the given transaction. | ||
replace(tr, content = Slice.empty) { | ||
// Put the new selection at the position after the inserted | ||
// content. When that ended in an inline node, search backwards, | ||
// to get the position after that node. If not, search forward. | ||
let lastNode = content.content.lastChild, lastParent = null | ||
for (let i = 0; i < content.openRight; i++) { | ||
lastParent = lastNode | ||
lastNode = lastNode.lastChild | ||
} | ||
let mapFrom = tr.steps.length, ranges = this.ranges | ||
for (let i = 0; i < ranges.length; i++) { | ||
let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom) | ||
tr.replaceRange(mapping.map($from.pos), mapping.map($to.pos), i ? Slice.empty : content) | ||
if (i == 0) | ||
selectionToInsertionEnd(tr, mapFrom, (lastNode ? lastNode.isInline : lastParent && lastParent.isTextblock) ? -1 : 1) | ||
} | ||
} | ||
// :: (Transaction, Node) | ||
// Replace the selection with the given node, appending the changes | ||
// to the given transaction. | ||
replaceWith(tr, node) { | ||
let mapFrom = tr.steps.length, ranges = this.ranges | ||
for (let i = 0; i < ranges.length; i++) { | ||
let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom) | ||
let from = mapping.map($from.pos), to = mapping.map($to.pos) | ||
if (i) { | ||
tr.deleteRange(from, to) | ||
} else { | ||
tr.replaceRangeWith(from, to, node) | ||
selectionToInsertionEnd(tr, mapFrom, node.isInline ? -1 : 1) | ||
} | ||
} | ||
} | ||
// toJSON:: () → Object | ||
@@ -76,5 +128,2 @@ // Convert the selection to a JSON representation. When implementing | ||
// implementation adds `type`, `head`, and `anchor` properties. | ||
toJSON() { | ||
return {type: this.jsonID, anchor: this.anchor, head: this.head} | ||
} | ||
@@ -99,8 +148,8 @@ // :: (ResolvedPos, number, ?bool) → ?Selection | ||
// :: (ResolvedPos, ?number, ?bool) → Selection | ||
// :: (ResolvedPos, ?number) → Selection | ||
// Find a valid cursor or leaf node selection near the given | ||
// position. Searches forward first by default, but if `bias` is | ||
// negative, it will search backwards first. | ||
static near($pos, bias = 1, textOnly = false) { | ||
let result = this.findFrom($pos, bias, textOnly) || this.findFrom($pos, -bias, textOnly) | ||
static near($pos, bias = 1) { | ||
let result = this.findFrom($pos, bias) || this.findFrom($pos, -bias) | ||
if (!result) throw new RangeError("Searching for selection in invalid document " + $pos.node(0)) | ||
@@ -126,23 +175,2 @@ return result | ||
static between($anchor, $head, bias) { | ||
if (!warnedAboutBetween && typeof console != "undefined" && console.warn) { | ||
warnedAboutBetween = true | ||
console.warn("Selection.between is now called TextSelection.between") | ||
} | ||
return TextSelection.between($anchor, $head, bias) | ||
} | ||
// : (Object, Mapping) → Object | ||
// Map a JSON object representing this selection through a mapping. | ||
static mapJSON(json, mapping) { | ||
let result = {} | ||
for (let prop in json) { | ||
let value = json[prop] | ||
if (prop == "anchor" || prop == "head") | ||
value = mapping.map(value, json.type == "node" && prop == "head" ? -1 : 1) | ||
result[prop] = value | ||
} | ||
return result | ||
} | ||
// :: (Node, Object) → Selection | ||
@@ -174,2 +202,14 @@ // Deserialize a JSON representation of a selection. Must be | ||
} | ||
// :: () → SelectionBookmark | ||
// Get a [bookmark](#state.SelectionBookmark) for this selection, | ||
// which is a value that can be mapped without having access to a | ||
// current document, and later resolved to a real selection for a | ||
// given document again. (This is used mostly by the history to | ||
// track and restore old selections.) The default implementation of | ||
// this method just converts the selection to a text selection and | ||
// returns the bookmark for that. | ||
getBookmark() { | ||
return TextSelection.between(this.anchor, this.head).getBookmark() | ||
} | ||
} | ||
@@ -184,11 +224,45 @@ exports.Selection = Selection | ||
// ::- A text selection represents a classical editor | ||
// selection, with a head (the moving side) and anchor (immobile | ||
// side), both of which point into textblock nodes. It can be empty (a | ||
// regular cursor position). | ||
// SelectionBookmark:: interface | ||
// A lightweight, document-independent representation of a selection. | ||
// You can define a custom bookmark type for a custom selection class | ||
// to make the history handle it well. | ||
// | ||
// map:: (mapping: Mapping) → SelectionBookmark | ||
// Map the bookmark through a set of changes. | ||
// | ||
// resolve:: (doc: Node) → Selection | ||
// Resolve the bookmark to a real selection again. This may need to | ||
// do some error checking and may fall back to a default (usually | ||
// [`TextSelection.between`](#state.TextSelection.between) if | ||
// mapping made the bookmark invalid. | ||
// ::- Represents a selected range in a document. | ||
class SelectionRange { | ||
// :: (ResolvedPos, ResolvedPos) | ||
constructor($from, $to) { | ||
// :: ResolvedPos | ||
// The lower bound of the range. | ||
this.$from = $from | ||
// :: ResolvedPos | ||
// The upper bound of the range. | ||
this.$to = $to | ||
} | ||
} | ||
exports.SelectionRange = SelectionRange | ||
// ::- A text selection represents a classical editor selection, with | ||
// a head (the moving side) and anchor (immobile side), both of which | ||
// point into textblock nodes. It can be empty (a regular cursor | ||
// position). | ||
class TextSelection extends Selection { | ||
// :: (ResolvedPos, ?ResolvedPos) | ||
// Construct a text selection between the given points. | ||
constructor($anchor, $head = $anchor) { | ||
super($anchor, $head) | ||
} | ||
// :: ?ResolvedPos | ||
// Returns a resolved position if this is a cursor selection (an | ||
// empty text selection), and null otherwise. | ||
get $cursor() { return this.empty ? this.$head : null } | ||
get $cursor() { return this.$anchor.pos == this.$head.pos ? this.$head : null } | ||
@@ -202,2 +276,26 @@ map(doc, mapping) { | ||
replace(tr, content = Slice.empty) { | ||
super.replace(tr, content) | ||
if (content == Slice.empty) { | ||
if (this.$from.parentOffset < this.$from.parent.content.size) | ||
tr.ensureMarks(this.$from.marks(true)) | ||
} | ||
} | ||
eq(other) { | ||
return other instanceof TextSelection && other.anchor == this.anchor && other.head == this.head | ||
} | ||
getBookmark() { | ||
return new TextBookmark(this.anchor, this.head) | ||
} | ||
toJSON() { | ||
return {type: "text", anchor: this.anchor, head: this.head} | ||
} | ||
static fromJSON(doc, json) { | ||
return new TextSelection(doc.resolve(json.anchor), doc.resolve(json.head)) | ||
} | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -210,25 +308,27 @@ // Create a text selection from non-resolved positions. | ||
// :: (ResolvedPos, ResolvedPos, ?number) → TextSelection | ||
// :: (ResolvedPos, ResolvedPos, ?number) → Selection | ||
// Return a text selection that spans the given positions or, if | ||
// they aren't text positions, find a text selection near them. | ||
// `bias` determines whether the method searches forward (default) | ||
// or backwards (negative number) first. | ||
// or backwards (negative number) first. Will fall back to returning | ||
// a node selection when the document doesn't contain a valid text | ||
// position. | ||
static between($anchor, $head, bias) { | ||
let dir = $anchor.pos > $head.pos ? -1 : 1 | ||
if (!$head.parent.inlineContent) | ||
$head = Selection.near($head, bias || -dir, true).$head | ||
let dPos = $anchor.pos - $head.pos | ||
if (!bias || dPos) bias = dPos >= 0 ? 1 : -1 | ||
if (!$head.parent.inlineContent) { | ||
let found = Selection.findFrom($head, bias, true) || Selection.findFrom($head, -bias, true) | ||
if (found) $head = found.$head | ||
else return Selection.near($head, bias) | ||
} | ||
if (!$anchor.parent.inlineContent) { | ||
$anchor = Selection.near($anchor, dir, true).$anchor | ||
if (($anchor.pos > $head.pos) != (dir < 0)) $anchor = $head | ||
if (dPos == 0) { | ||
$anchor = $head | ||
} else { | ||
$anchor = (Selection.findFrom($anchor, -bias, true) || Selection.findFrom($anchor, bias, true)).$anchor | ||
if (($anchor.pos < $head.pos) != (dPos < 0)) $anchor = $head | ||
} | ||
} | ||
return new TextSelection($anchor, $head) | ||
} | ||
static fromJSON(doc, json) { | ||
// This is cautious, because the history will blindly map | ||
// selections and then try to deserialize them, and the endpoints | ||
// might not point at appropriate positions anymore (though they | ||
// are guaranteed to be inside of the document's range). | ||
return TextSelection.between(doc.resolve(json.anchor), doc.resolve(json.head)) | ||
} | ||
} | ||
@@ -239,2 +339,15 @@ exports.TextSelection = TextSelection | ||
class TextBookmark { | ||
constructor(anchor, head) { | ||
this.anchor = anchor | ||
this.head = head | ||
} | ||
map(mapping) { | ||
return new TextBookmark(mapping.map(this.anchor), mapping.map(this.head)) | ||
} | ||
resolve(doc) { | ||
return TextSelection.between(doc.resolve(this.anchor), doc.resolve(this.head)) | ||
} | ||
} | ||
// ::- A node selection is a selection that points at a | ||
@@ -248,17 +361,35 @@ // single node. All nodes marked [selectable](#model.NodeSpec.selectable) | ||
// argument. | ||
constructor($from) { | ||
let $to = $from.node(0).resolve($from.pos + $from.nodeAfter.nodeSize) | ||
super($from, $to) | ||
constructor($pos) { | ||
let node = $pos.nodeAfter | ||
let $end = $pos.node(0).resolve($pos.pos + node.nodeSize) | ||
super($pos, $end) | ||
// :: Node The selected node. | ||
this.node = $from.nodeAfter | ||
this.node = node | ||
} | ||
map(doc, mapping) { | ||
let from = mapping.mapResult(this.anchor, 1), to = mapping.mapResult(this.head, -1) | ||
let $from = doc.resolve(from.pos), node = $from.nodeAfter | ||
if (!from.deleted && !to.deleted && node && to.pos == from.pos + node.nodeSize && NodeSelection.isSelectable(node)) | ||
return new NodeSelection($from) | ||
return Selection.near($from) | ||
let {deleted, pos} = mapping.mapResult(this.anchor) | ||
let $pos = doc.resolve(pos) | ||
if (deleted) return Selection.near($pos) | ||
return new NodeSelection($pos) | ||
} | ||
content() { | ||
return new Slice(Fragment.from(this.node), 0, 0) | ||
} | ||
eq(other) { | ||
return other instanceof NodeSelection && other.anchor == this.anchor | ||
} | ||
toJSON() { | ||
return {type: "node", anchor: this.anchor} | ||
} | ||
getBookmark() { return new NodeBookmark(this.anchor) } | ||
static fromJSON(doc, json) { | ||
return new NodeSelection(doc.resolve(json.anchor)) | ||
} | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -276,8 +407,2 @@ // Create a node selection from non-resolved positions. | ||
} | ||
static fromJSON(doc, json) { | ||
let $from = doc.resolve(json.anchor), node = $from.nodeAfter | ||
if (node && json.head == json.anchor + node.nodeSize && NodeSelection.isSelectable(node)) return new NodeSelection($from) | ||
else return Selection.near($from) | ||
} | ||
} | ||
@@ -290,2 +415,45 @@ exports.NodeSelection = NodeSelection | ||
class NodeBookmark { | ||
constructor(anchor) { | ||
this.anchor = anchor | ||
} | ||
map(mapping) { | ||
let {deleted, pos} = mapping.mapResult(this.anchor) | ||
return deleted ? new TextBookmark(pos, pos) : new NodeBookmark(pos) | ||
} | ||
resolve(doc) { | ||
let $pos = doc.resolve(this.anchor), node = $pos.nodeAfter | ||
if (node && NodeSelection.isSelectable(node)) return new NodeSelection($pos) | ||
return Selection.near($pos) | ||
} | ||
} | ||
// ::- A selection type that represents selecting the whole document | ||
// (which can not necessarily be expressed with a text selection, when | ||
// there are for example leaf block nodes at the start or end of the | ||
// document). | ||
class AllSelection extends Selection { | ||
constructor(doc) { | ||
super(doc.resolve(0), doc.resolve(doc.content.size)) | ||
} | ||
toJSON() { return {type: "all"} } | ||
static fromJSON(doc) { return new AllSelection(doc) } | ||
map(doc) { return new AllSelection(doc) } | ||
eq(other) { return other instanceof AllSelection } | ||
getBookmark() { return AllBookmark } | ||
} | ||
exports.AllSelection = AllSelection | ||
Selection.jsonID("all", AllSelection) | ||
const AllBookmark = { | ||
map() { return this }, | ||
resolve(doc) { return new AllSelection(doc) } | ||
} | ||
// FIXME we'll need some awareness of text direction when scanning for selections | ||
@@ -309,1 +477,8 @@ | ||
} | ||
function selectionToInsertionEnd(tr, startLen, bias) { | ||
if (tr.steps.length == startLen) return | ||
let map = tr.mapping.maps[tr.mapping.maps.length - 1], end | ||
map.forEach((_from, _to, _newFrom, newTo) => end = newTo) | ||
if (end != null) tr.setSelection(Selection.near(tr.doc.resolve(end), bias)) | ||
} |
@@ -31,3 +31,3 @@ const {Node} = require("prosemirror-model") | ||
init() { return null }, | ||
apply(tr, _marks, _old, state) { return state.selection.empty ? tr.storedMarks : null } | ||
apply(tr, _marks, _old, state) { return state.selection.$cursor ? tr.storedMarks : null } | ||
}), | ||
@@ -132,2 +132,3 @@ | ||
if (tr && newState.filterTransaction(tr, i)) { | ||
tr.setMeta("appendedTransaction", tr) | ||
if (!seen) { | ||
@@ -134,0 +135,0 @@ seen = [] |
const {Transform} = require("prosemirror-transform") | ||
const {Mark} = require("prosemirror-model") | ||
const {Selection} = require("./selection") | ||
@@ -113,13 +112,3 @@ const UPDATED_SEL = 1, UPDATED_MARKS = 2, UPDATED_SCROLL = 4 | ||
replaceSelection(slice) { | ||
let {from, to} = this.selection, startLen = this.steps.length | ||
this.replaceRange(from, to, slice) | ||
// Move the selection to the position after the inserted content. | ||
// When that ended in an inline node, search backwards, to get the | ||
// position after that node. If not, search forward. | ||
let lastNode = slice.content.lastChild, lastParent = null | ||
for (let i = 0; i < slice.openRight; i++) { | ||
lastParent = lastNode | ||
lastNode = lastNode.lastChild | ||
} | ||
selectionToInsertionEnd(this, startLen, (lastNode ? lastNode.isInline : lastParent && lastParent.isTextblock) ? -1 : 1) | ||
this.selection.replace(this, slice) | ||
return this | ||
@@ -134,7 +123,6 @@ } | ||
replaceSelectionWith(node, inheritMarks) { | ||
let {$from, from, to} = this.selection, startLen = this.steps.length | ||
let selection = this.selection | ||
if (inheritMarks !== false) | ||
node = node.mark(this.storedMarks || $from.marks(to > from)) | ||
this.replaceRangeWith(from, to, node) | ||
selectionToInsertionEnd(this, startLen, node.isInline ? -1 : 1) | ||
node = node.mark(this.storedMarks || selection.$from.marks(selection.to > selection.from)) | ||
selection.replaceWith(this, node) | ||
return this | ||
@@ -146,5 +134,3 @@ } | ||
deleteSelection() { | ||
let {from, to, $from} = this.selection | ||
this.deleteRange(from, to) | ||
if ($from.parentOffset < $from.parent.content.size) this.ensureMarks($from.marks(true)) | ||
this.selection.replace(this) | ||
return this | ||
@@ -215,8 +201,1 @@ } | ||
exports.Transaction = Transaction | ||
function selectionToInsertionEnd(tr, startLen, bias) { | ||
if (tr.steps.length == startLen) return | ||
let map = tr.mapping.maps[tr.mapping.maps.length - 1], end | ||
map.forEach((_from, _to, _newFrom, newTo) => end = newTo) | ||
if (end != null) tr.setSelection(Selection.near(tr.doc.resolve(end), bias)) | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
89099
1913
5
+ Addedprosemirror-model@0.20.0(transitive)
+ Addedprosemirror-transform@0.20.0(transitive)
- Removedprosemirror-model@0.19.0(transitive)
- Removedprosemirror-transform@0.19.0(transitive)
Updatedprosemirror-model@^0.20.0