// Modified from Grail UI v0.9.6 (2023-06-10) | |
// Source: https://github.com/grail-ui/grail-ui | |
// https://github.com/grail-ui/grail-ui/tree/master/packages/grail-ui/src/clickOutside/clickOutside.ts | |
import { readable } from 'svelte/store'; | |
import { addEventListener } from '../../helpers/event.js'; | |
import { get } from 'svelte/store'; | |
import { isFunction } from '../../helpers/is.js'; | |
/** | |
* Creates a readable store that tracks the latest PointerEvent that occurred on the document. | |
* | |
* @returns A function to unsubscribe from the event listener and stop tracking pointer events. | |
*/ | |
const documentClickStore = readable(undefined, (set) => { | |
/** | |
* Event handler for pointerdown events on the document. | |
* Updates the store's value with the latest PointerEvent and then resets it to undefined. | |
*/ | |
function clicked(event) { | |
set(event); | |
// New subscriptions will not trigger immediately | |
set(undefined); | |
} | |
// Adds a pointerdown event listener to the document, calling the clicked function when triggered. | |
const unsubscribe = addEventListener(document, 'pointerup', clicked, { | |
passive: false, | |
capture: true, | |
}); | |
// Returns a function to unsubscribe from the event listener and stop tracking pointer events. | |
return unsubscribe; | |
}); | |
export const useClickOutside = (node, config = {}) => { | |
let options = { enabled: true, ...config }; | |
// Returns true if the click outside handler is enabled | |
function isEnabled() { | |
return typeof options.enabled === 'boolean' ? options.enabled : get(options.enabled); | |
} | |
// Handle document clicks | |
const unsubscribe = documentClickStore.subscribe((e) => { | |
// If the click outside handler is disabled, or if the event is null or the node itself, return early | |
if (!isEnabled() || !e || e.target === node) { | |
return; | |
} | |
const composedPath = e.composedPath(); | |
// If the target is in the node, return early | |
if (composedPath.includes(node)) | |
return; | |
// If an ignore function is passed, check if it returns true | |
if (options.ignore) { | |
if (isFunction(options.ignore)) { | |
if (options.ignore(e)) | |
return; | |
} | |
// If an ignore array is passed, check if any elements in the array match the target | |
else if (Array.isArray(options.ignore)) { | |
if (options.ignore.length > 0 && | |
options.ignore.some((ignoreEl) => { | |
return ignoreEl && (e.target === ignoreEl || composedPath.includes(ignoreEl)); | |
})) | |
return; | |
} | |
} | |
// If none of the above conditions are met, call the handler | |
options.handler?.(e); | |
}); | |
return { | |
update(params) { | |
options = { ...options, ...params }; | |
}, | |
destroy() { | |
unsubscribe(); | |
}, | |
}; | |
}; | |