substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).
311 lines (296 loc) • 9.96 kB
JavaScript
import { _isDefined, isNil, last } from '../util'
import Selection from './Selection'
import PropertySelection from './PropertySelection'
import ContainerSelection from './ContainerSelection'
import NodeSelection from './NodeSelection'
import CustomSelection from './CustomSelection'
import getContainerRoot from './_getContainerRoot'
import getContainerPosition from './_getContainerPosition'
import compareCoordinates from './_compareCoordinates'
import Coordinate from './Coordinate'
export function fromJSON (json) {
if (!json) return Selection.nullSelection
var type = json.type
switch (type) {
case 'property':
return PropertySelection.fromJSON(json)
case 'container':
return ContainerSelection.fromJSON(json)
case 'node':
return NodeSelection.fromJSON(json)
case 'custom':
return CustomSelection.fromJSON(json)
default:
// console.error('Selection.fromJSON(): unsupported selection data', json)
return Selection.nullSelection
}
}
/**
* Helper to check if a coordinate is the first position of a node.
* Attention: this works only for Text and List nodes
*/
export function isFirst (doc, containerPath, coor) {
if (coor.isNodeCoordinate()) {
return coor.offset === 0
}
const node = getContainerRoot(doc, containerPath, coor.path[0])
if (node.isText()) {
return coor.offset === 0
}
if (node.isList()) {
const itemId = coor.path[0]
return (node.items[0] === itemId && coor.offset === 0)
}
return false
}
/**
* Helper to check if a coordinate is the last position of a node.
* Attention: this works only for Text and List nodes
*/
export function isLast (doc, containerPath, coor) {
if (coor.isNodeCoordinate()) {
return coor.offset > 0
}
const node = getContainerRoot(doc, containerPath, coor.path[0])
if (node.isText()) {
return coor.offset >= node.getLength()
}
if (node.isList()) {
const itemId = coor.path[0]
const item = doc.get(itemId)
return (last(node.items) === itemId && coor.offset === item.getLength())
}
return false
}
export function isEntirelySelected (doc, node, start, end) {
const { isEntirelySelected } = _getRangeInfo(doc, node, start, end)
return isEntirelySelected
}
function _getRangeInfo (doc, node, start, end) {
let isFirst = true
let isLast = true
if (node.isText() || node.isListItem()) {
if (start && start.offset !== 0) isFirst = false
if (end && end.offset < node.getLength()) isLast = false
}
const isEntirelySelected = isFirst && isLast
return { isFirst, isLast, isEntirelySelected }
}
export function setCursor (tx, node, containerPath, mode) {
if (node.isText()) {
let offset = 0
if (mode === 'after') {
const text = node.getText()
offset = text.length
}
tx.setSelection({
type: 'property',
path: node.getPath(),
startOffset: offset,
containerPath
})
} else if (node.isList()) {
let item, offset
if (mode === 'after') {
item = node.getLastItem()
offset = item.getLength()
} else {
item = node.getFirstItem()
offset = 0
}
tx.setSelection({
type: 'property',
path: item.getPath(),
startOffset: offset,
containerPath
})
} else {
tx.setSelection({
type: 'node',
containerPath,
nodeId: node.id
// NOTE: ATM we mostly use 'full' NodeSelections
// Still, they are supported internally
// mode: mode
})
}
}
export function selectNode (tx, nodeId, containerPath) {
tx.setSelection(createNodeSelection({ doc: tx, nodeId, containerPath }))
}
export function createSelection (doc, data) {
let sel
if (isNil(data)) return Selection.nullSelection
switch (data.type) {
case 'property': {
if (isNil(data.endOffset)) {
data.endOffset = data.startOffset
}
if (!_isDefined(data.reverse)) {
if (data.startOffset > data.endOffset) {
[data.startOffset, data.endOffset] = [data.endOffset, data.startOffset]
data.reverse = !data.reverse
}
}
// integrity checks:
const text = doc.get(data.path, 'strict')
if (data.startOffset < 0 || data.startOffset > text.length) {
throw new Error('Invalid startOffset: target property has length ' + text.length + ', given startOffset is ' + data.startOffset)
}
if (data.endOffset < 0 || data.endOffset > text.length) {
throw new Error('Invalid startOffset: target property has length ' + text.length + ', given endOffset is ' + data.endOffset)
}
sel = new PropertySelection(data)
break
}
case 'container': {
const containerPath = data.containerPath
const ids = doc.get(containerPath)
if (!ids) throw new Error('Can not create ContainerSelection: container "' + containerPath + '" does not exist.')
let start = _normalizeCoor(doc, { path: data.startPath, offset: data.startOffset, containerPath })
let end = _normalizeCoor(doc, { path: data.endPath, offset: data.endOffset, containerPath })
if (!_isDefined(data.reverse)) {
if (compareCoordinates(doc, containerPath, start, end) > 0) {
[start, end] = [end, start]
data.reverse = true
}
}
sel = new ContainerSelection(containerPath, start.path, start.offset, end.path, end.offset, data.reverse, data.surfaceId)
break
}
case 'node': {
sel = createNodeSelection({
doc,
nodeId: data.nodeId,
mode: data.mode,
containerPath: data.containerPath,
reverse: data.reverse,
surfaceId: data.surfaceId
})
break
}
case 'custom': {
sel = CustomSelection.fromJSON(data)
break
}
default:
throw new Error('Illegal selection type', data)
}
if (!sel.isNull()) {
sel.attach(doc)
}
return sel
}
function _normalizeCoor (doc, { path, offset, containerPath }) {
// NOTE: normalizing so that a node coordinate is used only for 'isolated nodes'
if (path.length === 1) {
// FIXME: originally getContainerRoot was called here
// however in this case
const node = getContainerRoot(doc, containerPath, path[0])
if (node.isText()) {
// console.warn("DEPRECATED: don't use node coordinates for TextNodes. Use selectionHelpers instead to set cursor at first or last position conveniently.")
return new Coordinate(node.getPath(), offset === 0 ? 0 : node.getLength())
} else if (node.isList()) {
// console.warn("DEPRECATED: don't use node coordinates for ListNodes. Use selectionHelpers instead to set cursor at first or last position conveniently.")
if (offset === 0) {
const item = node.getItemAt(0)
return new Coordinate(item.getPath(), 0)
} else {
const item = doc.get(last(node.items))
return new Coordinate(item.getPath(), item.getLength())
}
}
}
return new Coordinate(path, offset)
}
export function createNodeSelection ({ doc, nodeId, containerPath, mode, reverse, surfaceId }) {
let node = doc.get(nodeId)
if (!node) return Selection.nullSelection
node = getContainerRoot(doc, containerPath, nodeId)
if (node.isText()) {
return new PropertySelection({
path: node.getPath(),
startOffset: mode === 'after' ? node.getLength() : 0,
endOffset: mode === 'before' ? 0 : node.getLength(),
reverse,
containerPath,
surfaceId
})
} else if (node.isList() && node.getLength() > 0) {
const first = node.getFirstItem()
const last = node.getLastItem()
let start = {
path: first.getPath(),
offset: 0
}
let end = {
path: last.getPath(),
offset: last.getLength()
}
if (mode === 'after') start = end
else if (mode === 'before') end = start
return new ContainerSelection({
startPath: start.path,
startOffset: start.offset,
endPath: end.path,
endOffset: end.offset,
reverse,
containerPath,
surfaceId
})
} else {
return new NodeSelection({ nodeId, mode, reverse, containerPath, surfaceId })
}
}
export function stepIntoIsolatedNode (editorSession, comp) {
// this succeeds if the content component provides
// a grabFocus() implementation
if (comp.grabFocus()) return true
// otherwise we try to find the first surface
const surface = comp.find('.sc-surface')
if (surface) {
// TODO: what about CustomSurfaces?
if (surface._isTextPropertyEditor) {
const doc = editorSession.getDocument()
const path = surface.getPath()
const text = doc.get(path, 'strict')
editorSession.setSelection({
type: 'property',
path: path,
startOffset: text.length,
surfaceId: surface.id
})
return true
} else if (surface._isContainerEditor) {
const doc = editorSession.getDocument()
const containerPath = surface.getContainerPath()
const nodeIds = doc.get()
if (nodeIds.length > 0) {
const first = doc.get(nodeIds[0])
setCursor(editorSession, first, containerPath, 'after')
}
return true
}
}
return false
}
export function augmentSelection (selData, oldSel) {
// don't do magically if a surfaceId is present
if (selData && oldSel && !selData.surfaceId && !oldSel.isNull()) {
selData.containerPath = selData.containerPath || oldSel.containerPath
selData.surfaceId = selData.surfaceId || oldSel.surfaceId
}
return selData
}
/**
* Get the node ids covered by this selection.
*
* @returns {String[]} an getNodeIds of ids
*/
export function getNodeIdsCoveredByContainerSelection (doc, sel) {
const containerPath = sel.containerPath
const startPos = getContainerPosition(doc, containerPath, sel.start.path[0])
const endPos = getContainerPosition(doc, containerPath, sel.end.path[0])
const nodeIds = doc.get(containerPath)
return nodeIds.slice(startPos, endPos + 1)
}