react-faux-dom
Version:
DOM like data structure to be mutated by D3 et al, then rendered to React elements
490 lines (422 loc) • 12.6 kB
JavaScript
var React = require('react')
var styleAttr = require('style-attr')
var querySelectorAll = require('query-selector')
var camelCase = require('./utils/camelCase')
var isString = require('./utils/isString')
var isUndefined = require('./utils/isUndefined')
var assign = require('./utils/assign')
var mapValues = require('./utils/mapValues')
var styleCamelCase = require('./utils/styleCamelCase')
function element () {
function Element (nodeName, parentNode) {
this.nodeName = nodeName
this.parentNode = parentNode
this.childNodes = []
this.eventListeners = {}
this.text = ''
var self = this
var props = this.props = {
ref: function (component) {
self.component = component
},
style: {
setProperty: function (name, value) {
props.style[styleCamelCase(name)] = value
},
getProperty: function (name) {
return props.style[styleCamelCase(name)] || ''
},
getPropertyValue: function (name) {
return props.style.getProperty(name)
},
removeProperty: function (name) {
delete props.style[styleCamelCase(name)]
}
}
}
this.style = props.style
}
Element.ELEMENT_NODE = 1
Element.DOCUMENT_POSITION_DISCONNECTED = 1
Element.DOCUMENT_POSITION_PRECEDING = 2
Element.DOCUMENT_POSITION_FOLLOWING = 4
Element.DOCUMENT_POSITION_CONTAINS = 8
Element.DOCUMENT_POSITION_CONTAINED_BY = 16
Element.prototype.nodeType = 1
// This was easy to do with Vim.
// Just saying.
Element.prototype.eventNameMappings = {
'blur': 'onBlur',
'change': 'onChange',
'click': 'onClick',
'contextmenu': 'onContextMenu',
'copy': 'onCopy',
'cut': 'onCut',
'doubleclick': 'onDoubleClick',
'drag': 'onDrag',
'dragend': 'onDragEnd',
'dragenter': 'onDragEnter',
'dragexit': 'onDragExit',
'dragleave': 'onDragLeave',
'dragover': 'onDragOver',
'dragstart': 'onDragStart',
'drop': 'onDrop',
'error': 'onError',
'focus': 'onFocus',
'input': 'onInput',
'keydown': 'onKeyDown',
'keypress': 'onKeyPress',
'keyup': 'onKeyUp',
'load': 'onLoad',
'mousedown': 'onMouseDown',
'mouseenter': 'onMouseEnter',
'mouseleave': 'onMouseLeave',
'mousemove': 'onMouseMove',
'mouseout': 'onMouseOut',
'mouseover': 'onMouseOver',
'mouseup': 'onMouseUp',
'paste': 'onPaste',
'scroll': 'onScroll',
'submit': 'onSubmit',
'touchcancel': 'onTouchCancel',
'touchend': 'onTouchEnd',
'touchmove': 'onTouchMove',
'touchstart': 'onTouchStart',
'wheel': 'onWheel'
}
Element.prototype.skipNameTransformationExpressions = [
/^data-/,
/^aria-/
]
Element.prototype.attributeNameMappings = {
'class': 'className'
}
Element.prototype.attributeToPropName = function (name) {
var skipTransformMatches = this.skipNameTransformationExpressions.map(function (expr) {
return expr.test(name)
})
if (skipTransformMatches.some(Boolean)) {
return name
} else {
return this.attributeNameMappings[name] || camelCase(name)
}
}
Element.prototype.setAttribute = function (name, value) {
if (name === 'style' && isString(value)) {
var styles = styleAttr.parse(value)
for (var key in styles) {
this.style.setProperty(key, styles[key])
}
} else {
this.props[this.attributeToPropName(name)] = value
}
}
Element.prototype.getAttribute = function (name) {
return this.props[this.attributeToPropName(name)]
}
Element.prototype.getAttributeNode = function (name) {
var value = this.getAttribute(name)
if (!isUndefined(value)) {
return {
value: value,
specified: true
}
}
}
Element.prototype.removeAttribute = function (name) {
delete this.props[this.attributeToPropName(name)]
}
Element.prototype.eventToPropName = function (name) {
return this.eventNameMappings[name] || name
}
Element.prototype.addEventListener = function (name, fn) {
var prop = this.eventToPropName(name)
this.eventListeners[prop] = this.eventListeners[prop] || []
this.eventListeners[prop].push(fn)
}
Element.prototype.removeEventListener = function (name, fn) {
var listeners = this.eventListeners[this.eventToPropName(name)]
if (listeners) {
var match = listeners.indexOf(fn)
if (match !== -1) {
listeners.splice(match, 1)
}
}
}
Element.prototype.appendChild = function (el) {
if (el instanceof Element) {
el.parentNode = this
}
this.childNodes.push(el)
return el
}
Element.prototype.insertBefore = function (el, before) {
var index = this.childNodes.indexOf(before)
el.parentNode = this
if (index !== -1) {
this.childNodes.splice(index, 0, el)
} else {
this.childNodes.push(el)
}
return el
}
Element.prototype.removeChild = function (child) {
var target = this.childNodes.indexOf(child)
this.childNodes.splice(target, 1)
}
Element.prototype.querySelector = function () {
return this.querySelectorAll.apply(this, arguments)[0] || null
}
Element.prototype.querySelectorAll = function (selector) {
if (!selector) {
throw new Error('Not enough arguments')
}
return querySelectorAll(selector, this)
}
Element.prototype.getElementsByTagName = function (nodeName) {
var children = this.children
if (children.length === 0) {
return []
} else {
var matches
if (nodeName !== '*') {
matches = children.filter(function (el) {
return el.nodeName === nodeName
})
} else {
matches = children
}
var childMatches = children.map(function (el) {
return el.getElementsByTagName(nodeName)
})
return matches.concat.apply(matches, childMatches)
}
}
Element.prototype.getElementById = function (id) {
var children = this.children
if (children.length === 0) {
return null
} else {
var match = children.filter(function (el) {
return el.getAttribute('id') === id
})[0]
if (match) {
return match
} else {
var childMatches = children.map(function (el) {
return el.getElementById(id)
})
return childMatches.filter(function (match) {
return match !== null
})[0] || null
}
}
}
Element.prototype.getBoundingClientRect = function () {
if (!this.component) {
return undefined
}
return this.component.getBoundingClientRect()
}
Element.prototype.cloneNode = function (deep) {
// if deep is not provided, it default to true
if (deep === undefined) {
deep = true
}
var el = new Element(this.nodeName, this.parentNode)
// copy nodeType
if (this.nodeType) {
el.nodeType = this.nodeType
}
var k
// copy the props
for (k in this.props) {
if (this.props.hasOwnProperty(k) && k !== 'ref' && k !== 'style') {
el.props[k] = this.props[k]
}
}
// copy the styles
for (k in this.style) {
if (this.style.hasOwnProperty(k) && [
'setProperty',
'getProperty',
'getPropertyValue',
'removeProperty'
].indexOf(k) === -1) {
el.style[k] = this.style[k]
}
}
if (deep) {
el.childNodes = this.childNodes.map(function (childEl) {
if (!childEl.nodeType) {
// It's a React element, let React clone it
return React.cloneElement(childEl)
}
// either Element or true dom element
childEl = childEl.cloneNode(true)
// if a faux dom element, modify parentNode
if (childEl instanceof Element) {
childEl.parentNode = el
}
return childEl
})
}
return el
}
Element.prototype.toReact = function (index) {
index = index || 0
var props = assign({}, this.props)
props.style = assign({}, props.style)
var originalElement = this
function uniqueKey () {
return 'faux-dom-' + index
}
if (isUndefined(props.key)) {
props.key = uniqueKey()
}
delete props.style.setProperty
delete props.style.getProperty
delete props.style.getPropertyValue
delete props.style.removeProperty
assign(props, mapValues(this.eventListeners, function (listeners) {
return function (syntheticEvent) {
var event
if (syntheticEvent) {
event = syntheticEvent.nativeEvent
event.syntheticEvent = syntheticEvent
}
mapValues(listeners, function (listener) {
listener.call(originalElement, event)
})
}
}))
return React.createElement(this.nodeName, props, this.text || this.children.map(function (el, i) {
if (el instanceof Element) {
return el.toReact(i)
} else {
return el
}
}))
}
Element.prototype.compareDocumentPosition = function (other) {
function getFirstNodeByOrder (nodes, nodeOne, nodeTwo) {
return nodes.reduce(function (result, node) {
if (result !== false) {
return result
} else if (node === nodeOne) {
return nodeOne
} else if (node === nodeTwo) {
return nodeTwo
} else if (node.childNodes) {
return getFirstNodeByOrder(node.childNodes, nodeOne, nodeTwo)
} else {
return false
}
}, false)
}
function isAncestor (source, target) {
while (target.parentNode) {
target = target.parentNode
if (target === source) {
return true
}
}
return false
}
function eitherContains (left, right) {
return isAncestor(left, right)
? Element.DOCUMENT_POSITION_CONTAINED_BY + Element.DOCUMENT_POSITION_FOLLOWING
: isAncestor(right, left)
? Element.DOCUMENT_POSITION_CONTAINS + Element.DOCUMENT_POSITION_PRECEDING
: false
}
function getRootNode (node) {
while (node.parentNode) {
node = node.parentNode
}
return node
}
if (this === other) {
return 0
}
var referenceRoot = getRootNode(this)
var otherRoot = getRootNode(other)
if (referenceRoot !== otherRoot) {
return Element.DOCUMENT_POSITION_DISCONNECTED
}
var result = eitherContains(this, other)
if (result) {
return result
}
var first = getFirstNodeByOrder([referenceRoot], this, other)
return first === this
? Element.DOCUMENT_POSITION_FOLLOWING
: first === other
? Element.DOCUMENT_POSITION_PRECEDING
: Element.DOCUMENT_POSITION_DISCONNECTED
}
Object.defineProperties(Element.prototype, {
nextSibling: {
get: function () {
var siblings = this.parentNode.children
var me = siblings.indexOf(this)
return siblings[me + 1]
}
},
previousSibling: {
get: function () {
var siblings = this.parentNode.children
var me = siblings.indexOf(this)
return siblings[me - 1]
}
},
innerHTML: {
get: function () {
return this.text
},
set: function (text) {
this.text = text
}
},
textContent: {
get: function () {
return this.text
},
set: function (text) {
this.text = text
}
},
children: {
get: function () {
// So far nodes created by this library are all of nodeType 1 (elements),
// but this could change in the future.
return this.childNodes.filter(function (el) {
if (!el.nodeType) {
// It's a React element, we always add it
return true
}
// It's a HTML node. We want to filter to have only nodes with type 1
return el.nodeType === 1
})
}
}
})
// These NS methods are called by things like D3 if it spots a namespace.
// Like xlink:href. I don't care about namespaces, so these functions have NS aliases created.
var namespaceMethods = [
'setAttribute',
'getAttribute',
'getAttributeNode',
'removeAttribute',
'getElementsByTagName',
'getElementById'
]
namespaceMethods.forEach(function (name) {
var fn = Element.prototype[name]
Element.prototype[name + 'NS'] = function () {
return fn.apply(this, Array.prototype.slice.call(arguments, 1))
}
})
return Element
}
module.exports = element