@chatui/core
Version:
The React library for Chatbot UI
439 lines (434 loc) • 18.5 kB
JavaScript
;
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
}));
});