@chayns-components/core
Version:
A set of beautiful React components for developing your own applications with chayns.
395 lines (394 loc) • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = require("motion/react");
var _react2 = _interopRequireWildcard(require("react"));
var _reactDom = require("react-dom");
var _mentionFinder = require("../../constants/mentionFinder");
var _MentionFinderItem = _interopRequireDefault(require("./mention-finder-item/MentionFinderItem"));
var _MentionFinder = require("./MentionFinder.styles");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
const DRAG_CLOSE_THRESHOLD_IN_PX = 60;
const isTextInputElement = element => {
if (!element) {
return false;
}
const tagName = element.tagName.toLowerCase();
return tagName === 'input' && element.type !== 'button' || tagName === 'textarea' || element.isContentEditable;
};
const findTouchByIdentifier = (touchList, identifier) => {
if (identifier === null) {
return null;
}
for (let i = 0; i < touchList.length; i += 1) {
const touch = touchList.item(i);
if (touch && touch.identifier === identifier) {
return touch;
}
}
return null;
};
const MentionFinder = ({
inputValue,
members,
onSelect,
popupAlignment,
enableDragHandle = false,
dragCloseThresholdInPx = DRAG_CLOSE_THRESHOLD_IN_PX,
overlayContainerSelector
}) => {
const [activeMember, setActiveMember] = (0, _react2.useState)(members[0]);
const [focusedIndex, setFocusedIndex] = (0, _react2.useState)(0);
const [shouldShowPopup, setShouldShowPopup] = (0, _react2.useState)(true);
const popupRef = (0, _react2.useRef)(null);
const listRef = (0, _react2.useRef)(null);
const inputElementRef = (0, _react2.useRef)(null);
const dragStartYRef = (0, _react2.useRef)(null);
const hasTriggeredDragCloseRef = (0, _react2.useRef)(false);
const activePointerIdRef = (0, _react2.useRef)(null);
const activeTouchIdRef = (0, _react2.useRef)(null);
const [isDraggingHandle, setIsDraggingHandle] = (0, _react2.useState)(false);
const [dragOffset, setDragOffset] = (0, _react2.useState)(0);
const [dragProgress, setDragProgress] = (0, _react2.useState)(0);
const [overlayContainer, setOverlayContainer] = (0, _react2.useState)(null);
const [fullMatch, searchString] = (0, _react2.useMemo)(() => {
var _regExpMatchArray$;
// eslint-disable-next-line no-irregular-whitespace
const regExpMatchArray = inputValue.match(/@(?!\s)([^\s]*)/);
return [regExpMatchArray === null || regExpMatchArray === void 0 ? void 0 : regExpMatchArray[0], (regExpMatchArray === null || regExpMatchArray === void 0 || (_regExpMatchArray$ = regExpMatchArray[1]) === null || _regExpMatchArray$ === void 0 ? void 0 : _regExpMatchArray$.toLowerCase()) ?? ''];
}, [inputValue]);
const filteredMembers = (0, _react2.useMemo)(() => searchString !== '' ? members.filter(({
id,
info,
name
}) => id.toLowerCase().includes(searchString) || (info === null || info === void 0 ? void 0 : info.replace('chayns', '').toLowerCase().includes(searchString)) || name.toLowerCase().includes(searchString)) : members, [members, searchString]);
const shouldRenderPopup = shouldShowPopup && !!fullMatch && filteredMembers.length > 0;
const handleContainerBlur = (0, _react2.useCallback)(event => {
var _popupRef$current;
const nextFocusedElement = event.relatedTarget;
const currentContainer = event.currentTarget;
if (!nextFocusedElement || !currentContainer.contains(nextFocusedElement) && !((_popupRef$current = popupRef.current) !== null && _popupRef$current !== void 0 && _popupRef$current.contains(nextFocusedElement))) {
setShouldShowPopup(false);
}
}, []);
const handleKeyDown = (0, _react2.useCallback)(event => {
const targetElement = event.target;
if (isTextInputElement(targetElement)) {
inputElementRef.current = targetElement;
}
if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && shouldRenderPopup) {
var _listRef$current;
event.preventDefault();
const children = (_listRef$current = listRef.current) === null || _listRef$current === void 0 ? void 0 : _listRef$current.children;
if (children && children.length > 0) {
const newIndex = focusedIndex !== null ? (focusedIndex + (event.key === 'ArrowUp' ? -1 : 1) + children.length) % children.length : 0;
if (focusedIndex !== null) {
const prevElement = children[focusedIndex];
prevElement.tabIndex = -1;
}
setFocusedIndex(newIndex);
const member = filteredMembers[newIndex];
setActiveMember(member);
const newElement = children[newIndex];
newElement.tabIndex = 0;
newElement.focus();
}
} else if (event.key === 'Enter' && shouldRenderPopup) {
event.preventDefault();
event.stopPropagation();
if (fullMatch && activeMember) {
onSelect({
fullMatch,
member: activeMember
});
}
} else if (event.key === 'Escape' && shouldRenderPopup) {
event.preventDefault();
event.stopPropagation();
setShouldShowPopup(false);
window.requestAnimationFrame(() => {
var _inputElementRef$curr;
(_inputElementRef$curr = inputElementRef.current) === null || _inputElementRef$curr === void 0 || _inputElementRef$curr.focus();
});
}
}, [activeMember, filteredMembers, focusedIndex, fullMatch, onSelect, shouldRenderPopup]);
const handleMemberClick = (0, _react2.useCallback)(member => {
if (fullMatch) {
onSelect({
fullMatch,
member
});
}
}, [fullMatch, onSelect]);
const handleMemberHover = (0, _react2.useCallback)(member => {
setActiveMember(member);
}, []);
(0, _react2.useEffect)(() => {
if (filteredMembers.length > 0) {
const isActiveMemberShown = filteredMembers.some(({
id
}) => id === (activeMember === null || activeMember === void 0 ? void 0 : activeMember.id));
if (!isActiveMemberShown) {
setActiveMember(filteredMembers[0]);
}
}
}, [activeMember === null || activeMember === void 0 ? void 0 : activeMember.id, filteredMembers]);
const items = (0, _react2.useMemo)(() => filteredMembers.map(member => /*#__PURE__*/_react2.default.createElement(_MentionFinderItem.default, {
isActive: member.id === (activeMember === null || activeMember === void 0 ? void 0 : activeMember.id),
key: member.id,
member: member,
onClick: handleMemberClick,
onHover: handleMemberHover
})), [activeMember, filteredMembers, handleMemberClick, handleMemberHover]);
(0, _react2.useEffect)(() => {
setDragOffset(0);
setDragProgress(0);
hasTriggeredDragCloseRef.current = false;
setShouldShowPopup(true);
}, [inputValue]);
(0, _react2.useEffect)(() => {
if (!shouldRenderPopup) {
return;
}
const activeElement = document.activeElement;
if (isTextInputElement(activeElement)) {
inputElementRef.current = activeElement;
}
}, [shouldRenderPopup]);
(0, _react2.useEffect)(() => {
if (shouldShowPopup) {
window.addEventListener('keydown', handleKeyDown, true);
}
return () => {
window.removeEventListener('keydown', handleKeyDown, true);
};
}, [handleKeyDown, shouldShowPopup]);
const closePopupViaDrag = (0, _react2.useCallback)(() => {
if (hasTriggeredDragCloseRef.current) {
return;
}
hasTriggeredDragCloseRef.current = true;
setDragOffset(0);
setDragProgress(0);
setShouldShowPopup(false);
}, []);
const handleOverlayClick = (0, _react2.useCallback)(() => {
closePopupViaDrag();
}, [closePopupViaDrag]);
const updateDragByClientY = (0, _react2.useCallback)(clientY => {
if (dragStartYRef.current === null) {
return false;
}
const closingDirectionMultiplier = popupAlignment === _mentionFinder.MentionFinderPopupAlignment.Bottom ? -1 : 1;
const rawDelta = clientY - dragStartYRef.current;
const normalizedDelta = rawDelta * closingDirectionMultiplier;
const positiveDelta = Math.max(0, normalizedDelta);
const limitedDelta = Math.min(positiveDelta, dragCloseThresholdInPx);
setDragOffset(limitedDelta * closingDirectionMultiplier);
setDragProgress(Math.min(positiveDelta / dragCloseThresholdInPx, 1));
if (positiveDelta >= dragCloseThresholdInPx) {
dragStartYRef.current = null;
setIsDraggingHandle(false);
closePopupViaDrag();
return true;
}
return false;
}, [closePopupViaDrag, dragCloseThresholdInPx, popupAlignment]);
const handleDragPointerDown = (0, _react2.useCallback)(event => {
if (event.pointerType === 'touch') {
return;
}
if (event.pointerType === 'mouse' && event.button !== 0) {
return;
}
if (dragStartYRef.current !== null) {
return;
}
activePointerIdRef.current = event.pointerId;
dragStartYRef.current = event.clientY;
hasTriggeredDragCloseRef.current = false;
setDragOffset(0);
setDragProgress(0);
setIsDraggingHandle(true);
event.currentTarget.setPointerCapture(event.pointerId);
event.stopPropagation();
}, []);
const handleDragPointerMove = (0, _react2.useCallback)(event => {
if (event.pointerType === 'touch' || activePointerIdRef.current !== event.pointerId) {
return;
}
if (dragStartYRef.current === null) {
return;
}
event.preventDefault();
event.stopPropagation();
const hasClosed = updateDragByClientY(event.clientY);
if (hasClosed) {
event.currentTarget.releasePointerCapture(event.pointerId);
activePointerIdRef.current = null;
}
}, [updateDragByClientY]);
const resetDragState = (0, _react2.useCallback)((shouldClose = false) => {
dragStartYRef.current = null;
setIsDraggingHandle(false);
if (shouldClose) {
closePopupViaDrag();
} else {
setDragOffset(0);
setDragProgress(0);
}
}, [closePopupViaDrag]);
const handleDragPointerUp = (0, _react2.useCallback)(event => {
if (event.pointerType === 'touch' || activePointerIdRef.current !== event.pointerId) {
return;
}
event.stopPropagation();
const shouldClose = dragProgress >= 1;
resetDragState(shouldClose);
event.currentTarget.releasePointerCapture(event.pointerId);
activePointerIdRef.current = null;
}, [dragProgress, resetDragState]);
const handleDragPointerCancel = (0, _react2.useCallback)(event => {
if (event.pointerType === 'touch' || activePointerIdRef.current !== event.pointerId) {
return;
}
event.stopPropagation();
resetDragState();
event.currentTarget.releasePointerCapture(event.pointerId);
activePointerIdRef.current = null;
}, [resetDragState]);
const handleDragTouchStart = (0, _react2.useCallback)(event => {
if (event.touches.length === 0 || dragStartYRef.current !== null) {
return;
}
const touch = event.touches[0];
if (!touch) return;
activeTouchIdRef.current = touch.identifier;
dragStartYRef.current = touch.clientY;
hasTriggeredDragCloseRef.current = false;
setDragOffset(0);
setDragProgress(0);
setIsDraggingHandle(true);
event.preventDefault();
event.stopPropagation();
}, []);
const handleDragTouchMove = (0, _react2.useCallback)(event => {
if (dragStartYRef.current === null || activeTouchIdRef.current === null) {
return;
}
const touch = findTouchByIdentifier(event.changedTouches, activeTouchIdRef.current) ?? findTouchByIdentifier(event.touches, activeTouchIdRef.current);
if (!touch) return;
event.preventDefault();
event.stopPropagation();
const hasClosed = updateDragByClientY(touch.clientY);
if (hasClosed) {
activeTouchIdRef.current = null;
}
}, [updateDragByClientY]);
const handleDragTouchEnd = (0, _react2.useCallback)(event => {
if (activeTouchIdRef.current === null) return;
const touch = findTouchByIdentifier(event.changedTouches, activeTouchIdRef.current);
if (!touch) return;
event.preventDefault();
event.stopPropagation();
activeTouchIdRef.current = null;
const shouldClose = dragProgress >= 1;
resetDragState(shouldClose);
}, [dragProgress, resetDragState]);
const handleDragTouchCancel = (0, _react2.useCallback)(event => {
if (activeTouchIdRef.current === null) return;
const touch = findTouchByIdentifier(event.changedTouches, activeTouchIdRef.current);
if (!touch) return;
event.preventDefault();
event.stopPropagation();
activeTouchIdRef.current = null;
resetDragState();
}, [resetDragState]);
const dragHandle = (0, _react2.useMemo)(() => {
if (!enableDragHandle) {
return null;
}
return /*#__PURE__*/_react2.default.createElement(_MentionFinder.StyledMentionFinderDragHandle, {
$alignment: popupAlignment
}, /*#__PURE__*/_react2.default.createElement(_MentionFinder.StyledMentionFinderDragHandleInner, {
$alignment: popupAlignment,
$dragOffset: dragOffset,
$dragProgress: dragProgress,
$isDragging: isDraggingHandle,
onPointerDown: handleDragPointerDown,
onPointerMove: handleDragPointerMove,
onPointerUp: handleDragPointerUp,
onPointerCancel: handleDragPointerCancel,
onTouchStart: handleDragTouchStart,
onTouchMove: handleDragTouchMove,
onTouchEnd: handleDragTouchEnd,
onTouchCancel: handleDragTouchCancel
}));
}, [enableDragHandle, popupAlignment, dragOffset, dragProgress, isDraggingHandle, handleDragPointerDown, handleDragPointerMove, handleDragPointerUp, handleDragPointerCancel, handleDragTouchStart, handleDragTouchMove, handleDragTouchEnd, handleDragTouchCancel]);
(0, _react2.useEffect)(() => {
var _popupRef$current2;
if (!enableDragHandle || !shouldRenderPopup) {
setOverlayContainer(null);
return;
}
const container = (_popupRef$current2 = popupRef.current) === null || _popupRef$current2 === void 0 ? void 0 : _popupRef$current2.closest(overlayContainerSelector ?? '.dialog-inner, .chayns-threads, .page-provider, .tapp');
if (container) {
setOverlayContainer(container);
return;
}
if (typeof document !== 'undefined') {
setOverlayContainer(document.body);
}
}, [enableDragHandle, overlayContainerSelector, shouldRenderPopup]);
const overlayPortal = overlayContainer && enableDragHandle ? /*#__PURE__*/(0, _reactDom.createPortal)(/*#__PURE__*/_react2.default.createElement(_react.AnimatePresence, {
initial: false
}, shouldRenderPopup && /*#__PURE__*/_react2.default.createElement(_MentionFinder.StyledMentionFinderOverlay, {
key: "mention-finder-overlay",
animate: {
opacity: 1
},
exit: {
opacity: 0
},
initial: {
opacity: 0
},
onClick: handleOverlayClick
})), overlayContainer) : null;
return /*#__PURE__*/_react2.default.createElement(_react2.default.Fragment, null, overlayPortal, /*#__PURE__*/_react2.default.createElement(_MentionFinder.StyledMentionFinder, {
className: "beta-chayns-mention-finder",
onBlur: handleContainerBlur
}, /*#__PURE__*/_react2.default.createElement(_react.AnimatePresence, {
initial: false
}, shouldRenderPopup && /*#__PURE__*/_react2.default.createElement(_MentionFinder.StyledMotionMentionFinderPopup, {
ref: popupRef,
animate: {
height: 'auto',
opacity: 1,
y: dragOffset
},
className: "prevent-lose-focus",
exit: {
height: 0,
opacity: 0
},
initial: {
height: 0,
opacity: 0
},
$popupAlignment: popupAlignment,
$isDragging: enableDragHandle,
transition: {
duration: 0.15
},
tabIndex: 0
}, enableDragHandle && popupAlignment === _mentionFinder.MentionFinderPopupAlignment.Top && dragHandle, /*#__PURE__*/_react2.default.createElement(_MentionFinder.StyledMentionFinderItemList, {
ref: listRef
}, items), enableDragHandle && popupAlignment === _mentionFinder.MentionFinderPopupAlignment.Bottom && dragHandle))));
};
MentionFinder.displayName = 'MentionFinder';
var _default = exports.default = MentionFinder;
//# sourceMappingURL=MentionFinder.js.map