cable_ready
Version:
CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.
254 lines (220 loc) • 6.54 kB
JavaScript
import { inputTags, textInputTypes } from './enums'
import ActiveElement from './active_element'
// Indicates if the passed element is considered a text input.
//
const isTextInput = element => {
return inputTags[element.tagName] && textInputTypes[element.type]
}
// Assigns focus to the appropriate element... preferring the explicitly passed selector
//
// * selector - a CSS selector for the element that should have focus
//
const assignFocus = selector => {
const element =
selector && selector.nodeType === Node.ELEMENT_NODE
? selector
: document.querySelector(selector)
const focusElement = element || ActiveElement.element
if (focusElement && focusElement.focus) focusElement.focus()
}
// Dispatches an event on the passed element
//
// * element - the element
// * name - the name of the event
// * detail - the event detail
//
const dispatch = (element, name, detail = {}) => {
const init = { bubbles: true, cancelable: true, detail }
const event = new CustomEvent(name, init)
element.dispatchEvent(event)
if (window.jQuery) window.jQuery(element).trigger(name, detail)
}
// Accepts an xPath query and returns the element found at that position in the DOM
//
const xpathToElement = xpath => {
return document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue
}
// Accepts an xPath query and returns all matching elements in the DOM
//
const xpathToElementArray = (xpath, reverse = false) => {
const snapshotList = document.evaluate(
xpath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
)
const snapshots = []
for (let i = 0; i < snapshotList.snapshotLength; i++) {
snapshots.push(snapshotList.snapshotItem(i))
}
return reverse ? snapshots.reverse() : snapshots
}
// Return an array with the class names to be used
//
// * names - could be a string or an array of strings for multiple classes.
//
const getClassNames = names => Array.from(names).flat()
// Perform operation for either the first or all of the elements returned by CSS selector
//
// * operation - the instruction payload from perform
// * callback - the operation function to run for each element
//
const processElements = (operation, callback) => {
Array.from(
operation.selectAll ? operation.element : [operation.element]
).forEach(callback)
}
// convert string to kebab-case
// most other implementations (lodash) are focused on camelCase to kebab-case
// instead, this uses word token boundaries to produce readable URL slugs and keys
// this implementation will not support Emoji or other non-ASCII characters
//
const kebabize = createCompounder(function (result, word, index) {
return result + (index ? '-' : '') + word.toLowerCase()
})
function createCompounder (callback) {
return function (str) {
return words(str).reduce(callback, '')
}
}
const words = str => {
str = str == null ? '' : str
return str.match(/([A-Z]{2,}|[0-9]+|[A-Z]?[a-z]+|[A-Z])/g) || []
}
// Provide a standardized pipeline of checks and modifications to all operations based on provided options
// Currently skips execution if cancelled and implements an optional delay
//
const operate = (operation, callback) => {
if (!operation.cancel) {
operation.delay ? setTimeout(callback, operation.delay) : callback()
return true
}
return false
}
// Dispatch life-cycle events with standardized naming
const before = (target, operation) =>
dispatch(
target,
`cable-ready:before-${kebabize(operation.operation)}`,
operation
)
const after = (target, operation) =>
dispatch(
target,
`cable-ready:after-${kebabize(operation.operation)}`,
operation
)
function debounce (fn, delay = 250) {
let timer
return (...args) => {
const callback = () => fn.apply(this, args)
if (timer) clearTimeout(timer)
timer = setTimeout(callback, delay)
}
}
function handleErrors (response) {
if (!response.ok) throw Error(response.statusText)
return response
}
function safeScalar (val) {
if (
val !== undefined &&
!['string', 'number', 'boolean'].includes(typeof val)
)
console.warn(
`Operation expects a string, number or boolean, but got ${val} (${typeof val})`
)
return val != null ? val : ''
}
function safeString (str) {
if (str !== undefined && typeof str !== 'string')
console.warn(`Operation expects a string, but got ${str} (${typeof str})`)
return str != null ? String(str) : ''
}
function safeArray (arr) {
if (arr !== undefined && !Array.isArray(arr))
console.warn(`Operation expects an array, but got ${arr} (${typeof arr})`)
return arr != null ? Array.from(arr) : []
}
function safeObject (obj) {
if (obj !== undefined && typeof obj !== 'object')
console.warn(`Operation expects an object, but got ${obj} (${typeof obj})`)
return obj != null ? Object(obj) : {}
}
function safeStringOrArray (elem) {
if (elem !== undefined && !Array.isArray(elem) && typeof elem !== 'string')
console.warn(`Operation expects an Array or a String, but got ${elem} (${typeof elem})`)
return elem == null ? '' : Array.isArray(elem) ? Array.from(elem) : String(elem)
}
function fragmentToString (fragment) {
return new XMLSerializer().serializeToString(fragment)
}
// A proxy method to wrap a fetch call in error handling
//
// * url - the URL to fetch
// * additionalHeaders - an object of additional headers passed to fetch
//
async function graciouslyFetch (url, additionalHeaders) {
try {
const response = await fetch(url, {
headers: {
'X-REQUESTED-WITH': 'XmlHttpRequest',
...additionalHeaders
}
})
if (response == undefined) return
handleErrors(response)
return response
} catch (e) {
console.error(`Could not fetch ${url}`)
}
}
class BoundedQueue {
constructor (maxSize) {
this.maxSize = maxSize
this.queue = []
}
push (item) {
if (this.isFull()) {
// Remove the oldest item to make space for the new one
this.shift()
}
this.queue.push(item)
}
shift () {
return this.queue.shift()
}
isFull () {
return this.queue.length === this.maxSize
}
}
export {
isTextInput,
assignFocus,
dispatch,
xpathToElement,
xpathToElementArray,
getClassNames,
processElements,
operate,
before,
after,
debounce,
handleErrors,
graciouslyFetch,
kebabize,
safeScalar,
safeString,
safeArray,
safeObject,
safeStringOrArray,
fragmentToString,
BoundedQueue
}