UNPKG

@chatui/core

Version:

The React library for Chatbot UI

439 lines (434 loc) 18.5 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.Composer = exports.CLASS_NAME_FOCUSING = void 0; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = _interopRequireWildcard(require("react")); var _clsx = _interopRequireDefault(require("clsx")); var _Recorder = require("../Recorder"); var _Toolbar = require("../Toolbar"); var _AccessoryWrap = require("./AccessoryWrap"); var _Popover = require("../Popover"); var _ToolbarItem = require("./ToolbarItem"); var _ComposerInput = require("./ComposerInput"); var _SendButton = require("./SendButton"); var _Action = require("./Action"); var _MarqueeBorder = require("./MarqueeBorder"); var _VoiceInput = require("../VoiceInput"); var _toggleClass = _interopRequireDefault(require("../../utils/toggleClass")); var _ua = require("../../utils/ua"); var _viewportTop = require("./viewportTop"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(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 (var _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); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var CLASS_NAME_FOCUSING = exports.CLASS_NAME_FOCUSING = 'S--focusing'; /** 阻止 mouseup 冒泡,用于拦截 ClickOutside 的 document 监听 */ var stopMouseUp = function stopMouseUp(ev) { ev.stopPropagation(); }; var Composer = exports.Composer = /*#__PURE__*/_react.default.forwardRef(function (props, ref) { var _props$text = props.text, initialText = _props$text === void 0 ? '' : _props$text, oTextOnce = props.textOnce, _props$inputType = props.inputType, initialInputType = _props$inputType === void 0 ? 'text' : _props$inputType, wideBreakpoint = props.wideBreakpoint, _props$placeholder = props.placeholder, oPlaceholder = _props$placeholder === void 0 ? '请输入...' : _props$placeholder, _props$recorder = props.recorder, recorder = _props$recorder === void 0 ? {} : _props$recorder, enableNewVoiceInput = props.enableNewVoiceInput, onInputTypeChange = props.onInputTypeChange, onFocus = props.onFocus, onBlur = props.onBlur, onChange = props.onChange, onSend = props.onSend, onBeforeSend = props.onBeforeSend, onImageSend = props.onImageSend, onAccessoryToggle = props.onAccessoryToggle, _props$toolbar = props.toolbar, toolbar = _props$toolbar === void 0 ? [] : _props$toolbar, onToolbarClick = props.onToolbarClick, rightAction = props.rightAction, inputOptions = props.inputOptions; var _useState = (0, _react.useState)(initialText), _useState2 = (0, _slicedToArray2.default)(_useState, 2), text = _useState2[0], setText = _useState2[1]; var _useState3 = (0, _react.useState)(oTextOnce), _useState4 = (0, _slicedToArray2.default)(_useState3, 2), textOnce = _useState4[0], setTextOnce = _useState4[1]; var _useState5 = (0, _react.useState)(!!text), _useState6 = (0, _slicedToArray2.default)(_useState5, 2), hasValue = _useState6[0], setHasValue = _useState6[1]; var _useState7 = (0, _react.useState)(oTextOnce || oPlaceholder), _useState8 = (0, _slicedToArray2.default)(_useState7, 2), placeholder = _useState8[0], setPlaceholder = _useState8[1]; var _useState9 = (0, _react.useState)(initialInputType || 'text'), _useState0 = (0, _slicedToArray2.default)(_useState9, 2), inputType = _useState0[0], setInputType = _useState0[1]; var _useState1 = (0, _react.useState)(false), _useState10 = (0, _slicedToArray2.default)(_useState1, 2), isAccessoryOpen = _useState10[0], setAccessoryOpen = _useState10[1]; var _useState11 = (0, _react.useState)(''), _useState12 = (0, _slicedToArray2.default)(_useState11, 2), accessoryContent = _useState12[0], setAccessoryContent = _useState12[1]; var _useState13 = (0, _react.useState)(false), _useState14 = (0, _slicedToArray2.default)(_useState13, 2), isVoicePanelOpen = _useState14[0], setVoicePanelOpen = _useState14[1]; // 新版语音输入 UI 面板状态 var _useState15 = (0, _react.useState)(_VoiceInput.VoiceInputStatus.INITED), _useState16 = (0, _slicedToArray2.default)(_useState15, 2), voiceStatus = _useState16[0], setVoiceStatus = _useState16[1]; var inputRef = (0, _react.useRef)(null); var focused = (0, _react.useRef)(false); var blurTimer = (0, _react.useRef)(); var valueTimer = (0, _react.useRef)(); var popoverTarget = (0, _react.useRef)(); var isMountRef = (0, _react.useRef)(false); var _useState17 = (0, _react.useState)(false), _useState18 = (0, _slicedToArray2.default)(_useState17, 2), isWide = _useState18[0], setWide = _useState18[1]; (0, _react.useEffect)(function () { var mq = wideBreakpoint && window.matchMedia ? window.matchMedia("(min-width: ".concat(wideBreakpoint, ")")) : false; function handleMq(e) { setWide(e.matches); } setWide(mq && mq.matches); if (mq) { mq.addListener(handleMq); } return function () { if (mq) { mq.removeListener(handleMq); } }; }, [wideBreakpoint]); (0, _react.useEffect)(function () { (0, _toggleClass.default)('S--wide', isWide); if (!isWide) { setAccessoryContent(''); } }, [isWide]); (0, _react.useEffect)(function () { if (isMountRef.current && onAccessoryToggle) { onAccessoryToggle(isAccessoryOpen); } }, [isAccessoryOpen, onAccessoryToggle]); (0, _react.useEffect)(function () { isMountRef.current = true; }, []); (0, _react.useEffect)(function () { var _window = window, visualViewport = _window.visualViewport; if (!visualViewport) return; var winHeight = window.innerHeight; function toggleFocusing() { if (window.innerHeight > winHeight) { // iOS 下第一次的时候 winHeight 有可能不准 winHeight = window.innerHeight; } // 视窗变高做失焦处理 // 场景:键盘收起键盘时并没有失去焦点 if (focused.current && visualViewport.height >= winHeight) { var _inputRef$current; (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.blur(); } } function resizeHandler() { // Android 没有下面安全区且可以悬浮键盘,故不做收起失焦处理 if (_ua.isIOS || _ua.isArkWeb && _ua.isAliApp) { toggleFocusing(); } } visualViewport.addEventListener('resize', resizeHandler); return function () { visualViewport.removeEventListener('resize', resizeHandler); }; }, []); (0, _react.useEffect)(function () { if (text) { clearTimeout(valueTimer.current); setHasValue(true); } else { // 中文上屏时有一瞬间会无值,所以做延迟处理 valueTimer.current = setTimeout(function () { setHasValue(false); }); } }, [text]); // 聚焦输入框 语音面板收起、唤起键盘后 重新计算 viewport-top(iOS) (0, _react.useEffect)(function () { if (!isVoicePanelOpen && _ua.isIOS && focused.current) { // 面板收起后延迟触发 updateViewportTop,等待软键盘弹出稳定 var timer = setTimeout(function () { (0, _viewportTop.updateViewportTop)(); }, 300); return function () { return clearTimeout(timer); }; } return; }, [isVoicePanelOpen]); (0, _react.useImperativeHandle)(ref, function () { return { setText: setText }; }); var handleInputTypeChange = (0, _react.useCallback)(function () { if (enableNewVoiceInput) { // 启用了新版语音输入 UI,使用 VoiceInput 面板交互 var willOpenVoicePanel = !isVoicePanelOpen; var nextType = willOpenVoicePanel ? 'voice' : 'text'; setInputType(nextType); setVoicePanelOpen(willOpenVoicePanel); if (willOpenVoicePanel) { // 打开语音面板时自动关闭工具栏,避免两个面板同时显示 setAccessoryOpen(false); setAccessoryContent(''); } else { // 关闭语音面板时重置语音输入状态、聚焦和选区文本输入框 setVoiceStatus(_VoiceInput.VoiceInputStatus.INITED); var input = inputRef.current; input.focus(); input.selectionStart = input.selectionEnd = input.value.length; } if (onInputTypeChange) { onInputTypeChange(nextType); } } else { // 原有逻辑:切换内联 Recorder var isVoice = inputType === 'voice'; var _nextType = isVoice ? 'text' : 'voice'; setInputType(_nextType); if (isVoice) { var _input = inputRef.current; _input.focus(); // eslint-disable-next-line no-multi-assign _input.selectionStart = _input.selectionEnd = _input.value.length; } if (onInputTypeChange) { onInputTypeChange(_nextType); } } }, [inputType, onInputTypeChange, enableNewVoiceInput, isVoicePanelOpen]); var handleInputFocus = (0, _react.useCallback)(function (e) { clearTimeout(blurTimer.current); (0, _toggleClass.default)(CLASS_NAME_FOCUSING, true); focused.current = true; if (_ua.isIOS) { (0, _viewportTop.updateViewportTop)(); } if (onFocus) { onFocus(e); } }, [onFocus]); var handleInputBlur = (0, _react.useCallback)(function (e) { blurTimer.current = setTimeout(function () { (0, _toggleClass.default)(CLASS_NAME_FOCUSING, false); focused.current = false; }, 0); if (_ua.isIOS) { (0, _viewportTop.setViewportTop)(0); } if (onBlur) { onBlur(e); } }, [onBlur]); var send = (0, _react.useCallback)(/*#__PURE__*/(0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee() { var sendType, sendContent, allowed; return _regenerator.default.wrap(function (_context) { while (1) switch (_context.prev = _context.next) { case 0: sendType = 'text'; sendContent = text || textOnce || ''; if (sendContent) { _context.next = 1; break; } return _context.abrupt("return"); case 1: if (!onBeforeSend) { _context.next = 3; break; } _context.next = 2; return Promise.resolve(onBeforeSend(sendType, sendContent)); case 2: allowed = _context.sent; if (allowed) { _context.next = 3; break; } return _context.abrupt("return"); case 3: if (text) { onSend(sendType, text); setText(''); } else if (textOnce) { onSend(sendType, textOnce); } if (textOnce) { setTextOnce(''); setPlaceholder(oPlaceholder); } if (focused.current) { inputRef.current.focus(); } case 4: case "end": return _context.stop(); } }, _callee); })), [oPlaceholder, onBeforeSend, onSend, text, textOnce]); var handleInputKeyDown = (0, _react.useCallback)(function (e) { if (!e.shiftKey && e.keyCode === 13) { send(); e.preventDefault(); } }, [send]); var handleTextChange = (0, _react.useCallback)(function (value, e) { setText(value); if (onChange) { onChange(value, e); } }, [onChange]); var handleSendBtnClick = (0, _react.useCallback)(function (e) { // 语音面板展开时,阻止本次点击后续的 mouseup 冒泡到 document // 否则 VoiceInput 内部的 ClickOutside(bubble 阶段监听 document.mouseup)会把面板收起 // 利用 capture 阶段 + once,抢在 ClickOutside 前处理,且不影响其他场景 if (isVoicePanelOpen) { document.addEventListener('mouseup', stopMouseUp, { capture: true, once: true }); } send(); e.preventDefault(); }, [send, isVoicePanelOpen]); var handleAccessoryToggle = (0, _react.useCallback)(function () { setAccessoryOpen(!isAccessoryOpen); }, [isAccessoryOpen]); var handleAccessoryBlur = (0, _react.useCallback)(function () { setTimeout(function () { setAccessoryOpen(false); setAccessoryContent(''); }); }, []); var handleToolbarClick = (0, _react.useCallback)(function (item, e) { if (onToolbarClick) { onToolbarClick(item, e); } if (item.render) { popoverTarget.current = e.currentTarget; setAccessoryContent(item.render); } }, [onToolbarClick]); var handlePopoverClose = (0, _react.useCallback)(function () { setAccessoryContent(''); }, []); /** 语音输入状态变化处理 - 联动输入框样式 */ var handleVoiceStatusChange = (0, _react.useCallback)(function (status) { setVoiceStatus(status); }, []); /** 关闭语音面板 */ var handleVoicePanelClose = (0, _react.useCallback)(function () { setVoicePanelOpen(false); setInputType('text'); setVoiceStatus(_VoiceInput.VoiceInputStatus.INITED); }, []); var isInputText = inputType === 'text'; var inputTypeIcon = isInputText ? 'mic' : 'keyboard'; var hasToolbar = toolbar.length > 0; var inputProps = _objectSpread(_objectSpread({}, inputOptions), {}, { value: text, inputRef: inputRef, placeholder: placeholder, onFocus: handleInputFocus, onBlur: handleInputBlur, onKeyDown: handleInputKeyDown, onChange: handleTextChange, onImageSend: onImageSend }); if (isWide) { return /*#__PURE__*/_react.default.createElement("div", { className: "Composer Composer--lg" }, hasToolbar && toolbar.map(function (item) { return /*#__PURE__*/_react.default.createElement(_ToolbarItem.ToolbarItem, { item: item, onClick: function onClick(e) { return handleToolbarClick(item, e); }, key: item.type }); }), accessoryContent && /*#__PURE__*/_react.default.createElement(_Popover.Popover, { active: !!accessoryContent, target: popoverTarget.current, onClose: handlePopoverClose }, accessoryContent), /*#__PURE__*/_react.default.createElement("div", { className: "Composer-inputWrap" }, /*#__PURE__*/_react.default.createElement(_ComposerInput.ComposerInput, (0, _extends2.default)({ invisible: false }, inputProps))), /*#__PURE__*/_react.default.createElement(_SendButton.SendButton, { onClick: handleSendBtnClick, disabled: !hasValue })); } return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", { className: "Composer", "data-has-value": hasValue, "data-has-text-once": !!textOnce, "data-voice-status": voiceStatus, "data-new-voice-input": !!enableNewVoiceInput }, recorder.canRecord && /*#__PURE__*/_react.default.createElement(_Action.Action, { className: "Composer-inputTypeBtn", "data-icon": inputTypeIcon, icon: inputTypeIcon, onClick: handleInputTypeChange, onMouseUp: function onMouseUp(e) { return e.stopPropagation(); }, "aria-label": isInputText ? '切换到语音输入' : '切换到键盘输入' }), /*#__PURE__*/_react.default.createElement("div", { className: "Composer-inputWrap" }, /*#__PURE__*/_react.default.createElement(_ComposerInput.ComposerInput, (0, _extends2.default)({ invisible: !isInputText && !enableNewVoiceInput }, inputProps)), !isInputText && !enableNewVoiceInput && /*#__PURE__*/_react.default.createElement(_Recorder.Recorder, recorder), enableNewVoiceInput && /*#__PURE__*/_react.default.createElement(_MarqueeBorder.MarqueeBorder, null)), !text && rightAction && /*#__PURE__*/_react.default.createElement(_Action.Action, rightAction), hasToolbar && /*#__PURE__*/_react.default.createElement(_Action.Action, { className: (0, _clsx.default)('Composer-toggleBtn', { active: isAccessoryOpen }), icon: "plus", onClick: handleAccessoryToggle, "aria-label": isAccessoryOpen ? '关闭工具栏' : '展开工具栏' }), hasValue && /*#__PURE__*/_react.default.createElement(_SendButton.SendButton, { onClick: handleSendBtnClick, disabled: !hasValue })), isAccessoryOpen && /*#__PURE__*/_react.default.createElement(_AccessoryWrap.AccessoryWrap, { onClickOutside: handleAccessoryBlur }, accessoryContent || /*#__PURE__*/_react.default.createElement(_Toolbar.Toolbar, { items: toolbar, onClick: handleToolbarClick })), enableNewVoiceInput && isVoicePanelOpen && /*#__PURE__*/_react.default.createElement(_VoiceInput.VoiceInput, { onStart: recorder.onStart, onEnd: recorder.onEnd, onCancel: recorder.onCancel, onStatusChange: handleVoiceStatusChange, onClose: handleVoicePanelClose })); });