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).
1,091 lines (898 loc) • 25.1 kB
JavaScript
import isObject from '../util/isObject'
import isFunction from '../util/isFunction'
import isString from '../util/isString'
import isArray from '../util/isArray'
import forEach from '../util/forEach'
import ArrayIterator from '../util/ArrayIterator'
import DOMEventListener from './DOMEventListener'
const NOT_IMPLEMENTED = 'This method is not implemented.'
/**
A unified interface for DOM elements used by Substance.
@abstract
*/
export default class DOMElement {
/*
The element's id.
@property {String} DOMElement#id
*/
/*
The element's tag name in lower case.
@property {String} DOMElement#tagName
*/
/*
@property {String} DOMElement#textContent
*/
/*
The inner HTML string.
@property {String} DOMElement#innerHTML
*/
/*
The outer HTML string.
@property {String} DOMElement#outerHTML
*/
/*
An array of child nodes, including nodes such as TextNodes.
@property {Array<DOMElement>} DOMElement#childNodes
*/
/*
An array of child elements.
@property {Array<DOMElement>} DOMElement#children children
*/
/*
The computed height.
@property {Array<DOMElement>} DOMElement#height
*/
/*
The computed width.
@property {Array<DOMElement>} DOMElement#width
*/
/**
@returns the native element
*/
getNativeElement () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Checks if the element is a TextNode.
@returns {Boolean} true if the element is of type `Node.TEXT_NODE`
*/
isTextNode () {
/* istanbul ignore next */
return false
}
/**
Checks if the element is actually an element as opposed to a node.
@returns {Boolean} true if the element is of type `Node.ELEMENT_NODE`
*/
isElementNode () {
/* istanbul ignore next */
return false
}
/**
Checks if the element is a CommentNode.
@returns {Boolean} true if the element is of type `Node.COMMENT_NODE`
*/
isCommentNode () {
/* istanbul ignore next */
return false
}
/**
Checks if the element is a DocumentNode.
@returns {Boolean} true if the element is of type `Node.DOCUMENT_NODE`
*/
isDocumentNode () {
/* istanbul ignore next */
return false
}
/**
Get the tagName of this element.
@private
@note Considered as private API, in favor of the property {DOMElement.prototype.tagName}
@returns {String} the tag name in lower-case.
*/
getTagName () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Set the tagName of this element.
@private
@note Considered as private API, in favor of the property {DOMElement.prototype.tagName}
@param {String} tagName the new tag name
@returns {this}
*/
setTagName (tagName) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Get the id of this element.
@private
@note Considered as private API, in favor of the property {DOMElement.prototype.id}
@returns {String} the id.
*/
getId () {
return this.getAttribute('id')
}
/**
Set the id of this element.
@private
@note Considered as private API, in favor of the property {DOMElement.prototype.id}
@param {String} id the new id
@returns {this}
*/
setId (id) {
this.setAttribute('id', id)
return this
}
/**
Checks if a CSS class is set.
@param {String} className
@returns {Boolean} true if the CSS class is set
*/
hasClass (className) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Adds a CSS class.
@param {String} classString A space-separated string with CSS classes
@returns {this}
*/
addClass (classString) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Removes a CSS class.
@param {String} classString A space-separated string with CSS classes
@returns {this}
*/
removeClass (classString) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
hasAttribute (name) {
return Boolean(this.getAttribute(name))
}
/**
jQuery style getter and setter for attributes.
@param {String} name
@param {String} [value] if present the attribute will be set
@returns {String|this} if used as getter the attribute value, otherwise this element for chaining
*/
attr () {
if (arguments.length === 1) {
if (isString(arguments[0])) {
return this.getAttribute(arguments[0])
} else if (isObject(arguments[0])) {
forEach(arguments[0], function (value, name) {
this.setAttribute(name, value)
}.bind(this))
}
} else if (arguments.length === 2) {
this.setAttribute(arguments[0], arguments[1])
}
return this
}
/**
Removes an attribute.
@param {String} name
@returns {this}
*/
removeAttr (name) {
var names = name.split(/\s+/)
if (names.length === 1) {
this.removeAttribute(name)
} else {
names.forEach(function (name) {
this.removeAttribute(name)
}.bind(this))
}
return this
}
/**
Get the attribute with a given name.
@returns {String} the attribute's value.
*/
getAttribute (name) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Set the attribute with a given name.
@param {String} the attribute's value.
@returns {this}
*/
setAttribute (name, value) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
removeAttribute (name) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getAttributes () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
jQuery style getter and setter for HTML element properties.
@param {String} name
@param {String} [value] if present the property will be set
@returns {String|this} if used as getter the property value, otherwise this element for chaining
*/
htmlProp () {
if (arguments.length === 1) {
if (isString(arguments[0])) {
return this.getProperty(arguments[0])
} else if (isObject(arguments[0])) {
forEach(arguments[0], function (value, name) {
this.setProperty(name, value)
}.bind(this))
}
} else if (arguments.length === 2) {
this.setProperty(arguments[0], arguments[1])
}
return this
}
getProperty (name) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
setProperty (name, value) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
jQuery style getter and setter for the *value* of an element.
@param {String} [value] The value to set.
@returns {String|this} the value if used as a getter, `this` otherwise
*/
val (value) {
if (arguments.length === 0) {
return this.getValue()
} else {
this.setValue(value)
return this
}
}
getValue () {
return this.getProperty('value')
}
setValue (value) {
this.setProperty('value', value)
return this
}
/**
jQuery style method to set or get inline CSS styles.
@param {String} name the style name
@param {String} [value] the style value
@returns {String|this} the style value or this if used as a setter
*/
css () {
/* istanbul ignore else */
if (arguments.length === 1) {
/* istanbul ignore else */
if (isString(arguments[0])) {
return this.getStyle(arguments[0])
} else if (isObject(arguments[0])) {
forEach(arguments[0], function (value, name) {
this.setStyle(name, value)
}.bind(this))
} else {
throw new Error('Illegal arguments.')
}
} else if (arguments.length === 2) {
this.setStyle(arguments[0], arguments[1])
} else {
throw new Error('Illegal arguments.')
}
return this
}
getStyle (name) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
setStyle (name, value) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Gets or sets the text content of an element.
@param {String} [text] The text content to set.
@returns {String|this} The text content if used as a getter, `this` otherwise
*/
text (text) {
if (arguments.length === 0) {
return this.getTextContent()
} else {
this.setTextContent(text)
}
return this
}
/**
Get the textContent of this element.
@private
@note Considered as private API, in favor of the property {DOMElement.prototype.innerHTML}
@returns {String}
*/
getTextContent () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Set the textContent of this element.
@private
@note Considered as private API, in favor of the property {DOMElement.prototype.innerHTML}
@param {String} text the new text content
@returns {this}
*/
setTextContent (text) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
jQuery style getter and setter for the innerHTML of an element.
@param {String} [html] The html to set.
@returns {String|this} the inner html if used as a getter, `this` otherwise
*/
html (html) {
if (arguments.length === 0) {
return this.getInnerHTML()
} else {
this.setInnerHTML(html)
}
return this
}
/**
Get the innerHTML of this element.
@private
@note Considered as private API, in favor of the property {@link DOMElement.prototype.innerHTML}
@returns {String}
*/
getInnerHTML () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Just an alias for getInnerHTML() which feels
better when working with XML DOMs
*/
getInnerXML () {
return this.getInnerHTML()
}
/**
Set the innerHTML of this element.
@private
@note Considered as private API, in favor of the property {@link DOMElement.prototype.innerHTML}
@param {String} text the new text content
@returns {this}
*/
setInnerHTML (html) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
setInnerXML (xml) {
return this.setInnerHTML(xml)
}
/**
Get the outerHTML of this element.
@private
@note Considered as private API, in favor of the property {@link DOMElement.prototype.outerHTML}
@returns {String}
*/
getOuterHTML () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getOuterXML () {
return this.getOuterHTML()
}
/**
Registers an Element event handler.
@param {String} event The event name.
@param {Function} handler The handler function.
@param {Object} [context] context where the function should be bound to
@param {Object} [options]
@param {Object} [options.capture] to register the event in the event's capture phase (bubbling top-down)
@returns {this}
*/
on (eventName, handler, context, options) {
/* istanbul ignore next */
if (!isString(eventName)) {
throw new Error('Illegal argument: "event" must be a String.')
}
options = options || {}
if (context) {
options.context = context
}
/* istanbul ignore next */
if (!handler || !isFunction(handler)) {
throw new Error('Illegal argument: invalid handler function for event ' + eventName)
}
this.addEventListener(eventName, handler, options)
return this
}
/**
Unregisters the handler of a given event.
@param {String} event The event name.
@returns {this}
*/
off (eventName, handler) {
// el.off(this): disconnect all listeners bound to the given context
if (arguments.length === 1 && !isString(eventName)) {
const context = arguments[0]
this.getEventListeners().filter(function (l) {
return l.context === context
}).forEach(function (l) {
this.removeEventListener(l)
}.bind(this))
} else {
this.removeEventListener(eventName, handler)
}
return this
}
addEventListener (eventName, handler, options = {}) {
let listener
if (arguments.length === 1 && arguments[0]) {
listener = arguments[0]
} else {
listener = this._createEventListener(eventName, handler, options)
}
if (!this.eventListeners) {
this.eventListeners = []
}
listener._el = this
this.eventListeners.push(listener)
this._addEventListenerNative(listener)
return this
}
_createEventListener (eventName, handler, options) {
return new DOMEventListener(eventName, handler, options)
}
_addEventListenerNative(listener) {} // eslint-disable-line
removeEventListener (eventName, handler) {
if (!this.eventListeners) return
// console.log('removing event listener', eventName, handler);
let listener = null
const idx = DOMEventListener.findIndex(this.eventListeners, eventName, handler)
listener = this.eventListeners[idx]
if (idx > -1) {
this.eventListeners.splice(idx, 1)
// console.log('BrowserDOMElement.removeEventListener:', eventName, this.eventListeners.length);
listener._el = null
this._removeEventListenerNative(listener)
}
return this
}
_removeEventListenerNative(listener) {} // eslint-disable-line
removeAllEventListeners () {
if (!this.eventListeners) return
for (let i = 0; i < this.eventListeners.length; i++) {
const listener = this.eventListeners[i]
// console.log('BrowserDOMElement.removeEventListener:', eventName, this.eventListeners.length);
listener._el = null
this._removeEventListenerNative(listener)
}
delete this.eventListeners
}
getEventListeners () {
return this.eventListeners || []
}
/**
Gets the type of this element in lower-case.
@private
@note Considered as private API, in favor of the property {@link DOMElement.prototype.nodeType}
@returns {String}
*/
getNodeType () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getContentType () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getChildCount () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Get child nodes of this element.
This method provides a new array with wrapped native elements.
Better use getChildAt().
@private Considered as private API, in favor of the property {DOMElement.prototype.childNodes}
@returns {Array<DOMElement>}
*/
getChildNodes () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Get child elements of this element.
This method provides a new array with wrapped native elements.
Better use getChildAt().
@private Considered as private API, in favor of the property {DOMElement.prototype.children}
@returns {Array<DOMElement>}
*/
getChildren () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getChildAt (pos) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getChildIndex (child) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getChildNodeIterator () {
return new ArrayIterator(this.getChildNodes())
}
getLastChild () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getFirstChild () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getNextSibling () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
getPreviousSibling () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Creates a clone of the current element.
@returns {DOMElement} A clone of this element.
*/
clone(deep) { // eslint-disable-line
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Creates a DOMElement.
@param {String} str a tag name or an HTML element as string.
@returns {DOMElement}
*/
createElement (str) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
createTextNode (text) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
createComment (data) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
createProcessingInstruction (name, data) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
createCDATASection (data) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Checks if a given CSS selector matches for this element.
**Attention**
This method is currently not implemented for {VirtualElement}.
This means you should use it only in importer implementations.
@param {String} cssSelector
@returns {Boolean}
*/
is (cssSelector) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Get the parent element of this element.
@returns {DOMElement} the parent element
*/
getParent () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Get the ownerDocument of this element.
@returns {DOMElement} the document element
*/
getOwnerDocument () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/*
@returns {DOMElement} the doctype element `<!DOCTYPE <name> PUBLIC "<publicId>" "<systemId>" >`; null if not present
*/
getDoctype () {
/* istanbul ignore next */
throw new Error('NOT_IMPLEMENTED')
}
/**
* @param {*} qualifiedNameStr the name of the root element
* @param {*} publicId the id of the schema
* @param {*} systemId typically a DTD URI
*/
setDoctype(qualifiedNameStr, publicId, systemId) { // eslint-disable-line
throw new Error('NOT_IMPLEMENTED')
}
getElementById (id) {
throw new Error('NOT_IMPLEMENTED')
}
/**
Find the first descendant element matching the given CSS selector.
Note this differs from jQuery.find() that it returns only one element.
**Attention**
This method is currently not implemented for {VirtualElement}.
This means you can use it only in importer implementations, but not in render or exporter implementations.
@param {String} cssSelector
@returns {DOMElement} found element
*/
find (cssSelector) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Find all descendant elements matching the given CSS selector.
**Attention**
This method is currently not implemented for {VirtualElement}.
This means you can use it only in importer implementations, but not in render or exporter implementations.
@param {String} cssSelector
@returns {Array<DOMElement>} found elements
*/
findAll (cssSelector) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Append a child element.
@param {DOMElement|String} child An element or text to append
@returns {this}
*/
append (children) {
if (arguments.length === 1) {
if (isArray(children)) {
children = children.slice()
} else {
const child = children
return this.appendChild(child)
}
} else {
children = arguments
}
if (children) {
Array.prototype.forEach.call(children, this.appendChild.bind(this))
}
return this
}
appendChild (child) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Insert a child element at a given position.
@param {Number} pos insert position
@param {DOMElement|String} child The child element or text to insert.
@returns {this}
*/
insertAt (pos, child) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
insertBefore (newChild, before) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Remove the child at a given position.
@param {Number} pos
@returns {this}
*/
removeAt (pos) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
removeChild (child) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
replaceChild (oldChild, newChild) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
/**
Removes this element from its parent.
@returns {this}
*/
remove () {
var parent = this.getParent()
if (parent) {
parent.removeChild(this)
}
}
/**
Removes all child nodes from this element.
@returns {this}
*/
empty () {
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
serialize () {
return this.getOuterHTML()
}
isInDocument () {
let el = this
while (el) {
if (el.isDocumentNode()) {
return true
}
el = el.getParent()
}
}
/**
Focusses this element.
**Attention: this makes only sense for elements which are rendered in the browser**
*/
focus () {
/* istanbul ignore next */
return this
}
/**
Selects this element.
*/
select () {
/* istanbul ignore next */
return this
}
/**
Blur this element.
*/
blur () {
/* istanbul ignore next */
return this
}
/**
Trigger a click event on this element.
*/
click () {
/* istanbul ignore next */
return this
}
/* API to retrieve layout information */
getWidth () {
/* istanbul ignore next */
return 0
}
getHeight () {
/* istanbul ignore next */
return 0
}
/**
Outer height as provided by $.outerHeight(withMargin)
*/
getOuterHeight (withMargin) { // eslint-disable-line no-unused-vars
/* istanbul ignore next */
return 0
}
/**
Offset values as provided by $.offset()
*/
getOffset () {
/* istanbul ignore next */
return { top: 0, left: 0 }
}
/**
Position values as provided by $.position()
*/
getPosition () {
/* istanbul ignore next */
return { top: 0, left: 0 }
}
/**
Get element factory conveniently
@example
var $$ = el.getElementFactory()
$$('div').append('bla')
*/
getElementFactory () {
return this.createElement.bind(this)
}
/**
Triggers a custom event.
@param {String} name
@param {Object} data
*/
emit(name, data) { // eslint-disable-line
/* istanbul ignore next */
throw new Error(NOT_IMPLEMENTED)
}
// properties
get id () {
return this.getId()
}
set id (id) {
this.setId(id)
}
get tagName () {
return this.getTagName()
}
set tagName (tagName) {
this.setTagName(tagName)
}
get nodeName () {
return this.getTagName()
}
get nodeType () {
return this.getNodeType()
}
get className () {
return this.getAttribute('class')
}
set className (className) {
this.setAttribute('class', className)
}
get textContent () {
return this.getTextContent()
}
set textContent (text) {
this.setTextContent(text)
}
get innerHTML () {
return this.getInnerHTML()
}
set innerHTML (html) {
this.setInnerHTML(html)
}
get outerHTML () {
return this.getOuterHTML()
}
get firstChild () {
return this.getFirstChild()
}
get lastChild () {
return this.getLastChild()
}
get nextSibling () {
return this.getNextSibling()
}
get previousSibling () {
return this.getPreviousSibling()
}
get parentNode () {
return this.getParent()
}
get height () {
return this.getHeight()
}
get width () {
return this.getWidth()
}
get value () {
return this.getValue()
}
set value (value) {
return this.setValue(value)
}
get _isDOMElement () { return true }
// TODO: probably we should just export these symbols
static get pxStyles () { return PX_STYLES }
static get EMPTY_HTML () { return EMPTY_HTML }
}
const PX_STYLES = {
top: true,
bottom: true,
left: true,
right: true,
height: true,
width: true
}
const EMPTY_HTML = '<html><head></head><body></body></html>'