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 systems.

1,055 lines (868 loc) 24 kB
import { isObject, isFunction, isString, isArray, forEach, ArrayIterator } from '../util' 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) } /** 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) } /** 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) } /** 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) } /** 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)) { let 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, idx = -1 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++) { let 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() { /* 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') } /** 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(child) { var children if (arguments.length === 1) { if (isArray(child)) { children = child } else { this.appendChild(child) return this } } 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) } } DOMElement.prototype._isDOMElement = true DOMElement.pxStyles = { top: true, bottom: true, left: true, right: true, height: true, width: true } DOMElement.EMPTY_HTML = '<html><head></head><body></body></html>'