UNPKG

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
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>'