|
import { addEventListener, addMeltEventListener, effect, executeCallbacks, isFirefox, isHTMLElement, isTouchDevice, makeElement, noop, styleToString, } from '../../internal/helpers/index.js'; |
|
import { createStateMachine } from '../../internal/helpers/store/stateMachine.js'; |
|
import { name } from './create.js'; |
|
import { debounceCallback, getThumbSize, resizeObserver } from './helpers.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function createBaseScrollbarAction(state) { |
|
const { rootState, scrollbarState } = state; |
|
scrollbarState.isVisible.set(true); |
|
function handleDragScroll(e) { |
|
const $domRect = scrollbarState.domRect.get(); |
|
if (!$domRect) |
|
return; |
|
const x = e.clientX - $domRect.left; |
|
const y = e.clientY - $domRect.top; |
|
const $isHorizontal = scrollbarState.isHorizontal.get(); |
|
if ($isHorizontal) { |
|
scrollbarState.onDragScroll(x); |
|
} |
|
else { |
|
scrollbarState.onDragScroll(y); |
|
} |
|
} |
|
function handlePointerDown(e) { |
|
if (e.button !== 0) |
|
return; |
|
const target = e.target; |
|
if (!isHTMLElement(target)) |
|
return; |
|
target.setPointerCapture(e.pointerId); |
|
const currentTarget = e.currentTarget; |
|
if (!isHTMLElement(currentTarget)) |
|
return; |
|
scrollbarState.domRect.set(currentTarget.getBoundingClientRect()); |
|
scrollbarState.prevWebkitUserSelect.set(document.body.style.webkitUserSelect); |
|
document.body.style.webkitUserSelect = 'none'; |
|
const $viewportEl = rootState.viewportEl.get(); |
|
if ($viewportEl) { |
|
$viewportEl.style.scrollBehavior = 'auto'; |
|
} |
|
handleDragScroll(e); |
|
} |
|
function handlePointerMove(e) { |
|
handleDragScroll(e); |
|
} |
|
function handlePointerUp(e) { |
|
const target = e.target; |
|
if (!isHTMLElement(target)) |
|
return; |
|
if (target.hasPointerCapture(e.pointerId)) { |
|
target.releasePointerCapture(e.pointerId); |
|
} |
|
document.body.style.webkitUserSelect = scrollbarState.prevWebkitUserSelect.get(); |
|
const $viewportEl = rootState.viewportEl.get(); |
|
if ($viewportEl) { |
|
$viewportEl.style.scrollBehavior = ''; |
|
} |
|
scrollbarState.domRect.set(null); |
|
} |
|
function handleWheel(e) { |
|
const target = e.target; |
|
const currentTarget = e.currentTarget; |
|
if (!isHTMLElement(target) || !isHTMLElement(currentTarget)) |
|
return; |
|
const isScrollbarWheel = currentTarget.contains(target); |
|
if (!isScrollbarWheel) |
|
return; |
|
const $sizes = scrollbarState.sizes.get(); |
|
if (!$sizes) |
|
return; |
|
const maxScrollPos = $sizes.content - $sizes.viewport; |
|
scrollbarState.handleWheelScroll(e, maxScrollPos); |
|
} |
|
function baseAction(node) { |
|
scrollbarState.scrollbarEl.set(node); |
|
const unsubEvents = executeCallbacks(addMeltEventListener(node, 'pointerdown', handlePointerDown), addMeltEventListener(node, 'pointermove', handlePointerMove), addMeltEventListener(node, 'pointerup', handlePointerUp), addEventListener(document, 'wheel', handleWheel, { passive: false })); |
|
const unsubResizeContent = effect([rootState.contentEl], ([$contentEl]) => { |
|
if (!$contentEl) |
|
return noop; |
|
return resizeObserver($contentEl, scrollbarState.handleSizeChange); |
|
}); |
|
return { |
|
destroy() { |
|
unsubEvents(); |
|
unsubResizeContent(); |
|
}, |
|
}; |
|
} |
|
return baseAction; |
|
} |
|
|
|
|
|
|
|
|
|
export function createAutoScrollbarAction(state) { |
|
|
|
|
|
const baseAction = createBaseScrollbarAction(state); |
|
const { rootState, scrollbarState } = state; |
|
const handleResize = debounceCallback(() => { |
|
const $viewportEl = rootState.viewportEl.get(); |
|
if (!$viewportEl) |
|
return; |
|
const isOverflowX = $viewportEl.offsetWidth < $viewportEl.scrollWidth; |
|
const isOverflowY = $viewportEl.offsetHeight < $viewportEl.scrollHeight; |
|
scrollbarState.isVisible.set(scrollbarState.isHorizontal.get() ? isOverflowX : isOverflowY); |
|
}, 10); |
|
function scrollbarAutoAction(node) { |
|
const unsubBaseAction = baseAction(node)?.destroy; |
|
handleResize(); |
|
const unsubObservers = []; |
|
const $viewportEl = rootState.viewportEl.get(); |
|
if ($viewportEl) { |
|
unsubObservers.push(resizeObserver($viewportEl, handleResize)); |
|
} |
|
const $contentEl = rootState.contentEl.get(); |
|
if ($contentEl) { |
|
unsubObservers.push(resizeObserver($contentEl, handleResize)); |
|
} |
|
return { |
|
destroy() { |
|
unsubObservers.forEach((unsub) => unsub()); |
|
unsubBaseAction(); |
|
}, |
|
}; |
|
} |
|
return scrollbarAutoAction; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function createHoverScrollbarAction(state) { |
|
|
|
|
|
const baseAction = createBaseScrollbarAction(state); |
|
const { rootState, scrollbarState } = state; |
|
|
|
|
|
scrollbarState.isVisible.set(false); |
|
let timeout; |
|
function handlePointerEnter() { |
|
window.clearTimeout(timeout); |
|
if (scrollbarState.isVisible.get()) |
|
return; |
|
const $viewportEl = rootState.viewportEl.get(); |
|
if (!$viewportEl) |
|
return; |
|
const isOverflowX = $viewportEl.offsetWidth < $viewportEl.scrollWidth; |
|
const isOverflowY = $viewportEl.offsetHeight < $viewportEl.scrollHeight; |
|
scrollbarState.isVisible.set(scrollbarState.isHorizontal.get() ? isOverflowX : isOverflowY); |
|
} |
|
function handlePointerLeave() { |
|
timeout = window.setTimeout(() => { |
|
if (!scrollbarState.isVisible.get()) |
|
return; |
|
scrollbarState.isVisible.set(false); |
|
}, rootState.options.hideDelay.get()); |
|
} |
|
function scrollbarHoverAction(node) { |
|
const unsubBaseAction = baseAction(node)?.destroy; |
|
const scrollAreaEl = node.closest('[data-melt-scroll-area]'); |
|
let unsubScrollAreaListeners = noop; |
|
if (scrollAreaEl) { |
|
if (isTouchDevice()) { |
|
unsubScrollAreaListeners = executeCallbacks(addEventListener(scrollAreaEl, 'touchstart', handlePointerEnter), addEventListener(scrollAreaEl, 'touchend', handlePointerLeave)); |
|
} |
|
else if (isFirefox()) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
unsubScrollAreaListeners = executeCallbacks(addEventListener(scrollAreaEl, 'pointerenter', handlePointerEnter), addEventListener(scrollAreaEl, 'mouseenter', handlePointerEnter), addEventListener(scrollAreaEl, 'mouseleave', handlePointerLeave)); |
|
} |
|
else { |
|
unsubScrollAreaListeners = executeCallbacks(addEventListener(scrollAreaEl, 'pointerenter', handlePointerEnter), addEventListener(scrollAreaEl, 'pointerleave', handlePointerLeave)); |
|
} |
|
} |
|
return { |
|
destroy() { |
|
unsubBaseAction?.(); |
|
unsubScrollAreaListeners(); |
|
}, |
|
}; |
|
} |
|
return scrollbarHoverAction; |
|
} |
|
|
|
|
|
|
|
|
|
export function createScrollScrollbarAction(state) { |
|
|
|
|
|
const baseAction = createBaseScrollbarAction(state); |
|
const { rootState, scrollbarState } = state; |
|
const machine = createStateMachine('hidden', { |
|
hidden: { |
|
SCROLL: 'scrolling', |
|
}, |
|
scrolling: { |
|
SCROLL_END: 'idle', |
|
POINTER_ENTER: 'interacting', |
|
}, |
|
interacting: { |
|
SCROLL: 'interacting', |
|
POINTER_LEAVE: 'idle', |
|
}, |
|
idle: { |
|
HIDE: 'hidden', |
|
SCROLL: 'scrolling', |
|
POINTER_ENTER: 'interacting', |
|
}, |
|
}); |
|
effect([machine.state], ([$status]) => { |
|
if ($status === 'idle') { |
|
window.setTimeout(() => { |
|
machine.dispatch('HIDE'); |
|
}, rootState.options.hideDelay.get()); |
|
} |
|
if ($status === 'hidden') { |
|
scrollbarState.isVisible.set(false); |
|
} |
|
else { |
|
scrollbarState.isVisible.set(true); |
|
} |
|
}); |
|
const debounceScrollEnd = debounceCallback(() => machine.dispatch('SCROLL_END'), 100); |
|
effect([rootState.viewportEl, scrollbarState.isHorizontal], ([$viewportEl, $isHorizontal]) => { |
|
const scrollDirection = $isHorizontal ? 'scrollLeft' : 'scrollTop'; |
|
let unsub = noop; |
|
if ($viewportEl) { |
|
let prevScrollPos = $viewportEl[scrollDirection]; |
|
const handleScroll = () => { |
|
const scrollPos = $viewportEl[scrollDirection]; |
|
const hasScrollInDirectionChanged = prevScrollPos !== scrollPos; |
|
if (hasScrollInDirectionChanged) { |
|
machine.dispatch('SCROLL'); |
|
debounceScrollEnd(); |
|
} |
|
prevScrollPos = scrollPos; |
|
}; |
|
unsub = addEventListener($viewportEl, 'scroll', handleScroll); |
|
} |
|
return () => { |
|
unsub(); |
|
}; |
|
}); |
|
function scrollbarScrollAction(node) { |
|
const unsubBaseAction = baseAction(node)?.destroy; |
|
const unsubListeners = executeCallbacks(addEventListener(node, 'pointerenter', () => machine.dispatch('POINTER_ENTER')), addEventListener(node, 'pointerleave', () => machine.dispatch('POINTER_LEAVE'))); |
|
return { |
|
destroy() { |
|
unsubBaseAction?.(); |
|
unsubListeners(); |
|
}, |
|
}; |
|
} |
|
return scrollbarScrollAction; |
|
} |
|
|
|
|
|
|
|
export function createScrollbarX(state, createAction) { |
|
const action = createAction(state); |
|
const { rootState, scrollbarState } = state; |
|
return makeElement(name('scrollbar'), { |
|
stores: [scrollbarState.sizes, rootState.options.dir, scrollbarState.isVisible], |
|
returned: ([$sizes, $dir, $isVisible]) => { |
|
return { |
|
style: styleToString({ |
|
position: 'absolute', |
|
bottom: 0, |
|
left: $dir === 'rtl' ? 'var(--melt-scroll-area-corner-width)' : 0, |
|
right: $dir === 'ltr' ? 'var(--melt-scroll-area-corner-width)' : 0, |
|
'--melt-scroll-area-thumb-width': `${getThumbSize($sizes)}px`, |
|
visibility: !$isVisible ? 'hidden' : undefined, |
|
}), |
|
'data-state': $isVisible ? 'visible' : 'hidden', |
|
}; |
|
}, |
|
action: (node) => { |
|
const unsubAction = action(node)?.destroy; |
|
rootState.scrollbarXEl.set(node); |
|
rootState.scrollbarXEnabled.set(true); |
|
return { |
|
destroy() { |
|
unsubAction?.(); |
|
rootState.scrollbarXEl.set(null); |
|
}, |
|
}; |
|
}, |
|
}); |
|
} |
|
|
|
|
|
|
|
export function createScrollbarY(state, createAction) { |
|
const action = createAction(state); |
|
const { rootState, scrollbarState } = state; |
|
return makeElement(name('scrollbar'), { |
|
stores: [scrollbarState.sizes, rootState.options.dir, scrollbarState.isVisible], |
|
returned: ([$sizes, $dir, $isVisible]) => { |
|
return { |
|
style: styleToString({ |
|
position: 'absolute', |
|
top: 0, |
|
right: $dir === 'ltr' ? 0 : undefined, |
|
left: $dir === 'rtl' ? 0 : undefined, |
|
bottom: 'var(--melt-scroll-area-corner-height)', |
|
'--melt-scroll-area-thumb-height': `${getThumbSize($sizes)}px`, |
|
visibility: !$isVisible ? 'hidden' : undefined, |
|
}), |
|
'data-state': $isVisible ? 'visible' : 'hidden', |
|
}; |
|
}, |
|
action: (node) => { |
|
const unsubAction = action(node)?.destroy; |
|
rootState.scrollbarYEl.set(node); |
|
rootState.scrollbarYEnabled.set(true); |
|
return { |
|
destroy() { |
|
unsubAction?.(); |
|
rootState.scrollbarYEl.set(null); |
|
}, |
|
}; |
|
}, |
|
}); |
|
} |
|
export function getScrollbarActionByType(type) { |
|
switch (type) { |
|
case 'always': |
|
return createBaseScrollbarAction; |
|
case 'auto': |
|
return createAutoScrollbarAction; |
|
case 'hover': |
|
return createHoverScrollbarAction; |
|
case 'scroll': |
|
return createScrollScrollbarAction; |
|
default: |
|
return createBaseScrollbarAction; |
|
} |
|
} |
|
|