1
0
mirror of https://gitlab.com/bramw/baserow.git synced 2024-11-21 23:37:55 +00:00
bramw_baserow/web-frontend/modules/core/utils/dom.js
2024-07-18 12:51:03 +00:00

177 lines
5.8 KiB
JavaScript

/**
* Checks if the target is the same as the provided element of that the element
* contains the target. Returns true is this is the case.
*
* @returns boolean
*/
export const isElement = (element, target) => {
return element !== null && (element === target || element.contains(target))
}
/**
* Checks if the provided object is an html dom element.
*
* @returns boolean
*/
export const isDomElement = (obj) => {
try {
return obj instanceof HTMLElement
} catch (e) {
return (
typeof obj === 'object' &&
obj.nodeType === 1 &&
typeof obj.style === 'object' &&
typeof obj.ownerDocument === 'object'
)
}
}
/**
* This function will focus a contenteditable and place the cursor at the end.
*
* @param element
*/
export const focusEnd = (element) => {
const range = document.createRange()
const selection = window.getSelection()
range.selectNodeContents(element)
range.collapse(false)
selection.removeAllRanges()
selection.addRange(range)
element.focus()
}
/**
* Get the closest parent element that matches a predicate function.
*
* @param {Element} element - The starting element.
* @param {Function} predicate - A function that takes an element and returns a boolean.
* @returns {Element|null} The matching parent element or null if no match is found.
*/
export const getParentMatchingPredicate = (element, predicate) => {
while (element !== null && element.nodeType === Node.ELEMENT_NODE) {
if (predicate(element)) {
return element
}
element = element.parentElement
}
return null
}
/**
* Finds the closest scrollable parent element of the provided element.
*/
export const findScrollableParent = (element) => {
return getParentMatchingPredicate(
element,
(element) => element.scrollHeight > element.clientHeight
)
}
/**
* Detects clicks outside el element and call callback
*
* Returns a callback to unregister click handlers after successful outside click
* @param el
* @param callback
* @returns {(function(): void)|*}
*/
export const onClickOutside = (el, callback) => {
const insideEvent = new Set()
// Firefox and Chrome both can both have a different `target` element on `click`
// when you release the mouse at different coordinates. Therefore we expect this
// variable to be set on mousedown to be consistent.
let downElement = null
// Add the event to the `insideEvent` map. This allow to be sure a click event has
// been triggered from an element inside this context, even if the element has
// been removed after in the meantime.
const clickOutsideClickEvent = (event) => {
insideEvent.add(event)
}
el.addEventListener('click', clickOutsideClickEvent)
const clickOutsideMouseDownEvent = (event) => {
downElement = event.target
}
document.body.addEventListener('mousedown', clickOutsideMouseDownEvent)
const clickOutsideEvent = (event) => {
const target = downElement || event.target
// If the event is from current context or any element inside current context
// the current event should be in the insideEvent map, even if the element
// has been removed from the DOM in the meantime
const insideContext = insideEvent.has(event)
if (insideContext) {
insideEvent.delete(event)
}
// If the click was outside the context element because we want to ignore
// clicks inside it or any child of this element
if (!isElement(el, target) && !insideContext) {
callback(target, event)
}
}
document.body.addEventListener('click', clickOutsideEvent)
return () => {
el.removeEventListener('click', clickOutsideClickEvent)
document.body.removeEventListener('mousedown', clickOutsideMouseDownEvent)
document.body.removeEventListener('click', clickOutsideEvent)
}
}
/**
* Return whether one of the ancestors of the given node matches the given predicate.
*
* @param {DomElement} node The node to start the search for.
* @param {function} predicate The predicate to test on every ancestor.
* @param {DomElement} stop If provided, the search will stop when this element is met.
* It should an ancestor of the given node.
* @returns Whether one of the ancestors matches the predicate.
*/
export const doesAncestorMatchPredicate = (node, predicate, stop) => {
while (node != null) {
if (stop === node) {
return false
}
if (predicate(node)) {
return true
}
node = node.parentNode
}
return false
}
/**
* Checks a given predicate on all intermediate DOM elements between a descendant and its ancestor.
*
* @param {HTMLElement} ancestor - The ancestor DOM element.
* @param {HTMLElement} descendant - The descendant DOM element. The function assumes that
* this is a descendant of the ancestor element.
* @param {Function} predicate - A function that takes a DOM element as an argument and returns
* a boolean. This function is called on each intermediate element
* between the descendant and the ancestor.
* @returns {boolean} - Returns true if the predicate returns true for at least one intermediate
* element between the descendant and ancestor. Otherwise, returns false.
*
* @example
* // Example usage
* const ancestorElement = document.getElementById('parent');
* const descendantElement = document.getElementById('child');
* const result = checkIntermediateElements(ancestorElement, descendantElement, (el) => {
* return el.tagName === 'DIV';
* });
* console.log(result); // Outputs true if any intermediate element is a <div>, otherwise false.
*/
export const checkIntermediateElements = (ancestor, descendant, predicate) => {
for (let el = descendant; el && el !== ancestor; el = el.parentElement) {
if (predicate(el)) {
return true
}
}
return false
}