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 systems.
205 lines (193 loc) • 6.11 kB
JavaScript
import { last } from '../util'
import Selection from './Selection'
import PropertySelection from './PropertySelection'
import ContainerSelection from './ContainerSelection'
import NodeSelection from './NodeSelection'
import CustomSelection from './CustomSelection'
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.
*/
export function isFirst(doc, coor) {
if (coor.isNodeCoordinate() && coor.offset === 0) return true
let node = doc.get(coor.path[0]).getContainerRoot()
if (node.isText() && coor.offset === 0) return true
if (node.isList()) {
let itemId = coor.path[0]
if (node.items[0] === itemId && coor.offset === 0) return true
}
}
/*
Helper to check if a coordinate is the last position of a node.
*/
export function isLast(doc, coor) {
if (coor.isNodeCoordinate() && coor.offset > 0) return true
let node = doc.get(coor.path[0]).getContainerRoot()
if (node.isText() && coor.offset >= node.getLength()) return true
if (node.isList()) {
let itemId = coor.path[0]
let item = doc.get(itemId)
if (last(node.items) === itemId && coor.offset === item.getLength()) return true
}
}
export function isEntirelySelected(doc, node, start, end) {
let { isEntirelySelected } = getRangeInfo(doc, node, start, end)
return isEntirelySelected
}
export function getRangeInfo(doc, node, start, end) {
let isFirst = true
let isLast = true
if (node.isText()) {
if (start && start.offset !== 0) isFirst = false
if (end && end.offset < node.getLength()) isLast = false
} else if (node.isList()) {
if (start) {
let itemId = start.path[0]
let itemPos = node.getItemPosition(itemId)
if (itemPos > 0 || start.offset !== 0) isFirst = false
}
if (end) {
let itemId = end.path[0]
let itemPos = node.getItemPosition(itemId)
let item = doc.get(itemId)
if (itemPos < node.items.length-1 || end.offset < item.getLength()) isLast = false
}
}
let isEntirelySelected = isFirst && isLast
return {isFirst, isLast, isEntirelySelected}
}
export function setCursor(tx, node, containerId, mode) {
if (node.isText()) {
let offset = 0
if (mode === 'after') {
let text = node.getText()
offset = text.length
}
tx.setSelection({
type: 'property',
path: node.getTextPath(),
startOffset: offset,
containerId: containerId
})
} 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.getTextPath(),
startOffset: offset,
containerId: containerId
})
} else {
tx.setSelection({
type: 'node',
containerId: containerId,
nodeId: node.id,
// NOTE: ATM we mostly use 'full' NodeSelections
// Still, they are supported internally
// mode: mode
})
}
}
export function selectNode(tx, nodeId, containerId) {
tx.setSelection(createNodeSelection({ doc: tx, nodeId, containerId }))
}
export function createNodeSelection({ doc, nodeId, containerId, mode, reverse, surfaceId}) {
let node = doc.get(nodeId)
if (!node) return Selection.nullSelection
node = node.getContainerRoot()
if (node.isText()) {
return new PropertySelection({
path: node.getTextPath(),
startOffset: mode === 'after' ? node.getLength() : 0,
endOffset: mode === 'before' ? 0 : node.getLength(),
reverse: reverse,
containerId: containerId,
surfaceId: surfaceId
})
} else if (node.isList() && node.getLength()>0) {
let first = node.getFirstItem()
let last = node.getLastItem()
let start = {
path: first.getTextPath(),
offset: 0
}
let end = {
path: last.getTextPath(),
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: reverse,
containerId: containerId,
surfaceId: surfaceId
})
} else {
return new NodeSelection({ nodeId, mode, reverse, containerId, 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
let 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) {
let container = surface.getContainer()
if (container.length > 0) {
let first = container.getChildAt(0)
setCursor(editorSession, first, container.id, '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.containerId = selData.containerId || oldSel.containerId
selData.surfaceId = selData.surfaceId || oldSel.surfaceId
}
return selData
}