stimulus_reflex
Version:
Build reactive applications with the Rails tooling you already know and love.
197 lines (174 loc) • 5.6 kB
JavaScript
import Debug from './debug'
import Deprecate from './deprecate'
import Schema from './schema'
import { Utils } from 'cable_ready'
const { debounce, dispatch, xpathToElement, xpathToElementArray } = Utils
// uuid4 function taken from stackoverflow
// https://stackoverflow.com/a/2117523/554903
const uuidv4 = () => {
const crypto = window.crypto || window.msCrypto
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
)
}
const serializeForm = (form, options = {}) => {
if (!form) return ''
const w = options.w || window
const { element } = options
const formData = new w.FormData(form)
const data = Array.from(formData, e => e.map(encodeURIComponent).join('='))
const submitButton = form.querySelector('input[type=submit]')
if (
element &&
element.name &&
element.nodeName === 'INPUT' &&
element.type === 'submit'
) {
data.push(
`${encodeURIComponent(element.name)}=${encodeURIComponent(element.value)}`
)
} else if (submitButton && submitButton.name) {
data.push(
`${encodeURIComponent(submitButton.name)}=${encodeURIComponent(
submitButton.value
)}`
)
}
return Array.from(data).join('&')
}
const camelize = (value, uppercaseFirstLetter = true) => {
if (typeof value !== 'string') return ''
value = value
.replace(/[\s_](.)/g, $1 => $1.toUpperCase())
.replace(/[\s_]/g, '')
.replace(/^(.)/, $1 => $1.toLowerCase())
if (uppercaseFirstLetter)
value = value.substr(0, 1).toUpperCase() + value.substr(1)
return value
}
// TODO: remove this in v4 (potentially!)
const XPathToElement = xpathToElement
const XPathToArray = xpathToElementArray
const emitEvent = (name, detail = {}) => dispatch(document, name, detail)
const extractReflexName = reflexString => {
const match = reflexString.match(/(?:.*->)?(.*?)(?:Reflex)?#/)
return match ? match[1] : ''
}
// construct a valid xPath for an element in the DOM
const elementToXPath = element => {
if (element.id !== '') return "//*[@id='" + element.id + "']"
if (element === document.body) return '/html/body'
if (element.nodeName === 'HTML') return '/html'
let ix = 0
const siblings =
element && element.parentNode ? element.parentNode.childNodes : []
for (var i = 0; i < siblings.length; i++) {
const sibling = siblings[i]
if (sibling === element) {
const computedPath = elementToXPath(element.parentNode)
const tagName = element.tagName.toLowerCase()
const ixInc = ix + 1
return `${computedPath}/${tagName}[${ixInc}]`
}
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
ix++
}
}
}
const elementInvalid = element => {
return (
element.type === 'number' && element.validity && element.validity.badInput
)
}
const getReflexElement = (args, element) => {
return args[0] && args[0].nodeType === Node.ELEMENT_NODE
? args.shift()
: element
}
const getReflexOptions = args => {
const options = {}
// TODO: remove reflexId in v4
if (
args[0] &&
typeof args[0] === 'object' &&
Object.keys(args[0]).filter(key =>
[
'id',
'attrs',
'selectors',
'reflexId',
'resolveLate',
'serializeForm',
'suppressLogging',
'includeInnerHTML',
'includeTextContent'
].includes(key)
).length
) {
const opts = args.shift()
// TODO: in v4, all promises resolve during finalize stage
// if they specify resolveLate, console.warn to say that the option will be ignored
// deprecation warning in 3.5 is not required as it's still required until v4
Object.keys(opts).forEach(o => {
// TODO: no need to check for reflexId in v4
if (o === 'reflexId') {
if (Deprecate.enabled)
console.warn('reflexId option will be removed in v4. Use id instead.')
options['id'] = opts['reflexId']
} else options[o] = opts[o]
})
}
return options
}
// compute the DOM element(s) which will be the morph root
// use the data-reflex-root attribute on the reflex or the controller
// optional value is a CSS selector(s); comma-separated list
// order of preference is data-reflex, data-controller, document body (default)
const getReflexRoots = element => {
let list = []
while (list.length === 0 && element) {
let reflexRoot = element.getAttribute(Schema.reflexRoot)
if (reflexRoot) {
if (reflexRoot.length === 0 && element.id) reflexRoot = `#${element.id}`
const selectors = reflexRoot.split(',').filter(s => s.trim().length)
if (Debug.enabled && selectors.length === 0) {
console.error(
`No value found for ${Schema.reflexRoot}. Add an #id to the element or provide a value for ${Schema.reflexRoot}.`,
element
)
}
list = list.concat(selectors.filter(s => document.querySelector(s)))
}
element = element.parentElement
? element.parentElement.closest(`[${Schema.reflexRoot}]`)
: null
}
return list
}
const reflexNameToControllerIdentifier = (reflexName) => {
return reflexName
.replace(/([a-z0–9])([A-Z])/g, '$1-$2')
.replace(/(::)/g, '--')
.replace(/-reflex$/gi, '')
.toLowerCase()
}
export {
camelize,
debounce,
dispatch,
elementInvalid,
elementToXPath,
emitEvent,
extractReflexName,
getReflexElement,
getReflexOptions,
getReflexRoots,
reflexNameToControllerIdentifier,
serializeForm,
uuidv4,
XPathToArray,
XPathToElement,
}