mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-11 17:53:59 +02:00
Introduces state.activeMarks and has add/remove/toggleMark use it instead of state.marks (#990)
* WIP getting raw marks arrays from the current range * Always handle marksRaw as Array, fixes tests/lint * Clean up * Fixes collapsed selection raw marks, simpler _every_ condition, harmonize toolbar buttons in rich-text example * raw -> perCharacter * Add tests for toggleMark collapsed selection * Add .DS_Store to .gitignore * Added test for toggleMark add in partially marked selection, with and without other marks * Added docs for state.marksPerCharacter * replace marksPerCharacter with activeMarks * Update the other examples * Clarify getActiveMarksAtRange * AddMark/RemoveMark to use getActiveMarksByRange * User activeMarks for toggle§MarkAtRange transform
This commit is contained in:
committed by
Ian Storm Taylor
parent
d21728ed86
commit
786050f732
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,3 +14,6 @@ _book
|
|||||||
# NPM files.
|
# NPM files.
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
|
# Mac stuff.
|
||||||
|
.DS_Store
|
||||||
|
@@ -18,6 +18,7 @@ For convenience, in addition to transforms, many of the [`Selection`](./selectio
|
|||||||
- [`{edge}Text`](#edgetext)
|
- [`{edge}Text`](#edgetext)
|
||||||
- [`{edge}Block`](#edgeblock)
|
- [`{edge}Block`](#edgeblock)
|
||||||
- [`marks`](#marks)
|
- [`marks`](#marks)
|
||||||
|
- [`activeMarks`](#activeMarks)
|
||||||
- [`blocks`](#blocks)
|
- [`blocks`](#blocks)
|
||||||
- [`fragment`](#fragment)
|
- [`fragment`](#fragment)
|
||||||
- [`inlines`](#inlines)
|
- [`inlines`](#inlines)
|
||||||
@@ -79,7 +80,12 @@ Get the leaf [`Block`](./block.md) node at `{edge}`. Where `{edge}` is one of: `
|
|||||||
### `marks`
|
### `marks`
|
||||||
`Set`
|
`Set`
|
||||||
|
|
||||||
Get a set of the [`Marks`](./mark.md) in the current selection.
|
Get a set of the [`Marks`](./mark.md) in the current selection.
|
||||||
|
|
||||||
|
### `activeMarks`
|
||||||
|
`Set`
|
||||||
|
|
||||||
|
Get a subset of the [`Marks`](./mark.md) that are present in _all_ the characters in the current selection. It can be used to determine the active/inactive state of toolbar buttons corresponding to marks, based on the usual rich text editing conventions.
|
||||||
|
|
||||||
### `blocks`
|
### `blocks`
|
||||||
`List`
|
`List`
|
||||||
|
@@ -70,7 +70,7 @@ class RichText extends React.Component {
|
|||||||
|
|
||||||
hasMark = (type) => {
|
hasMark = (type) => {
|
||||||
const { state } = this.state
|
const { state } = this.state
|
||||||
return state.marks.some(mark => mark.type == type)
|
return state.activeMarks.some(mark => mark.type == type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -58,7 +58,7 @@ class HoveringMenu extends React.Component {
|
|||||||
|
|
||||||
hasMark = (type) => {
|
hasMark = (type) => {
|
||||||
const { state } = this.state
|
const { state } = this.state
|
||||||
return state.marks.some(mark => mark.type == type)
|
return state.activeMarks.some(mark => mark.type == type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -65,7 +65,7 @@ class Iframes extends React.Component {
|
|||||||
|
|
||||||
hasMark = (type) => {
|
hasMark = (type) => {
|
||||||
const { state } = this.state
|
const { state } = this.state
|
||||||
return state.marks.some(mark => mark.type == type)
|
return state.activeMarks.some(mark => mark.type == type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -70,7 +70,7 @@ class RichText extends React.Component {
|
|||||||
|
|
||||||
hasMark = (type) => {
|
hasMark = (type) => {
|
||||||
const { state } = this.state
|
const { state } = this.state
|
||||||
return state.marks.some(mark => mark.type == type)
|
return state.activeMarks.some(mark => mark.type == type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -885,6 +885,18 @@ const Node = {
|
|||||||
return new OrderedSet(array)
|
return new OrderedSet(array)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a set of the active marks in a `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {Set<Mark>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getActiveMarksAtRange(range) {
|
||||||
|
const array = this.getActiveMarksAtRangeAsArray(range)
|
||||||
|
return new Set(array)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a set of the marks in a `range`.
|
* Get a set of the marks in a `range`.
|
||||||
*
|
*
|
||||||
@@ -920,6 +932,36 @@ const Node = {
|
|||||||
}, [])
|
}, [])
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getActiveMarksAtRangeAsArray(range) {
|
||||||
|
range = range.normalize(this)
|
||||||
|
const { startKey, startOffset } = range
|
||||||
|
|
||||||
|
// If the range is collapsed at the start of the node, check the previous.
|
||||||
|
if (range.isCollapsed && startOffset == 0) {
|
||||||
|
const previous = this.getPreviousText(startKey)
|
||||||
|
if (!previous || !previous.length) return []
|
||||||
|
const char = previous.characters.get(previous.length - 1)
|
||||||
|
return char.marks.toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the range is collapsed, check the character before the start.
|
||||||
|
if (range.isCollapsed) {
|
||||||
|
const text = this.getDescendant(startKey)
|
||||||
|
const char = text.characters.get(range.startOffset - 1)
|
||||||
|
return char.marks.toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, get a set of the marks for each character in the range.
|
||||||
|
const chars = this.getCharactersAtRange(range)
|
||||||
|
const first = chars.first()
|
||||||
|
let memo = first.marks
|
||||||
|
chars.slice(1).forEach((char) => {
|
||||||
|
memo = memo.intersect(char.marks)
|
||||||
|
return memo.size != 0
|
||||||
|
})
|
||||||
|
return memo.toArray()
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all of the marks that match a `type`.
|
* Get all of the marks that match a `type`.
|
||||||
*
|
*
|
||||||
@@ -1852,6 +1894,8 @@ memoize(Node, [
|
|||||||
|
|
||||||
memoize(Node, [
|
memoize(Node, [
|
||||||
'areDescendantsSorted',
|
'areDescendantsSorted',
|
||||||
|
'getActiveMarksAtRange',
|
||||||
|
'getActiveMarksAtRangeAsArray',
|
||||||
'getAncestors',
|
'getAncestors',
|
||||||
'getBlocksAtRange',
|
'getBlocksAtRange',
|
||||||
'getBlocksAtRangeAsArray',
|
'getBlocksAtRangeAsArray',
|
||||||
|
@@ -401,6 +401,18 @@ class State extends new Record(DEFAULTS) {
|
|||||||
: this.selection.marks || this.document.getMarksAtRange(this.selection)
|
: this.selection.marks || this.document.getMarksAtRange(this.selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active marks of the current selection.
|
||||||
|
*
|
||||||
|
* @return {Set<Mark>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
get activeMarks() {
|
||||||
|
return this.selection.isUnset
|
||||||
|
? new Set()
|
||||||
|
: this.selection.marks || this.document.getActiveMarksAtRange(this.selection)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the block nodes in the current selection.
|
* Get the block nodes in the current selection.
|
||||||
*
|
*
|
||||||
|
@@ -33,7 +33,7 @@ Transforms.addMark = (transform, mark) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const marks = document.getMarksAtRange(selection).add(mark)
|
const marks = document.getActiveMarksAtRange(selection).add(mark)
|
||||||
const sel = selection.set('marks', marks)
|
const sel = selection.set('marks', marks)
|
||||||
transform.select(sel)
|
transform.select(sel)
|
||||||
}
|
}
|
||||||
@@ -346,7 +346,7 @@ Transforms.removeMark = (transform, mark) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const marks = document.getMarksAtRange(selection).remove(mark)
|
const marks = document.getActiveMarksAtRange(selection).remove(mark)
|
||||||
const sel = selection.set('marks', marks)
|
const sel = selection.set('marks', marks)
|
||||||
transform.select(sel)
|
transform.select(sel)
|
||||||
}
|
}
|
||||||
@@ -362,7 +362,7 @@ Transforms.removeMark = (transform, mark) => {
|
|||||||
Transforms.toggleMark = (transform, mark) => {
|
Transforms.toggleMark = (transform, mark) => {
|
||||||
mark = Normalize.mark(mark)
|
mark = Normalize.mark(mark)
|
||||||
const { state } = transform
|
const { state } = transform
|
||||||
const exists = state.marks.some(m => m.equals(mark))
|
const exists = state.activeMarks.some(m => m.equals(mark))
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
transform.removeMark(mark)
|
transform.removeMark(mark)
|
||||||
|
@@ -979,7 +979,7 @@ Transforms.toggleMarkAtRange = (transform, range, mark, options = {}) => {
|
|||||||
const { normalize = true } = options
|
const { normalize = true } = options
|
||||||
const { state } = transform
|
const { state } = transform
|
||||||
const { document } = state
|
const { document } = state
|
||||||
const marks = document.getMarksAtRange(range)
|
const marks = document.getActiveMarksAtRange(range)
|
||||||
const exists = marks.some(m => m.equals(mark))
|
const exists = marks.some(m => m.equals(mark))
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
|
export default function (state) {
|
||||||
|
const { document, selection } = state
|
||||||
|
const texts = document.getTexts()
|
||||||
|
const first = texts.first()
|
||||||
|
const range = selection.merge({
|
||||||
|
anchorKey: first.key,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusKey: first.key,
|
||||||
|
focusOffset: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const next = state
|
||||||
|
.transform()
|
||||||
|
.select(range)
|
||||||
|
.toggleMark('bold')
|
||||||
|
.insertText('a')
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
assert.deepEqual(next.selection.toJS(), range.move(1).toJS())
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: word
|
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
ranges:
|
||||||
|
- text: a
|
||||||
|
marks:
|
||||||
|
- type: bold
|
||||||
|
- text: word
|
@@ -7,16 +7,16 @@ export default function (state) {
|
|||||||
const first = texts.first()
|
const first = texts.first()
|
||||||
const range = selection.merge({
|
const range = selection.merge({
|
||||||
anchorKey: first.key,
|
anchorKey: first.key,
|
||||||
anchorOffset: 0,
|
anchorOffset: 4,
|
||||||
focusKey: first.key,
|
focusKey: first.key,
|
||||||
focusOffset: 0
|
focusOffset: 4
|
||||||
})
|
})
|
||||||
|
|
||||||
const next = state
|
const next = state
|
||||||
.transform()
|
.transform()
|
||||||
.select(range)
|
.select(range)
|
||||||
.toggleMark('bold')
|
.toggleMark('bold')
|
||||||
.insertText('a')
|
.insertText('s')
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
assert.deepEqual(next.selection.toJS(), range.move(1).toJS())
|
assert.deepEqual(next.selection.toJS(), range.move(1).toJS())
|
||||||
|
@@ -5,7 +5,7 @@ nodes:
|
|||||||
nodes:
|
nodes:
|
||||||
- kind: text
|
- kind: text
|
||||||
ranges:
|
ranges:
|
||||||
- text: a
|
- text: word
|
||||||
|
- text: s
|
||||||
marks:
|
marks:
|
||||||
- type: bold
|
- type: bold
|
||||||
- text: word
|
|
||||||
|
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
|
export default function (state) {
|
||||||
|
const { document, selection } = state
|
||||||
|
const texts = document.getTexts()
|
||||||
|
const first = texts.first()
|
||||||
|
const range = selection.merge({
|
||||||
|
anchorKey: first.key,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusKey: first.key,
|
||||||
|
focusOffset: 3
|
||||||
|
})
|
||||||
|
|
||||||
|
const next = state
|
||||||
|
.transform()
|
||||||
|
.select(range)
|
||||||
|
.toggleMark('bold')
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
assert.deepEqual(next.selection.toJS(), range.toJS())
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
ranges:
|
||||||
|
- text: a
|
||||||
|
marks:
|
||||||
|
- type: bold
|
||||||
|
- text: word
|
||||||
|
marks:
|
||||||
|
- type: italic
|
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
ranges:
|
||||||
|
- text: a
|
||||||
|
marks:
|
||||||
|
- type: bold
|
||||||
|
- text: wo
|
||||||
|
marks:
|
||||||
|
- type: italic
|
||||||
|
- type: bold
|
||||||
|
- text: rd
|
||||||
|
marks:
|
||||||
|
- type: italic
|
@@ -0,0 +1,23 @@
|
|||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
|
export default function (state) {
|
||||||
|
const { document, selection } = state
|
||||||
|
const texts = document.getTexts()
|
||||||
|
const first = texts.first()
|
||||||
|
const range = selection.merge({
|
||||||
|
anchorKey: first.key,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusKey: first.key,
|
||||||
|
focusOffset: 5
|
||||||
|
})
|
||||||
|
|
||||||
|
const next = state
|
||||||
|
.transform()
|
||||||
|
.select(range)
|
||||||
|
.toggleMark('bold')
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
assert.deepEqual(next.selection.toJS(), range.toJS())
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
ranges:
|
||||||
|
- text: a
|
||||||
|
- text: word
|
||||||
|
marks:
|
||||||
|
- type: bold
|
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
ranges:
|
||||||
|
- text: aword
|
||||||
|
marks:
|
||||||
|
- type: bold
|
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
|
export default function (state) {
|
||||||
|
const { document, selection } = state
|
||||||
|
const texts = document.getTexts()
|
||||||
|
const first = texts.first()
|
||||||
|
const range = selection.merge({
|
||||||
|
anchorKey: first.key,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusKey: first.key,
|
||||||
|
focusOffset: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const next = state
|
||||||
|
.transform()
|
||||||
|
.select(range)
|
||||||
|
.toggleMark('bold')
|
||||||
|
.toggleMark('bold')
|
||||||
|
.insertText('a')
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
assert.deepEqual(next.selection.toJS(), range.move(1).toJS())
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: word
|
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: aword
|
@@ -7,9 +7,9 @@ export default function (state) {
|
|||||||
const first = texts.first()
|
const first = texts.first()
|
||||||
const range = selection.merge({
|
const range = selection.merge({
|
||||||
anchorKey: first.key,
|
anchorKey: first.key,
|
||||||
anchorOffset: 0,
|
anchorOffset: 4,
|
||||||
focusKey: first.key,
|
focusKey: first.key,
|
||||||
focusOffset: 0
|
focusOffset: 4
|
||||||
})
|
})
|
||||||
|
|
||||||
const next = state
|
const next = state
|
||||||
@@ -17,7 +17,7 @@ export default function (state) {
|
|||||||
.select(range)
|
.select(range)
|
||||||
.toggleMark('bold')
|
.toggleMark('bold')
|
||||||
.toggleMark('bold')
|
.toggleMark('bold')
|
||||||
.insertText('a')
|
.insertText('s')
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
assert.deepEqual(next.selection.toJS(), range.move(1).toJS())
|
assert.deepEqual(next.selection.toJS(), range.move(1).toJS())
|
||||||
|
@@ -4,4 +4,4 @@ nodes:
|
|||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
- kind: text
|
- kind: text
|
||||||
text: aword
|
text: words
|
||||||
|
Reference in New Issue
Block a user