refui
Version:
The JavaScript framework that refuels your UI projects, across web, native, and embedded
280 lines (255 loc) • 7.05 kB
JavaScript
import { isSignal, nextTick, peek, bind } from '../signal.js'
import { createRenderer } from '../renderer.js'
import { nop, cachedStrKeyNoFalsy, splitFirst } from '../utils.js'
/*
const NODE_TYPES = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
PROCESSING_INSTRUCTION_NODE: 7,
COMMENT_NODE: 8,
DOCUMENT_NODE: 9,
DOCUMENT_FRAGMENT_NODE: 11
}
*/
/*
Apply order:
1. Get namespace
2. Get alias
3. Create with namespace
*/
const defaultRendererID = 'DOM'
function createDOMRenderer({
rendererID = defaultRendererID,
doc = document,
namespaces = {},
tagNamespaceMap = {},
tagAliases = {},
propAliases = {},
onDirective
} = {}) {
let eventPassiveSupported = false
let eventOnceSupported = false
try {
const options = {
passive: {
get() {
eventPassiveSupported = true
return eventPassiveSupported
}
},
once: {
get() {
eventOnceSupported = true
return eventOnceSupported
}
}
}
const testEvent = '__refui_event_option_test__'
doc.addEventListener(testEvent, nop, options)
doc.removeEventListener(testEvent, nop, options)
} catch (e) {
// do nothing
}
// eslint-disable-next-line max-params
function eventCallbackFallback(node, event, handler, options) {
if (options.once && !eventOnceSupported) {
const _handler = handler
handler = function(...args) {
_handler(...args)
node.removeEventListener(event, handler, options)
}
}
if (options.passive && !eventPassiveSupported) {
const _handler = handler
handler = function(...args) {
nextTick(_handler.bind(null, ...args))
}
}
return handler
}
function isNode(node) {
return !!(node && node.cloneNode)
}
const getNodeCreator = cachedStrKeyNoFalsy(function(tagNameRaw) {
let [nsuri, tagName] = tagNameRaw.split(':')
if (!tagName) {
tagName = nsuri
nsuri = tagNamespaceMap[tagName]
}
tagName = tagAliases[tagName] || tagName
if (nsuri) {
nsuri = namespaces[nsuri] || nsuri
return function() {
return doc.createElementNS(nsuri, tagName)
}
}
return function() {
return doc.createElement(tagName)
}
})
function createNode(tagName) {
return getNodeCreator(tagName)()
}
function createAnchor(anchorName) {
if (process.env.NODE_ENV !== 'production' && anchorName) {
return doc.createComment(anchorName)
}
return doc.createTextNode('')
}
function createTextNode (text) {
if (isSignal(text)) {
const node = doc.createTextNode('')
text.connect(function() {
const newData = peek(text)
if (newData === undefined) node.data = ''
else node.data = newData
})
return node
}
return doc.createTextNode(text)
}
function createFragment() {
return doc.createDocumentFragment()
}
function removeNode(node) {
if (!node.parentNode) return
node.parentNode.removeChild(node)
}
function appendNode(parent, ...nodes) {
for (let node of nodes) {
parent.insertBefore(node, null)
}
}
function insertBefore(node, ref) {
ref.parentNode.insertBefore(node, ref)
}
const getListenerAdder = cachedStrKeyNoFalsy(function(event) {
const [prefix, eventName] = event.split(':')
if (prefix === 'on') {
return function(node, cb) {
if (!cb) return
if (isSignal(cb)) {
let currentHandler = null
cb.connect(function() {
const newHandler = peek(cb)
if (currentHandler) node.removeEventListener(eventName, currentHandler)
if (newHandler) node.addEventListener(eventName, newHandler)
currentHandler = newHandler
})
} else node.addEventListener(eventName, cb)
}
} else {
const optionsArr = prefix.split('-')
optionsArr.shift()
const options = {}
for (let option of optionsArr) if (option) options[option] = true
return function(node, cb) {
if (!cb) return
if (isSignal(cb)) {
let currentHandler = null
cb.connect(function() {
let newHandler = peek(cb)
if (currentHandler) node.removeEventListener(eventName, currentHandler, options)
if (newHandler) {
newHandler = eventCallbackFallback(node, eventName, newHandler, options)
node.addEventListener(eventName, newHandler, options)
}
currentHandler = newHandler
})
} else node.addEventListener(eventName, eventCallbackFallback(node, eventName, cb, options), options)
}
}
})
function addListener(node, event, cb) {
getListenerAdder(event)(node, cb)
}
function setAttr(node, attr, val) {
if (val === undefined || val === null || val === false) return
function handler(newVal) {
if (newVal === undefined || newVal === null || newVal === false) node.removeAttribute(attr)
else if (newVal === true) node.setAttribute(attr, '')
else node.setAttribute(attr, newVal)
}
bind(handler, val)
}
// eslint-disable-next-line max-params
function setAttrNS(node, attr, val, ns) {
if (val === undefined || val === null || val === false) return
function handler(newVal) {
if (newVal === undefined || newVal === null || newVal === false) node.removeAttributeNS(ns, attr)
else if (newVal === true) node.setAttributeNS(ns, attr, '')
else node.setAttributeNS(ns, attr, newVal)
}
bind(handler, val)
}
const getPropSetter = cachedStrKeyNoFalsy(function(prop) {
prop = propAliases[prop] || prop
const [prefix, key] = splitFirst(prop, ':')
if (key) {
switch (prefix) {
default: {
if (prefix === 'on' || prefix.startsWith('on-')) {
return function(node, val) {
return addListener(node, prop, val)
}
}
if (onDirective) {
const setter = onDirective(prefix, key, prop)
if (setter) {
return setter
}
}
const nsuri = namespaces[prefix] || prefix
return function(node, val) {
return setAttrNS(node, key, val, nsuri)
}
}
case 'attr': {
return function(node, val) {
return setAttr(node, key, val)
}
}
case 'prop': {
prop = key
}
}
} else if (prop.indexOf('-') > -1) {
return function(node, val) {
return setAttr(node, prop, val)
}
}
return function(node, val) {
if (val === undefined || val === null) return
if (isSignal(val)) {
val.connect(function() {
node[prop] = peek(val)
})
} else {
node[prop] = val
}
}
})
function setProps(node, props) {
if (Object.hasOwn(props, 'children')) {
props = Object.assign({}, props)
delete props.children
}
for (let prop in props) getPropSetter(prop)(node, props[prop])
}
const nodeOps = {
isNode,
createNode,
createAnchor,
createTextNode,
createFragment,
setProps,
insertBefore,
appendNode,
removeNode
}
return createRenderer(nodeOps, rendererID)
}
export { createDOMRenderer, defaultRendererID }