@melt-ui/svelte
Advanced tools
Comparing version 0.78.0 to 0.79.0
@@ -31,3 +31,3 @@ /// <reference types="svelte" /> | ||
}, string>; | ||
edit: import("../../internal/helpers/index.js").MeltElement<[import("../../internal/helpers/withGet.js").WithGet<import("svelte/store").Writable<Tag | null>>, import("../../internal/helpers/withGet.js").WithGet<import("svelte/store").Writable<boolean>>], (node: HTMLElement) => MeltActionReturn<TagsInputEvents['edit']>, ([$editing, $editable]: [Tag | null, boolean]) => (tag: Tag) => { | ||
edit: import("../../internal/helpers/index.js").MeltElement<import("svelte/store").Readable<(tag: Tag) => boolean>, (node: HTMLElement) => MeltActionReturn<TagsInputEvents['edit']>, ($isEditing: (tag: Tag) => boolean) => (tag: Tag) => { | ||
readonly 'aria-hidden': boolean; | ||
@@ -37,3 +37,3 @@ readonly 'data-tag-id': string; | ||
readonly hidden: true | undefined; | ||
readonly contenteditable: boolean | undefined; | ||
readonly contenteditable: boolean; | ||
readonly tabindex: -1; | ||
@@ -71,2 +71,3 @@ readonly style: string | undefined; | ||
isSelected: import("svelte/store").Readable<(tag: Tag) => boolean>; | ||
isEditing: import("svelte/store").Readable<(tag: Tag) => boolean>; | ||
isInputValid: (v: string) => boolean; | ||
@@ -73,0 +74,0 @@ addTag: (v: string) => Promise<boolean>; |
@@ -1,2 +0,2 @@ | ||
import { addMeltEventListener, makeElement, createElHelpers, disabledAttr, effect, executeCallbacks, generateId, getElementByMeltId, isBrowser, isHTMLElement, kbd, omit, overridable, styleToString, toWritableStores, } from '../../internal/helpers/index.js'; | ||
import { addMeltEventListener, makeElement, createElHelpers, disabledAttr, effect, executeCallbacks, generateId, getElementByMeltId, isBrowser, isHTMLElement, kbd, omit, overridable, styleToString, toWritableStores, noop, } from '../../internal/helpers/index.js'; | ||
import { withGet } from '../../internal/helpers/withGet.js'; | ||
@@ -7,2 +7,3 @@ import { tick } from 'svelte'; | ||
import { focusInput, highlightText, setSelectedFromEl } from './helpers.js'; | ||
import { useInteractOutside } from '../../internal/actions/index.js'; | ||
const defaults = { | ||
@@ -170,2 +171,6 @@ placeholder: '', | ||
} | ||
// Used to determine if tag is being edited | ||
const isEditing = derived([editable, editing], ([$editable, $editing]) => { | ||
return (tag) => $editable && $editing?.id === tag.id; | ||
}); | ||
const root = makeElement(name(''), { | ||
@@ -530,7 +535,6 @@ stores: [disabled], | ||
const edit = makeElement(name('edit'), { | ||
stores: [editing, editable], | ||
returned: ([$editing, $editable]) => { | ||
stores: isEditing, | ||
returned: ($isEditing) => { | ||
return (tag) => { | ||
const editable = $editable; | ||
const editing = editable ? $editing?.id === tag.id : undefined; | ||
const editing = $isEditing(tag); | ||
return { | ||
@@ -563,48 +567,56 @@ 'aria-hidden': !editing, | ||
}; | ||
const unsub = executeCallbacks(addMeltEventListener(node, 'blur', () => { | ||
if (node.hasAttribute('hidden')) | ||
let unsubInteractOutside = noop; | ||
let unsubEvents = noop; | ||
const unsubDerived = effect(isEditing, ($isEditing) => { | ||
unsubInteractOutside(); | ||
unsubEvents(); | ||
if (!$isEditing(getElProps())) | ||
return; | ||
// Stop editing, reset the value to the original and clear an invalid state | ||
editing.set(null); | ||
node.textContent = getElProps().value; | ||
getElementByMeltId(meltIds.root)?.removeAttribute('data-invalid-edit'); | ||
node.removeAttribute('data-invalid-edit'); | ||
}), addMeltEventListener(node, 'keydown', async (e) => { | ||
if (node.hasAttribute('hidden')) | ||
return; | ||
if (e.key === kbd.ENTER) { | ||
// Capture the edit value, validate and then update | ||
e.preventDefault(); | ||
// Do nothing when the value is empty | ||
const value = node.textContent; | ||
if (!value) | ||
return; | ||
const t = { id: getElProps().id, value }; | ||
if (isInputValid(value) && (await updateTag(t, true))) { | ||
node.textContent = t.value; | ||
unsubInteractOutside = useInteractOutside(node).destroy; | ||
unsubEvents = executeCallbacks(addMeltEventListener(node, 'blur', () => { | ||
// Stop editing, reset the value to the original and clear an invalid state | ||
editing.set(null); | ||
node.textContent = getElProps().value; | ||
getElementByMeltId(meltIds.root)?.removeAttribute('data-invalid-edit'); | ||
node.removeAttribute('data-invalid-edit'); | ||
}), addMeltEventListener(node, 'keydown', async (e) => { | ||
if (e.key === kbd.ENTER) { | ||
// Capture the edit value, validate and then update | ||
e.preventDefault(); | ||
// Do nothing when the value is empty | ||
const value = node.textContent; | ||
if (!value) | ||
return; | ||
const t = { id: getElProps().id, value }; | ||
if (isInputValid(value) && (await updateTag(t, true))) { | ||
node.textContent = t.value; | ||
editValue.set(''); | ||
focusInput(meltIds.input); | ||
} | ||
else { | ||
getElementByMeltId(meltIds.root)?.setAttribute('data-invalid-edit', ''); | ||
node.setAttribute('data-invalid-edit', ''); | ||
} | ||
} | ||
else if (e.key === kbd.ESCAPE) { | ||
// Reset the value, clear the edit value store, set this tag as | ||
// selected and focus on input | ||
e.preventDefault(); | ||
e.stopImmediatePropagation(); | ||
node.textContent = getElProps().value; | ||
editValue.set(''); | ||
setSelectedFromEl(node, selected); | ||
focusInput(meltIds.input); | ||
} | ||
else { | ||
getElementByMeltId(meltIds.root)?.setAttribute('data-invalid-edit', ''); | ||
node.setAttribute('data-invalid-edit', ''); | ||
} | ||
} | ||
else if (e.key === kbd.ESCAPE) { | ||
// Reset the value, clear the edit value store, set this tag as | ||
// selected and focus on input | ||
e.preventDefault(); | ||
node.textContent = getElProps().value; | ||
editValue.set(''); | ||
setSelectedFromEl(node, selected); | ||
focusInput(meltIds.input); | ||
} | ||
}), addMeltEventListener(node, 'input', () => { | ||
if (node.hasAttribute('hidden')) | ||
return; | ||
// Update the edit value store | ||
editValue.set(node.textContent || ''); | ||
})); | ||
}), addMeltEventListener(node, 'input', () => { | ||
// Update the edit value store | ||
editValue.set(node.textContent || ''); | ||
})); | ||
}); | ||
return { | ||
destroy: unsub, | ||
destroy() { | ||
unsubInteractOutside(); | ||
unsubEvents(); | ||
unsubDerived(); | ||
}, | ||
}; | ||
@@ -659,2 +671,3 @@ }, | ||
isSelected, | ||
isEditing, | ||
isInputValid, | ||
@@ -661,0 +674,0 @@ addTag, |
import { addEventListener, addMeltEventListener, makeElement, createElHelpers, effect, executeCallbacks, getPortalDestination, isBrowser, isDocument, isElement, isTouch, makeHullFromElements, noop, omit, overridable, styleToString, toWritableStores, portalAttr, isPointerInGraceArea, } from '../../internal/helpers/index.js'; | ||
import { useEscapeKeydown, useFloating, usePortal } from '../../internal/actions/index.js'; | ||
import { useEscapeKeydown, useFloating, useInteractOutside, usePortal, } from '../../internal/actions/index.js'; | ||
import { derived, writable } from 'svelte/store'; | ||
@@ -145,2 +145,3 @@ import { generateIds } from '../../internal/helpers/id.js'; | ||
let unsubPortal = noop; | ||
let unsubInteractOutside = noop; | ||
let unsubEscapeKeydown = noop; | ||
@@ -150,2 +151,3 @@ const unsubDerived = effect([isVisible, positioning, portal], ([$isVisible, $positioning, $portal]) => { | ||
unsubFloating(); | ||
unsubInteractOutside(); | ||
unsubEscapeKeydown(); | ||
@@ -158,7 +160,10 @@ const triggerEl = getEl('trigger'); | ||
unsubFloating(); | ||
unsubInteractOutside(); | ||
unsubEscapeKeydown(); | ||
const portalDest = getPortalDestination(node, $portal); | ||
if (portalDest) | ||
if (portalDest !== null) { | ||
unsubPortal = usePortal(node, portalDest).destroy; | ||
} | ||
unsubFloating = useFloating(triggerEl, node, $positioning).destroy; | ||
unsubInteractOutside = useInteractOutside(node).destroy; | ||
const onEscapeKeyDown = () => { | ||
@@ -205,2 +210,3 @@ if (openTimeout) { | ||
unsubDerived(); | ||
unsubInteractOutside(); | ||
}, | ||
@@ -207,0 +213,0 @@ }; |
@@ -1,2 +0,1 @@ | ||
export * from './click-outside/index.js'; | ||
export * from './escape-keydown/index.js'; | ||
@@ -3,0 +2,0 @@ export * from './floating/index.js'; |
@@ -1,2 +0,1 @@ | ||
export * from './click-outside/index.js'; | ||
export * from './escape-keydown/index.js'; | ||
@@ -3,0 +2,0 @@ export * from './floating/index.js'; |
import type { InteractOutsideConfig } from './types.js'; | ||
export declare const useInteractOutside: <Node_1 extends HTMLElement>(node: Node_1, config: InteractOutsideConfig) => { | ||
export declare const useInteractOutside: <Node_1 extends HTMLElement>(node: Node_1, config?: InteractOutsideConfig) => { | ||
update: (config: InteractOutsideConfig) => void; | ||
destroy(): void; | ||
}; |
import { getOwnerDocument, isOrContainsTarget } from '../../helpers/elements.js'; | ||
import { addEventListener, isElement, executeCallbacks, noop, debounce, } from '../../helpers/index.js'; | ||
export const useInteractOutside = ((node, config) => { | ||
const layers = new Set(); | ||
export const useInteractOutside = ((node, config = {}) => { | ||
let unsubEvents = noop; | ||
@@ -8,2 +9,3 @@ let unsubPointerDown = noop; | ||
let unsubResetInterceptedEvents = noop; | ||
layers.add(node); | ||
const documentObj = getOwnerDocument(node); | ||
@@ -61,2 +63,3 @@ let isPointerDown = false; | ||
return; | ||
let wasTopLayerInPointerDownCapture = false; | ||
/** | ||
@@ -67,3 +70,3 @@ * Debouncing `onPointerDown` ensures that other events can be flagged as not intercepted, | ||
const onPointerDownDebounced = debounce((e) => { | ||
if (isAnyEventIntercepted()) | ||
if (!wasTopLayerInPointerDownCapture || isAnyEventIntercepted()) | ||
return; | ||
@@ -84,6 +87,7 @@ if (onInteractOutside && isValidEvent(e, node)) | ||
const onPointerUpDebounced = debounce((e) => { | ||
if (isAnyEventIntercepted()) | ||
return; | ||
if (shouldTriggerInteractOutside(e)) | ||
if (wasTopLayerInPointerDownCapture && | ||
!isAnyEventIntercepted() && | ||
shouldTriggerInteractOutside(e)) { | ||
onInteractOutside?.(e); | ||
} | ||
resetPointerState(); | ||
@@ -99,5 +103,8 @@ }, 10); | ||
unsubResetInterceptedEvents = resetInterceptedEventsDebounced.destroy; | ||
const markTopLayerInPointerDown = () => { | ||
wasTopLayerInPointerDownCapture = isHighestLayer(node); | ||
}; | ||
unsubEvents = executeCallbacks( | ||
/** Capture Events For Interaction Start */ | ||
setupCapturePhaseHandlerAndMarkAsIntercepted('pointerdown'), setupCapturePhaseHandlerAndMarkAsIntercepted('mousedown'), setupCapturePhaseHandlerAndMarkAsIntercepted('touchstart'), | ||
setupCapturePhaseHandlerAndMarkAsIntercepted('pointerdown', markTopLayerInPointerDown), setupCapturePhaseHandlerAndMarkAsIntercepted('mousedown', markTopLayerInPointerDown), setupCapturePhaseHandlerAndMarkAsIntercepted('touchstart', markTopLayerInPointerDown), | ||
/** | ||
@@ -138,2 +145,3 @@ * Intercepted events are reset only at the end of an interaction, allowing | ||
unsubResetInterceptedEvents(); | ||
layers.delete(node); | ||
}, | ||
@@ -155,1 +163,4 @@ }; | ||
} | ||
function isHighestLayer(node) { | ||
return Array.from(layers).at(-1) === node; | ||
} |
@@ -1,19 +0,5 @@ | ||
import { isElement, last, noop, sleep } from '../../helpers/index.js'; | ||
import { isElement, noop } from '../../helpers/index.js'; | ||
import { useInteractOutside } from '../index.js'; | ||
const visibleModals = []; | ||
export const useModal = ((node, config) => { | ||
let unsubInteractOutside = noop; | ||
// Filthy hack to allow closing elements before the new one appears | ||
sleep(100).then(() => { | ||
visibleModals.push(node); | ||
}); | ||
function removeNodeFromVisibleModals() { | ||
const index = visibleModals.indexOf(node); | ||
if (index >= 0) { | ||
visibleModals.splice(index, 1); | ||
} | ||
} | ||
function isLastModal() { | ||
return last(visibleModals) === node; | ||
} | ||
function update(config) { | ||
@@ -23,7 +9,3 @@ unsubInteractOutside(); | ||
function closeModal() { | ||
// we only want to call onClose if this is the topmost modal | ||
if (isLastModal() && onClose) { | ||
onClose(); | ||
removeNodeFromVisibleModals(); | ||
} | ||
onClose?.(); | ||
} | ||
@@ -34,11 +16,9 @@ function onInteractOutsideStart(e) { | ||
return; | ||
if (target && isLastModal()) { | ||
e.stopImmediatePropagation(); | ||
} | ||
e.stopImmediatePropagation(); | ||
} | ||
function onInteractOutside(e) { | ||
if (shouldCloseOnInteractOutside?.(e) && isLastModal()) { | ||
e.stopImmediatePropagation(); | ||
closeModal(); | ||
} | ||
if (!shouldCloseOnInteractOutside?.(e)) | ||
return; | ||
e.stopImmediatePropagation(); | ||
closeModal(); | ||
} | ||
@@ -48,2 +28,3 @@ unsubInteractOutside = useInteractOutside(node, { | ||
onInteractOutside: closeOnInteractOutside ? onInteractOutside : undefined, | ||
enabled: closeOnInteractOutside, | ||
}).destroy; | ||
@@ -55,3 +36,2 @@ } | ||
destroy() { | ||
removeNodeFromVisibleModals(); | ||
unsubInteractOutside(); | ||
@@ -58,0 +38,0 @@ }, |
{ | ||
"name": "@melt-ui/svelte", | ||
"version": "0.78.0", | ||
"version": "0.79.0", | ||
"license": "MIT", | ||
@@ -5,0 +5,0 @@ "repository": "github:melt-ui/melt-ui", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1151458
482
25555