prosemirror-state
Advanced tools
Comparing version 0.18.0 to 0.19.0
@@ -51,4 +51,2 @@ // PluginSpec:: Object | ||
var warnedAboutOptions = false | ||
// ::- Plugins wrap extra functionality that can be added to an | ||
@@ -74,17 +72,5 @@ // editor. They can define new [state fields](#state.StateField), and | ||
var prototypeAccessors = { options: {} }; | ||
prototypeAccessors.options.get = function () { | ||
if (!warnedAboutOptions && typeof "console" != "undefined" && console.warn) { | ||
warnedAboutOptions = true | ||
console.warn("Plugin.options was renamed to Plugin.spec") | ||
} | ||
return this.spec | ||
}; | ||
// :: (EditorState) → any | ||
// Get the state field for this plugin. | ||
Plugin.prototype.getState = function getState (state) { return state[this.key] }; | ||
Object.defineProperties( Plugin.prototype, prototypeAccessors ); | ||
exports.Plugin = Plugin | ||
@@ -102,3 +88,3 @@ | ||
// that `instance` is a half-initialized state instance, and will | ||
// not have values for any fields initialzed after this one. | ||
// not have values for any fields initialized after this one. | ||
// | ||
@@ -105,0 +91,0 @@ // apply:: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) → T |
@@ -0,16 +1,45 @@ | ||
var warnedAboutBetween = false | ||
var classesById = Object.create(null) | ||
// ::- Superclass for editor selections. | ||
var Selection = function Selection($from, $to) { | ||
var Selection = function Selection($anchor, $head) { | ||
if ( $head === void 0 ) $head = $anchor; | ||
// :: ResolvedPos | ||
// The resolved lower bound of the selection | ||
this.$from = $from | ||
// The resolved anchor of the selection (the side that stays in | ||
// place when the selection is modified). | ||
this.$anchor = $anchor | ||
// :: ResolvedPos | ||
// The resolved upper bound of the selection | ||
this.$to = $to | ||
// The resolved head of the selection (the side that moves when | ||
// the selection is modified). | ||
this.$head = $head | ||
}; | ||
var prototypeAccessors = { from: {},to: {},empty: {} }; | ||
var prototypeAccessors = { anchor: {},head: {},$from: {},$to: {},from: {},to: {},empty: {} }; | ||
// :: bool | ||
// True if the selection is an empty text selection (head an anchor | ||
// are the same). | ||
// :: number | ||
// The selection's immobile side (does not move when | ||
// shift-selecting). | ||
prototypeAccessors.anchor.get = function () { return this.$anchor.pos }; | ||
// :: number | ||
// The selection's mobile side (the side that moves when | ||
// shift-selecting). | ||
prototypeAccessors.head.get = function () { return this.$head.pos }; | ||
// :: ResolvedPos | ||
// The resolved lower bound of the selection. | ||
prototypeAccessors.$from.get = function () { | ||
return this.$head.pos < this.$anchor.pos ? this.$head : this.$anchor | ||
}; | ||
// :: ResolvedPos | ||
// The resolved upper bound of the selection. | ||
prototypeAccessors.$to.get = function () { | ||
return this.$head.pos < this.$anchor.pos ? this.$anchor : this.$head | ||
}; | ||
// :: number | ||
// The lower bound of the selection. | ||
prototypeAccessors.from.get = function () { return this.$from.pos }; | ||
@@ -22,8 +51,15 @@ | ||
// :: bool | ||
// True if the selection is empty (head and anchor are the same). | ||
prototypeAccessors.empty.get = function () { | ||
return this.from == this.to | ||
return this.head == this.anchor | ||
}; | ||
// eq:: (other: Selection) → bool | ||
// Test whether the selection is the same as another selection. | ||
// eq:: (Selection) → bool | ||
// Test whether the selection is the same as another selection. The | ||
// default implementation tests whether they have the same class, | ||
// head, and anchor. | ||
Selection.prototype.eq = function eq (other) { | ||
return other instanceof this.constructor && other.anchor == this.anchor && other.head == this.head | ||
}; | ||
@@ -35,3 +71,10 @@ // map:: (doc: Node, mapping: Mappable) → Selection | ||
// toJSON:: () → Object | ||
// Convert the selection to a JSON representation. | ||
// Convert the selection to a JSON representation. When implementing | ||
// this for a custom selection class, make sure to give the object a | ||
// `type` property whose value matches the ID under which you | ||
// [registered](#state.Selection^jsonID) your class. The default | ||
// implementation adds `type`, `head`, and `anchor` properties. | ||
Selection.prototype.toJSON = function toJSON () { | ||
return {type: this.jsonID, anchor: this.anchor, head: this.head} | ||
}; | ||
@@ -44,3 +87,3 @@ // :: (ResolvedPos, number, ?bool) → ?Selection | ||
Selection.findFrom = function findFrom ($pos, dir, textOnly) { | ||
var inner = $pos.parent.isTextblock ? new TextSelection($pos) | ||
var inner = $pos.parent.inlineContent ? new TextSelection($pos) | ||
: findSelectionIn($pos.node(0), $pos.parent, $pos.pos, $pos.index(), dir, textOnly) | ||
@@ -86,52 +129,59 @@ if (inner) { return inner } | ||
// :: (ResolvedPos, ResolvedPos, ?number) → Selection | ||
// Find a selection that spans the given positions, if both are text | ||
// positions. If not, return some other selection nearby, where | ||
// `bias` determines whether the method searches forward (default) | ||
// or backwards (negative number) first. | ||
Selection.between = function between ($anchor, $head, bias) { | ||
var found = Selection.near($head, bias) | ||
if (found instanceof TextSelection) { | ||
var nearAnchor = Selection.findFrom($anchor, $anchor.pos > found.to ? -1 : 1, true) | ||
found = new TextSelection(nearAnchor.$anchor, found.$head) | ||
} else if ($anchor.pos < found.from || $anchor.pos > found.to) { | ||
// If head falls on a node, but anchor falls outside of it, create | ||
// a text selection between them | ||
var inv = $anchor.pos > found.to | ||
var foundAnchor = Selection.findFrom($anchor, inv ? -1 : 1, true) | ||
var foundHead = Selection.findFrom(inv ? found.$from : found.$to, inv ? 1 : -1, true) | ||
if (foundAnchor && foundHead) | ||
{ found = new TextSelection(foundAnchor.$anchor, foundHead.$head) } | ||
if (!warnedAboutBetween && typeof console != "undefined" && console.warn) { | ||
warnedAboutBetween = true | ||
console.warn("Selection.between is now called TextSelection.between") | ||
} | ||
return found | ||
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) { | ||
if (json.anchor != null) | ||
{ return {head: mapping.map(json.head), anchor: mapping.map(json.anchor)} } | ||
else | ||
{ return {node: mapping.map(json.node), after: mapping.map(json.after, -1)} } | ||
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 | ||
// Deserialize a JSON representation of a selection. | ||
// Deserialize a JSON representation of a selection. Must be | ||
// implemented for custom classes (as a static class method). | ||
Selection.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). | ||
if (json.head != null) { | ||
var $anchor = doc.resolve(json.anchor), $head = doc.resolve(json.head) | ||
if ($anchor.parent.isTextblock && $head.parent.isTextblock) { return new TextSelection($anchor, $head) } | ||
else { return Selection.between($anchor, $head) } | ||
} else { | ||
var $pos = doc.resolve(json.node), after = $pos.nodeAfter | ||
if (after && json.after == json.pos + after.nodeSize && NodeSelection.isSelectable(after)) { return new NodeSelection($pos) } | ||
else { return Selection.near($pos) } | ||
} | ||
var cls = classesById[json.type] | ||
if (!cls) { return this.backwardsCompatFromJSON(doc, json) } | ||
return cls.fromJSON(doc, json) | ||
}; | ||
Selection.backwardsCompatFromJSON = function backwardsCompatFromJSON (doc, json) { | ||
if (json.anchor != null) { return TextSelection.fromJSON(doc, json) } | ||
if (json.node != null) { return NodeSelection.fromJSON(doc, {anchor: json.node, head: json.after}) } | ||
throw new RangeError("Unrecognized JSON data " + JSON.stringify(json)) | ||
}; | ||
// :: (string, constructor<Selection>) | ||
// To be able to deserialize selections from JSON, custom selection | ||
// classes must register themselves with an ID string, so that they | ||
// can be disambiguated. Try to pick something that's unlikely to | ||
// clash with classes from other modules. | ||
Selection.jsonID = function jsonID (id, selectionClass) { | ||
if (id in classesById) { throw new RangeError("Duplicate use of selection JSON ID " + id) } | ||
classesById[id] = selectionClass | ||
selectionClass.prototype.jsonID = id | ||
return selectionClass | ||
}; | ||
Object.defineProperties( Selection.prototype, prototypeAccessors ); | ||
exports.Selection = Selection | ||
// :: bool | ||
// Controls whether, when a selection of this type is active in the | ||
// browser, the selected range should be visible to the user. Defaults | ||
// to `true`. | ||
Selection.prototype.visible = true | ||
// ::- A text selection represents a classical editor | ||
@@ -142,11 +192,4 @@ // selection, with a head (the moving side) and anchor (immobile | ||
var TextSelection = (function (Selection) { | ||
function TextSelection($anchor, $head) { | ||
if ( $head === void 0 ) $head = $anchor; | ||
var inv = $anchor.pos > $head.pos | ||
Selection.call(this, inv ? $head : $anchor, inv ? $anchor : $head) | ||
// :: ResolvedPos The resolved anchor of the selection. | ||
this.$anchor = $anchor | ||
// :: ResolvedPos The resolved head of the selection. | ||
this.$head = $head | ||
function TextSelection () { | ||
Selection.apply(this, arguments); | ||
} | ||
@@ -158,27 +201,13 @@ | ||
var prototypeAccessors$1 = { anchor: {},head: {},inverted: {} }; | ||
var prototypeAccessors$1 = { $cursor: {} }; | ||
prototypeAccessors$1.anchor.get = function () { return this.$anchor.pos }; | ||
// :: number | ||
// The selection's mobile side (the side that moves when pressing | ||
// shift-arrow). | ||
prototypeAccessors$1.head.get = function () { return this.$head.pos }; | ||
prototypeAccessors$1.$cursor.get = function () { return this.empty ? this.$head : null }; | ||
prototypeAccessors$1.inverted.get = function () { return this.anchor > this.head }; | ||
TextSelection.prototype.eq = function eq (other) { | ||
return other instanceof TextSelection && other.head == this.head && other.anchor == this.anchor | ||
}; | ||
TextSelection.prototype.map = function map (doc, mapping) { | ||
var $head = doc.resolve(mapping.map(this.head)) | ||
if (!$head.parent.isTextblock) { return Selection.near($head) } | ||
if (!$head.parent.inlineContent) { return Selection.near($head) } | ||
var $anchor = doc.resolve(mapping.map(this.anchor)) | ||
return new TextSelection($anchor.parent.isTextblock ? $anchor : $head, $head) | ||
return new TextSelection($anchor.parent.inlineContent ? $anchor : $head, $head) | ||
}; | ||
TextSelection.prototype.toJSON = function toJSON () { | ||
return {head: this.head, anchor: this.anchor} | ||
}; | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -193,2 +222,26 @@ // Create a text selection from non-resolved positions. | ||
// :: (ResolvedPos, ResolvedPos, ?number) → TextSelection | ||
// 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. | ||
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 } | ||
if (!$anchor.parent.inlineContent) { | ||
$anchor = Selection.near($anchor, dir, true).$anchor | ||
if (($anchor.pos > $head.pos) != (dir < 0)) { $anchor = $head } | ||
} | ||
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 ); | ||
@@ -200,2 +253,4 @@ | ||
Selection.jsonID("text", TextSelection) | ||
// ::- A node selection is a selection that points at a | ||
@@ -217,8 +272,4 @@ // single node. All nodes marked [selectable](#model.NodeSpec.selectable) | ||
NodeSelection.prototype.eq = function eq (other) { | ||
return other instanceof NodeSelection && this.from == other.from | ||
}; | ||
NodeSelection.prototype.map = function map (doc, mapping) { | ||
var from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) | ||
var from = mapping.mapResult(this.anchor, 1), to = mapping.mapResult(this.head, -1) | ||
var $from = doc.resolve(from.pos), node = $from.nodeAfter | ||
@@ -230,6 +281,2 @@ if (!from.deleted && !to.deleted && node && to.pos == from.pos + node.nodeSize && NodeSelection.isSelectable(node)) | ||
NodeSelection.prototype.toJSON = function toJSON () { | ||
return {node: this.from, after: this.to} | ||
}; | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -248,2 +295,8 @@ // 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; | ||
@@ -253,2 +306,6 @@ }(Selection)); | ||
NodeSelection.prototype.visible = false | ||
Selection.jsonID("node", NodeSelection) | ||
// FIXME we'll need some awareness of text direction when scanning for selections | ||
@@ -260,3 +317,3 @@ | ||
function findSelectionIn(doc, node, pos, index, dir, text) { | ||
if (node.isTextblock) { return TextSelection.create(doc, pos) } | ||
if (node.inlineContent) { return TextSelection.create(doc, pos) } | ||
for (var i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) { | ||
@@ -263,0 +320,0 @@ var child = node.child(i) |
@@ -19,3 +19,3 @@ var ref = require("prosemirror-transform"); | ||
// metadata properties in a transaction, which are extra pieces of | ||
// informations that client code or plugins can use to describe what a | ||
// information that client code or plugins can use to describe what a | ||
// transacion represents, so that they can update their [own | ||
@@ -85,3 +85,3 @@ // state](##state.StateField) accordingly. | ||
// :: (?[Mark]) → Transaction | ||
// Replace the set of stored marks. | ||
// Set the current stored marks. | ||
Transaction.prototype.setStoredMarks = function setStoredMarks (marks) { | ||
@@ -93,2 +93,12 @@ this.storedMarks = marks | ||
// :: ([Mark]) → Transaction | ||
// Make sure the current stored marks or, if that is null, the marks | ||
// at the selection, match the given set of marks. Does nothing if | ||
// this is already the case. | ||
Transaction.prototype.ensureMarks = function ensureMarks (marks) { | ||
if (!Mark.sameSet(this.storedMarks || this.selection.$from.marks(), marks)) | ||
{ this.setStoredMarks(marks.length || this.storedMarks ? marks : null) } | ||
return this | ||
}; | ||
// :: bool | ||
@@ -156,3 +166,6 @@ // Whether the stored marks were explicitly set for this transaction. | ||
var to = ref.to; | ||
return this.deleteRange(from, to) | ||
var $from = ref.$from; | ||
this.deleteRange(from, to) | ||
if ($from.parentOffset < $from.parent.content.size) { this.ensureMarks($from.marks(true)) } | ||
return this | ||
}; | ||
@@ -216,4 +229,3 @@ | ||
Transaction.prototype.addStoredMark = function addStoredMark (mark) { | ||
this.storedMarks = mark.addToSet(this.storedMarks || currentMarks(this.selection)) | ||
return this | ||
return this.ensureMarks(mark.addToSet(this.storedMarks || this.selection.$head.marks())) | ||
}; | ||
@@ -224,4 +236,3 @@ | ||
Transaction.prototype.removeStoredMark = function removeStoredMark (mark) { | ||
this.storedMarks = mark.removeFromSet(this.storedMarks || currentMarks(this.selection)) | ||
return this | ||
return this.ensureMarks(mark.removeFromSet(this.storedMarks || this.selection.$head.marks())) | ||
}; | ||
@@ -241,5 +252,1 @@ | ||
} | ||
function currentMarks(selection) { | ||
return selection.head == null ? Mark.none : selection.$head.marks() | ||
} |
{ | ||
"name": "prosemirror-state", | ||
"version": "0.18.0", | ||
"version": "0.19.0", | ||
"description": "ProseMirror editor state", | ||
@@ -19,4 +19,4 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"prosemirror-model": "^0.18.0", | ||
"prosemirror-transform": "^0.18.0" | ||
"prosemirror-model": "^0.19.0", | ||
"prosemirror-transform": "^0.19.0" | ||
}, | ||
@@ -23,0 +23,0 @@ "devDependencies": { |
@@ -51,4 +51,2 @@ // PluginSpec:: Object | ||
let warnedAboutOptions = false | ||
// ::- Plugins wrap extra functionality that can be added to an | ||
@@ -75,10 +73,2 @@ // editor. They can define new [state fields](#state.StateField), and | ||
get options() { | ||
if (!warnedAboutOptions && typeof "console" != "undefined" && console.warn) { | ||
warnedAboutOptions = true | ||
console.warn("Plugin.options was renamed to Plugin.spec") | ||
} | ||
return this.spec | ||
} | ||
// :: (EditorState) → any | ||
@@ -100,3 +90,3 @@ // Get the state field for this plugin. | ||
// that `instance` is a half-initialized state instance, and will | ||
// not have values for any fields initialzed after this one. | ||
// not have values for any fields initialized after this one. | ||
// | ||
@@ -103,0 +93,0 @@ // apply:: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) → T |
@@ -24,3 +24,3 @@ This module implements the state object of a ProseMirror editor, along | ||
@NodeSelection | ||
c | ||
### Plugin System | ||
@@ -27,0 +27,0 @@ |
@@ -0,4 +1,41 @@ | ||
let warnedAboutBetween = false | ||
const classesById = Object.create(null) | ||
// ::- Superclass for editor selections. | ||
class Selection { | ||
constructor($anchor, $head = $anchor) { | ||
// :: ResolvedPos | ||
// The resolved anchor of the selection (the side that stays in | ||
// place when the selection is modified). | ||
this.$anchor = $anchor | ||
// :: ResolvedPos | ||
// The resolved head of the selection (the side that moves when | ||
// the selection is modified). | ||
this.$head = $head | ||
} | ||
// :: number | ||
// The selection's immobile side (does not move when | ||
// shift-selecting). | ||
get anchor() { return this.$anchor.pos } | ||
// :: number | ||
// The selection's mobile side (the side that moves when | ||
// shift-selecting). | ||
get head() { return this.$head.pos } | ||
// :: ResolvedPos | ||
// The resolved lower bound of the selection. | ||
get $from() { | ||
return this.$head.pos < this.$anchor.pos ? this.$head : this.$anchor | ||
} | ||
// :: ResolvedPos | ||
// The resolved upper bound of the selection. | ||
get $to() { | ||
return this.$head.pos < this.$anchor.pos ? this.$anchor : this.$head | ||
} | ||
// :: number | ||
// The lower bound of the selection. | ||
@@ -11,20 +48,15 @@ get from() { return this.$from.pos } | ||
constructor($from, $to) { | ||
// :: ResolvedPos | ||
// The resolved lower bound of the selection | ||
this.$from = $from | ||
// :: ResolvedPos | ||
// The resolved upper bound of the selection | ||
this.$to = $to | ||
} | ||
// :: bool | ||
// True if the selection is an empty text selection (head an anchor | ||
// are the same). | ||
// True if the selection is empty (head and anchor are the same). | ||
get empty() { | ||
return this.from == this.to | ||
return this.head == this.anchor | ||
} | ||
// eq:: (other: Selection) → bool | ||
// Test whether the selection is the same as another selection. | ||
// eq:: (Selection) → bool | ||
// Test whether the selection is the same as another selection. The | ||
// default implementation tests whether they have the same class, | ||
// head, and anchor. | ||
eq(other) { | ||
return other instanceof this.constructor && other.anchor == this.anchor && other.head == this.head | ||
} | ||
@@ -36,3 +68,10 @@ // map:: (doc: Node, mapping: Mappable) → Selection | ||
// toJSON:: () → Object | ||
// Convert the selection to a JSON representation. | ||
// Convert the selection to a JSON representation. When implementing | ||
// this for a custom selection class, make sure to give the object a | ||
// `type` property whose value matches the ID under which you | ||
// [registered](#state.Selection^jsonID) your class. The default | ||
// implementation adds `type`, `head`, and `anchor` properties. | ||
toJSON() { | ||
return {type: this.jsonID, anchor: this.anchor, head: this.head} | ||
} | ||
@@ -45,3 +84,3 @@ // :: (ResolvedPos, number, ?bool) → ?Selection | ||
static findFrom($pos, dir, textOnly) { | ||
let inner = $pos.parent.isTextblock ? new TextSelection($pos) | ||
let inner = $pos.parent.inlineContent ? new TextSelection($pos) | ||
: findSelectionIn($pos.node(0), $pos.parent, $pos.pos, $pos.index(), dir, textOnly) | ||
@@ -84,51 +123,58 @@ if (inner) return inner | ||
// :: (ResolvedPos, ResolvedPos, ?number) → Selection | ||
// Find a selection that spans the given positions, if both are text | ||
// positions. If not, return some other selection nearby, where | ||
// `bias` determines whether the method searches forward (default) | ||
// or backwards (negative number) first. | ||
static between($anchor, $head, bias) { | ||
let found = Selection.near($head, bias) | ||
if (found instanceof TextSelection) { | ||
let nearAnchor = Selection.findFrom($anchor, $anchor.pos > found.to ? -1 : 1, true) | ||
found = new TextSelection(nearAnchor.$anchor, found.$head) | ||
} else if ($anchor.pos < found.from || $anchor.pos > found.to) { | ||
// If head falls on a node, but anchor falls outside of it, create | ||
// a text selection between them | ||
let inv = $anchor.pos > found.to | ||
let foundAnchor = Selection.findFrom($anchor, inv ? -1 : 1, true) | ||
let foundHead = Selection.findFrom(inv ? found.$from : found.$to, inv ? 1 : -1, true) | ||
if (foundAnchor && foundHead) | ||
found = new TextSelection(foundAnchor.$anchor, foundHead.$head) | ||
if (!warnedAboutBetween && typeof console != "undefined" && console.warn) { | ||
warnedAboutBetween = true | ||
console.warn("Selection.between is now called TextSelection.between") | ||
} | ||
return found | ||
return TextSelection.between($anchor, $head, bias) | ||
} | ||
// : (Object, Mapping) → Object | ||
// Map a JSON object representing this selection through a mapping. | ||
static mapJSON(json, mapping) { | ||
if (json.anchor != null) | ||
return {head: mapping.map(json.head), anchor: mapping.map(json.anchor)} | ||
else | ||
return {node: mapping.map(json.node), after: mapping.map(json.after, -1)} | ||
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 | ||
// Deserialize a JSON representation of a selection. | ||
// Deserialize a JSON representation of a selection. Must be | ||
// implemented for custom classes (as a static class method). | ||
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). | ||
if (json.head != null) { | ||
let $anchor = doc.resolve(json.anchor), $head = doc.resolve(json.head) | ||
if ($anchor.parent.isTextblock && $head.parent.isTextblock) return new TextSelection($anchor, $head) | ||
else return Selection.between($anchor, $head) | ||
} else { | ||
let $pos = doc.resolve(json.node), after = $pos.nodeAfter | ||
if (after && json.after == json.pos + after.nodeSize && NodeSelection.isSelectable(after)) return new NodeSelection($pos) | ||
else return Selection.near($pos) | ||
} | ||
let cls = classesById[json.type] | ||
if (!cls) return this.backwardsCompatFromJSON(doc, json) | ||
return cls.fromJSON(doc, json) | ||
} | ||
static backwardsCompatFromJSON(doc, json) { | ||
if (json.anchor != null) return TextSelection.fromJSON(doc, json) | ||
if (json.node != null) return NodeSelection.fromJSON(doc, {anchor: json.node, head: json.after}) | ||
throw new RangeError("Unrecognized JSON data " + JSON.stringify(json)) | ||
} | ||
// :: (string, constructor<Selection>) | ||
// To be able to deserialize selections from JSON, custom selection | ||
// classes must register themselves with an ID string, so that they | ||
// can be disambiguated. Try to pick something that's unlikely to | ||
// clash with classes from other modules. | ||
static jsonID(id, selectionClass) { | ||
if (id in classesById) throw new RangeError("Duplicate use of selection JSON ID " + id) | ||
classesById[id] = selectionClass | ||
selectionClass.prototype.jsonID = id | ||
return selectionClass | ||
} | ||
} | ||
exports.Selection = Selection | ||
// :: bool | ||
// Controls whether, when a selection of this type is active in the | ||
// browser, the selected range should be visible to the user. Defaults | ||
// to `true`. | ||
Selection.prototype.visible = true | ||
// ::- A text selection represents a classical editor | ||
@@ -139,39 +185,14 @@ // selection, with a head (the moving side) and anchor (immobile | ||
class TextSelection extends Selection { | ||
// :: number | ||
// The selection's immobile side (does not move when pressing | ||
// shift-arrow). | ||
get anchor() { return this.$anchor.pos } | ||
// :: number | ||
// The selection's mobile side (the side that moves when pressing | ||
// shift-arrow). | ||
get head() { return this.$head.pos } | ||
// :: ?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 } | ||
// :: (ResolvedPos, ?ResolvedPos) | ||
// Construct a text selection. | ||
constructor($anchor, $head = $anchor) { | ||
let inv = $anchor.pos > $head.pos | ||
super(inv ? $head : $anchor, inv ? $anchor : $head) | ||
// :: ResolvedPos The resolved anchor of the selection. | ||
this.$anchor = $anchor | ||
// :: ResolvedPos The resolved head of the selection. | ||
this.$head = $head | ||
} | ||
get inverted() { return this.anchor > this.head } | ||
eq(other) { | ||
return other instanceof TextSelection && other.head == this.head && other.anchor == this.anchor | ||
} | ||
map(doc, mapping) { | ||
let $head = doc.resolve(mapping.map(this.head)) | ||
if (!$head.parent.isTextblock) return Selection.near($head) | ||
if (!$head.parent.inlineContent) return Selection.near($head) | ||
let $anchor = doc.resolve(mapping.map(this.anchor)) | ||
return new TextSelection($anchor.parent.isTextblock ? $anchor : $head, $head) | ||
return new TextSelection($anchor.parent.inlineContent ? $anchor : $head, $head) | ||
} | ||
toJSON() { | ||
return {head: this.head, anchor: this.anchor} | ||
} | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -183,5 +204,31 @@ // Create a text selection from non-resolved positions. | ||
} | ||
// :: (ResolvedPos, ResolvedPos, ?number) → TextSelection | ||
// 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. | ||
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 | ||
if (!$anchor.parent.inlineContent) { | ||
$anchor = Selection.near($anchor, dir, true).$anchor | ||
if (($anchor.pos > $head.pos) != (dir < 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)) | ||
} | ||
} | ||
exports.TextSelection = TextSelection | ||
Selection.jsonID("text", TextSelection) | ||
// ::- A node selection is a selection that points at a | ||
@@ -202,8 +249,4 @@ // single node. All nodes marked [selectable](#model.NodeSpec.selectable) | ||
eq(other) { | ||
return other instanceof NodeSelection && this.from == other.from | ||
} | ||
map(doc, mapping) { | ||
let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) | ||
let from = mapping.mapResult(this.anchor, 1), to = mapping.mapResult(this.head, -1) | ||
let $from = doc.resolve(from.pos), node = $from.nodeAfter | ||
@@ -215,6 +258,2 @@ if (!from.deleted && !to.deleted && node && to.pos == from.pos + node.nodeSize && NodeSelection.isSelectable(node)) | ||
toJSON() { | ||
return {node: this.from, after: this.to} | ||
} | ||
// :: (Node, number, ?number) → TextSelection | ||
@@ -232,5 +271,15 @@ // 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) | ||
} | ||
} | ||
exports.NodeSelection = NodeSelection | ||
NodeSelection.prototype.visible = false | ||
Selection.jsonID("node", NodeSelection) | ||
// FIXME we'll need some awareness of text direction when scanning for selections | ||
@@ -242,3 +291,3 @@ | ||
function findSelectionIn(doc, node, pos, index, dir, text) { | ||
if (node.isTextblock) return TextSelection.create(doc, pos) | ||
if (node.inlineContent) return TextSelection.create(doc, pos) | ||
for (let i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) { | ||
@@ -245,0 +294,0 @@ let child = node.child(i) |
@@ -16,3 +16,3 @@ const {Transform} = require("prosemirror-transform") | ||
// metadata properties in a transaction, which are extra pieces of | ||
// informations that client code or plugins can use to describe what a | ||
// information that client code or plugins can use to describe what a | ||
// transacion represents, so that they can update their [own | ||
@@ -76,3 +76,3 @@ // state](##state.StateField) accordingly. | ||
// :: (?[Mark]) → Transaction | ||
// Replace the set of stored marks. | ||
// Set the current stored marks. | ||
setStoredMarks(marks) { | ||
@@ -84,2 +84,12 @@ this.storedMarks = marks | ||
// :: ([Mark]) → Transaction | ||
// Make sure the current stored marks or, if that is null, the marks | ||
// at the selection, match the given set of marks. Does nothing if | ||
// this is already the case. | ||
ensureMarks(marks) { | ||
if (!Mark.sameSet(this.storedMarks || this.selection.$from.marks(), marks)) | ||
this.setStoredMarks(marks.length || this.storedMarks ? marks : null) | ||
return this | ||
} | ||
// :: bool | ||
@@ -137,4 +147,6 @@ // Whether the stored marks were explicitly set for this transaction. | ||
deleteSelection() { | ||
let {from, to} = this.selection | ||
return this.deleteRange(from, to) | ||
let {from, to, $from} = this.selection | ||
this.deleteRange(from, to) | ||
if ($from.parentOffset < $from.parent.content.size) this.ensureMarks($from.marks(true)) | ||
return this | ||
} | ||
@@ -194,4 +206,3 @@ | ||
addStoredMark(mark) { | ||
this.storedMarks = mark.addToSet(this.storedMarks || currentMarks(this.selection)) | ||
return this | ||
return this.ensureMarks(mark.addToSet(this.storedMarks || this.selection.$head.marks())) | ||
} | ||
@@ -202,4 +213,3 @@ | ||
removeStoredMark(mark) { | ||
this.storedMarks = mark.removeFromSet(this.storedMarks || currentMarks(this.selection)) | ||
return this | ||
return this.ensureMarks(mark.removeFromSet(this.storedMarks || this.selection.$head.marks())) | ||
} | ||
@@ -215,5 +225,1 @@ } | ||
} | ||
function currentMarks(selection) { | ||
return selection.head == null ? Mark.none : selection.$head.marks() | ||
} |
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
79244
1647
+ Addedprosemirror-model@0.19.0(transitive)
+ Addedprosemirror-transform@0.19.0(transitive)
- Removedprosemirror-model@0.18.0(transitive)
- Removedprosemirror-transform@0.18.0(transitive)
Updatedprosemirror-model@^0.19.0