bootstrap-table
Version:
An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
599 lines (515 loc) • 18.7 kB
JavaScript
/**
* Bootstrap Table DOM Manipulation Utility Library
* Provides jQuery-style DOM manipulation APIs using native JavaScript
*
* Security Notice:
* - The `create()` method uses innerHTML to parse HTML strings. Always sanitize user input
* before passing it to create() to prevent XSS attacks.
* - The `html()` method sets innerHTML directly. Use the `text()` method for user-provided content.
* - The `attr()` method allows setting arbitrary attributes including event handlers.
* Avoid setting event handler attributes (onclick, onerror, etc.) with user-controlled data.
*/
class DOMHelper {
/**
* Element selector
* @param {string|Element} selector - CSS selector or DOM element
* @param {Element} context - Search context, defaults to document
* @returns {Element|null} First matched element
*/
static $ (selector, context = document) {
if (typeof selector === 'string') {
return context.querySelector(selector)
}
if (selector instanceof Element) {
return selector
}
return null
}
/**
* Element selector (multiple)
* @param {string|Element|NodeList} selector - CSS selector, DOM element, or NodeList
* @param {Element} context - Search context, defaults to document
* @returns {Element[]} Array of all matched elements. Note: if selector is an Element, returns [Element]
*/
static $$ (selector, context = document) {
if (typeof selector === 'string') {
return Array.from(context.querySelectorAll(selector))
}
if (selector instanceof NodeList) {
return Array.from(selector)
}
if (selector instanceof Element) {
return [selector]
}
return []
}
/**
* Create DOM element
* @param {string} html - HTML string. Note: This method uses innerHTML and can execute scripts.
* Always sanitize user input before passing it to this method.
* @returns {Element|null} Created DOM element. Returns null if html is empty, not a string,
* or contains only whitespace.
*/
static create (html) {
if (typeof html !== 'string') return null
const trimmed = html.trim()
if (!trimmed) return null
const template = document.createElement('template')
template.innerHTML = trimmed
return template.content.firstChild
}
/**
* Add CSS class
* @param {Element|string} element - DOM element or selector
* @param {string} className - Class name to add (space-separated for multiple classes)
* @returns {Element|null} The element itself
*/
static addClass (element, className) {
if (typeof element === 'string') element = this.$(element)
if (!element || !element.classList) return element
if (!className) return element
const classes = className.split(' ').filter(c => c)
element.classList.add(...classes)
return element
}
/**
* Remove CSS class
* @param {Element|string} element - DOM element or selector
* @param {string} className - Class name to remove (space-separated for multiple classes)
* @returns {Element|null} The element itself
*/
static removeClass (element, className) {
if (typeof element === 'string') element = this.$(element)
if (!element || !element.classList) return element
if (!className) return element
const classes = className.split(' ').filter(c => c)
element.classList.remove(...classes)
return element
}
/**
* Toggle CSS class
* @param {Element|string} element - DOM element or selector
* @param {string} className - Class name to toggle (space-separated for multiple classes)
* @returns {Element|null} The element itself
*/
static toggleClass (element, className) {
if (typeof element === 'string') element = this.$(element)
if (!element || !element.classList) return element
if (!className) return element
const classes = className.split(' ').filter(c => c)
classes.forEach(cls => element.classList.toggle(cls))
return element
}
/**
* Check if element has CSS class
* @param {Element|string} element - DOM element or selector
* @param {string} className - Class name to check
* @returns {boolean} Whether the class exists
*/
static hasClass (element, className) {
if (typeof element === 'string') element = this.$(element)
if (!element || !element.classList) return false
if (!className) return false
return element.classList.contains(className)
}
/**
* Get or set attribute
* @param {Element|string} element - DOM element or selector
* @param {string} name - Attribute name. Warning: Avoid setting event handler attributes
* (onclick, onerror, etc.) with user-controlled data to prevent XSS.
* @param {string} [value] - Attribute value (omit to get)
* @returns {Element|null} Element when setting, or string|null when getting attribute
*/
static attr (element, name, value) {
if (typeof element === 'string') element = this.$(element)
if (!element) return value === undefined ? null : element
if (value === undefined) {
return element.getAttribute(name)
}
element.setAttribute(name, value)
return element
}
/**
* Remove attribute
* @param {Element|string} element - DOM element or selector
* @param {string} name - Attribute name
* @returns {Element|null} The element itself
*/
static removeAttr (element, name) {
if (typeof element === 'string') element = this.$(element)
if (!element) return element
element.removeAttribute(name)
return element
}
/**
* Get or set data attribute
* @param {Element|string} element - DOM element or selector
* @param {string} key - Data key name
* @param {string} [value] - Data value (omit to get)
* @returns {(string|undefined) when getting (value omitted); (Element|null|undefined) when setting (value provided)}
* Returns the data attribute value (string or undefined) when getting, or the element (or null/undefined if not found) when setting.
*/
static data (element, key, value) {
if (typeof element === 'string') element = this.$(element)
if (!element) return value === undefined ? undefined : element
if (value === undefined) {
return element.dataset[key]
}
element.dataset[key] = value
return element
}
/**
* Append child element
* @param {Element|string} parent - Parent element or selector
* @param {Element|string} child - Child element or HTML string
* @returns {Element|null} Parent element
*/
static append (parent, child) {
if (typeof parent === 'string') parent = this.$(parent)
if (typeof child === 'string') child = this.create(child)
if (parent && child) {
parent.appendChild(child)
}
return parent
}
/**
* Prepend child element
* @param {Element|string} parent - Parent element or selector
* @param {Element|string} child - Child element or HTML string
* @returns {Element|null} Parent element
*/
static prepend (parent, child) {
if (typeof parent === 'string') parent = this.$(parent)
if (typeof child === 'string') child = this.create(child)
if (parent && child) {
parent.insertBefore(child, parent.firstChild)
}
return parent
}
/**
* Insert element after target
* @param {Element|string} newElement - Element to insert
* @param {Element|string} targetElement - Target element
* @returns {Element|null} Inserted element
*/
static insertAfter (newElement, targetElement) {
if (typeof targetElement === 'string') targetElement = this.$(targetElement)
if (typeof newElement === 'string') newElement = this.create(newElement)
if (targetElement && newElement && targetElement.parentNode) {
targetElement.parentNode.insertBefore(newElement, targetElement.nextSibling)
}
return newElement
}
/**
* Insert element before target
* @param {Element|string} newElement - Element to insert
* @param {Element|string} targetElement - Target element
* @returns {Element|null} Inserted element
*/
static insertBefore (newElement, targetElement) {
if (typeof targetElement === 'string') targetElement = this.$(targetElement)
if (typeof newElement === 'string') newElement = this.create(newElement)
if (targetElement && newElement && targetElement.parentNode) {
targetElement.parentNode.insertBefore(newElement, targetElement)
}
return newElement
}
/**
* Find child elements
* @param {Element|string} element - Parent element or selector
* @param {string} selector - CSS selector
* @returns {Element[]} Array of matched child elements
*/
static find (element, selector) {
if (typeof element === 'string') element = this.$(element)
if (!element) return []
return Array.from(element.querySelectorAll(selector))
}
/**
* Find first matching child element
* @param {Element|string} element - Parent element or selector
* @param {string} selector - CSS selector
* @returns {Element|null} First matched child element
*/
static findFirst (element, selector) {
if (typeof element === 'string') element = this.$(element)
if (!element) return null
return element.querySelector(selector)
}
/**
* Get or set style
* @param {Element|string} element - DOM element or selector
* @param {string|Object} property - Property name or property object
* @param {string} [value] - Style value (when property is string)
* @returns {Element|string|null} Element when setting, style value when getting
*/
static css (element, property, value) {
if (typeof element === 'string') element = this.$(element)
if (!element) {
return null
}
if (typeof property === 'object') {
// Batch set styles
Object.assign(element.style, property)
return element
}
if (value === undefined) {
// Get style
return getComputedStyle(element)[property]
}
// Set style
element.style[property] = value
return element
}
/**
* Get element width
* @param {Element|string} element - DOM element or selector
* @returns {number} Element width
*/
static width (element) {
if (typeof element === 'string') element = this.$(element)
if (!element) return 0
return element.offsetWidth
}
/**
* Get element height
* @param {Element|string} element - DOM element or selector
* @returns {number} Element height
*/
static height (element) {
if (typeof element === 'string') element = this.$(element)
if (!element) return 0
return element.offsetHeight
}
/**
* Get element outer width (including border, optionally including margin)
* @param {Element|string} element - DOM element or selector
* @param {boolean} [includeMargin=false] - Whether to include margin
* @returns {number} Element outer width
*/
static outerWidth (element, includeMargin = false) {
if (typeof element === 'string') element = this.$(element)
if (!element) return 0
let width = element.offsetWidth
if (includeMargin) {
const style = getComputedStyle(element)
const marginLeft = parseInt(style.marginLeft, 10) || 0
const marginRight = parseInt(style.marginRight, 10) || 0
width += marginLeft + marginRight
}
return width
}
/**
* Get element outer height (including border, optionally including margin)
* @param {Element|string} element - DOM element or selector
* @param {boolean} [includeMargin=false] - Whether to include margin
* @returns {number} Element outer height
*/
static outerHeight (element, includeMargin = false) {
if (typeof element === 'string') element = this.$(element)
if (!element) return 0
let height = element.offsetHeight
if (includeMargin) {
const style = getComputedStyle(element)
const marginTop = parseInt(style.marginTop, 10) || 0
const marginBottom = parseInt(style.marginBottom, 10) || 0
height += marginTop + marginBottom
}
return height
}
/**
* Get or set element value
* @param {Element|string} element - DOM element or selector
* @param {string} [value] - Value (omit to get)
* @returns {Element|string|null} Element when setting, current value when getting
*/
static val (element, value) {
if (typeof element === 'string') element = this.$(element)
if (!element) return value === undefined ? null : element
if (value === undefined) {
return element.value
}
element.value = value
return element
}
/**
* Get or set HTML content
* @param {Element|string} element - DOM element or selector
* @param {string} [content] - HTML content (omit to get). Warning: This method uses innerHTML
* and can execute scripts. Use text() for user-provided content.
* @returns {Element|string|null} Element when setting, HTML content when getting
*/
static html (element, content) {
if (typeof element === 'string') element = this.$(element)
if (!element) return content === undefined ? null : element
if (content === undefined) {
return element.innerHTML
}
element.innerHTML = content
return element
}
/**
* Get or set text content
* @param {Element|string} element - DOM element or selector
* @param {string} [content] - Text content (omit to get)
* @returns {Element|string|null} Element when setting, text content when getting
*/
static text (element, content) {
if (typeof element === 'string') element = this.$(element)
if (!element) return content === undefined ? null : element
if (content === undefined) {
return element.textContent
}
element.textContent = content
return element
}
/**
* Remove element
* @param {Element|string} element - DOM element or selector
* @returns {Element|null} Removed element
*/
static remove (element) {
if (typeof element === 'string') element = this.$(element)
if (!element || !element.parentNode) return element
element.parentNode.removeChild(element)
return element
}
/**
* Empty element content
* @param {Element|string} element - DOM element or selector
* @returns {Element|null} Emptied element
*/
static empty (element) {
if (typeof element === 'string') element = this.$(element)
if (!element) return element
element.innerHTML = ''
return element
}
/**
* Iterate over element collection
* @param {Element[]|NodeList|string} elements - Element collection or selector
* @param {Function} callback - Callback function with params (index, element)
* @returns {Element[]} Element collection
*/
static each (elements, callback) {
if (typeof elements === 'string') {
elements = this.$$(elements)
} else if (elements instanceof NodeList) {
elements = Array.from(elements)
} else if (!Array.isArray(elements)) {
elements = [elements]
}
elements.forEach((element, index) => {
callback.call(element, index, element)
})
return elements
}
/**
* Get parent element
* @param {Element|string} element - DOM element or selector
* @param {string} [selector] - Parent element selector (optional)
* @returns {Element|null} Parent element
*/
static parent (element, selector) {
if (typeof element === 'string') element = this.$(element)
if (!element) return null
let parent = element.parentElement
if (selector) {
while (parent && !parent.matches(selector)) {
parent = parent.parentElement
}
}
return parent
}
/**
* Get child elements
* @param {Element|string} element - DOM element or selector
* @param {string} [selector] - Child element selector (optional)
* @returns {Element[]} Array of child elements
*/
static children (element, selector) {
if (typeof element === 'string') element = this.$(element)
if (!element) return []
let children = Array.from(element.children)
if (selector) {
children = children.filter(child => child.matches(selector))
}
return children
}
/**
* Get next sibling element
* @param {Element|string} element - DOM element or selector
* @param {string} [selector] - Sibling element selector (optional)
* @returns {Element|null} Next sibling element
*/
static next (element, selector) {
if (typeof element === 'string') element = this.$(element)
if (!element) return null
let next = element.nextElementSibling
if (selector) {
while (next && !next.matches(selector)) {
next = next.nextElementSibling
}
}
return next
}
/**
* Get previous sibling element
* @param {Element|string} element - DOM element or selector
* @param {string} [selector] - Sibling element selector (optional)
* @returns {Element|null} Previous sibling element
*/
static prev (element, selector) {
if (typeof element === 'string') element = this.$(element)
if (!element) return null
let prev = element.previousElementSibling
if (selector) {
while (prev && !prev.matches(selector)) {
prev = prev.previousElementSibling
}
}
return prev
}
/**
* Get element position relative to document
* @param {Element|string} element - DOM element or selector
* @returns {Object} Position info {top, left, width, height}
*/
static offset (element) {
if (typeof element === 'string') element = this.$(element)
if (!element) return { top: 0, left: 0, width: 0, height: 0 }
const rect = element.getBoundingClientRect()
return {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
width: rect.width,
height: rect.height
}
}
/**
* Get element position relative to parent
* @param {Element|string} element - DOM element or selector
* @returns {Object} Position info {top, left}
*/
static position (element) {
if (typeof element === 'string') element = this.$(element)
if (!element) return { top: 0, left: 0 }
return {
top: element.offsetTop,
left: element.offsetLeft
}
}
/**
* Check if element matches selector
* @param {Element|string} element - DOM element or selector
* @param {string} selector - CSS selector
* @returns {boolean} Whether it matches
*/
static is (element, selector) {
if (typeof element === 'string') element = this.$(element)
if (!element) return false
return element.matches(selector)
}
}
// Export DOMHelper class
export default DOMHelper