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).
272 lines (220 loc) • 6.32 kB
JavaScript
import isArray from '../util/isArray'
import isString from '../util/isString'
import cssSelect from '../vendor/css-select'
import DataNode from './Node'
import XPathNode from './XPathNode'
import DocumentNodeSelectAdapter from './_DocumentNodeSelectAdapter'
const cssSelectAdapter = new DocumentNodeSelectAdapter()
/**
Base node type for document nodes.
@example
The following example shows how a new node type is defined.
```js
class Todo extends DocumentNode {
define () {
return {
type: 'todo',
content: 'text',
done: { type: 'bool', default: false }
}
}
}
```
The following data types are supported:
- `string` bare metal string data type
- `text` a string that carries annotations
- `number` numeric values
- `bool` boolean values
- `id` a node id referencing another node in the document
*/
export default class DocumentNode extends DataNode {
_initialize (doc, props) {
this.document = doc
super._initialize(props)
/**
* Experimental:
* Provides an XPathNode that leads back to the root.
* An XPath of a DocumentNode is a sequence of XPathNodes, where the first one contains a node id as entry point
* followed by zero or more nodes with property and position.
* For example, the xpath for the second paragraph in a document's body could look like this [{id: 'article'}, { property: 'body', pos: 2 }]
*/
this._xpath = new XPathNode(this.id, this.type)
}
/**
Get the Document instance.
@returns {Document}
*/
getDocument () {
return this.document
}
resolve (propName) {
const val = this.get(propName)
if (val) {
const doc = this.getDocument()
if (isArray(val)) {
return val.map(id => doc.get(id))
} else {
return doc.get(val)
}
}
}
set (propName, value) {
this.getDocument().set([this.id, propName], value)
}
/**
* Convenience method to assign multiple values.
*
* @param {object} props
*/
assign (props) {
if (!props) return
Object.keys(props).forEach(propName => {
this.set(propName, props[propName])
})
}
/**
Whether this node has a parent.
`parent` is a built-in property for implementing nested nodes.
@returns {Boolean}
*/
hasParent () {
return Boolean(this.parent)
}
/**
@returns {DocumentNode} the parent node
*/
getParent () {
if (isString(this.parent)) return this.document.get(this.parent)
return this.parent
}
setParent (parent) {
if (isString(parent)) parent = this.document.get(parent)
this.parent = parent
}
/**
Get the root node.
The root node is the last ancestor returned
by a sequence of `getParent()` calls.
@returns {DocumentNode}
*/
getRoot () {
let node = this
while (node.parent) {
node = node.parent
}
return node
}
find (cssSelector) {
return cssSelect.selectOne(cssSelector, this, { xmlMode: true, adapter: cssSelectAdapter })
}
findAll (cssSelector) {
return cssSelect.selectAll(cssSelector, this, { xmlMode: true, adapter: cssSelectAdapter })
}
/**
* The xpath of this node.
*/
getXpath () {
return this._xpath
}
/**
* The position in the parent's children property.
*/
getPosition () {
return this._xpath.pos
}
// Node categories
// --------------------
/**
* An anchor is an inline-node with zero-width.
*/
isAnchor () {
return this.constructor.isAnchor()
}
/**
* An annotation has a `start` and an `end` coordinate that is used to anchor it within the document.
*/
isAnnotation () {
return this.constructor.isAnnotation()
}
/**
* A DocumentNode with a sequence of child nodes.
*/
isContainer () {
return this.constructor.isContainer()
}
/**
* A ContainerAnnotation may span over multiple nodes, i.e. `start` and `end` may be located on different text nodes within a Container.
*/
isContainerAnnotation () {
return this.constructor.isContainerAnnotation()
}
/**
* @returns {Boolean} true if node is an inline node (e.g. Inline Formula)
*
* > Attention: InlineNodes are substantially different to Annotations, as they **own** their content.
* In contrast, annotations do not own the content, they are just 'overlays' to text owned by other nodes.
*/
isInlineNode () {
return this.constructor.isInlineNode()
}
/**
* A DocumentNode used for modelling a List, consisting of a list of ListItems and a definition of ordering types.
*/
isList () {
return this.constructor.isList()
}
/**
* A ListItem is may only be a direct child of a ListNode and should be a TextNode.
*/
isListItem () {
return this.constructor.isListItem()
}
/**
* A PropertyAnnotation is an Annotation that is anchored to a single text property.
*/
isPropertyAnnotation () {
return this.constructor.isPropertyAnnotation()
}
/**
@returns {Boolean} true if node is a text node (e.g. Paragraph, Codebock)
*/
isText () {
return this.constructor.isText()
}
// actual implementations are static
static isAnchor () { return false }
static isAnnotation () { return false }
/**
Declares a node to be treated as block-type node.
BlockNodes are considers the direct descendant of `Container` nodes.
@type {Boolean} default: false
*/
static isBlock () { return false }
static isContainer () { return false }
/**
Declares a node to be treated as {@link model/ContainerAnnotation}.
@type {Boolean} default: false
*/
static isContainerAnnotation () { return false }
/**
* Declares a node to be treated as {@link model/InlineNode}.
*
* @type {Boolean} default: false
*/
static isInlineNode () { return false }
static isList () { return false }
static isListItem () { return false }
/**
* Declares a node to be treated as {@link model/PropertyAnnotation}.
*
* @type {Boolean} default: false
*/
static isPropertyAnnotation () { return false }
/**
Declares a node to be treated as text-ish node.
@type {Boolean} default: false
*/
static isText () { return false }
// used for 'instanceof' comparison
get _isDocumentNode () { return true }
}