@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
429 lines (414 loc) • 20.4 kB
JavaScript
'use client';
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
var _excluded = ["autoFocus", "disabled", "form", "classNames", "styles", "ignoreTabKey", "insertSpaces", "maxLength", "minLength", "onBlur", "onClick", "onFocus", "onKeyDown", "onKeyUp", "onValueChange", "placeholder", "readOnly", "required", "style", "className", "tabSize", "textareaId", "value", "language", "fontSize", "variant"];
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) { _defineProperty(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; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Flexbox } from 'react-layout-kit';
import { useStyles } from "./style";
import { isMacLike } from "../utils/platform";
import SyntaxHighlighter from "../Highlighter/SyntaxHighlighter";
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
var KEYCODE_Y = 89;
var KEYCODE_Z = 90;
var KEYCODE_M = 77;
var KEYCODE_PARENS = 57;
var KEYCODE_BRACKETS = 219;
var KEYCODE_QUOTE = 222;
var KEYCODE_BACK_QUOTE = 192;
var HISTORY_LIMIT = 100;
var HISTORY_TIME_GAP = 3000;
var isWindows = typeof window !== 'undefined' && 'navigator' in window && /win/i.test(navigator.platform);
var getLines = function getLines(text, position) {
return text.slice(0, Math.max(0, position)).split('\n');
};
var Editor = /*#__PURE__*/forwardRef(function (_ref, ref) {
var autoFocus = _ref.autoFocus,
disabled = _ref.disabled,
form = _ref.form,
_ref$classNames = _ref.classNames,
classNames = _ref$classNames === void 0 ? {} : _ref$classNames,
_ref$styles = _ref.styles,
s = _ref$styles === void 0 ? {} : _ref$styles,
_ref$ignoreTabKey = _ref.ignoreTabKey,
ignoreTabKey = _ref$ignoreTabKey === void 0 ? false : _ref$ignoreTabKey,
_ref$insertSpaces = _ref.insertSpaces,
insertSpaces = _ref$insertSpaces === void 0 ? true : _ref$insertSpaces,
maxLength = _ref.maxLength,
minLength = _ref.minLength,
onBlur = _ref.onBlur,
onClick = _ref.onClick,
onFocus = _ref.onFocus,
onKeyDown = _ref.onKeyDown,
onKeyUp = _ref.onKeyUp,
onValueChange = _ref.onValueChange,
placeholder = _ref.placeholder,
readOnly = _ref.readOnly,
required = _ref.required,
style = _ref.style,
className = _ref.className,
_ref$tabSize = _ref.tabSize,
tabSize = _ref$tabSize === void 0 ? 2 : _ref$tabSize,
textareaId = _ref.textareaId,
value = _ref.value,
language = _ref.language,
_ref$fontSize = _ref.fontSize,
fontSize = _ref$fontSize === void 0 ? 12 : _ref$fontSize,
_ref$variant = _ref.variant,
variant = _ref$variant === void 0 ? 'ghost' : _ref$variant,
rest = _objectWithoutProperties(_ref, _excluded);
var _useStyles = useStyles({
fontSize: fontSize,
variant: variant
}),
styles = _useStyles.styles,
cx = _useStyles.cx;
var historyRef = useRef({
offset: -1,
stack: []
});
var inputRef = useRef(null);
var _useState = useState(true),
_useState2 = _slicedToArray(_useState, 2),
capture = _useState2[0],
setCapture = _useState2[1];
var recordChange = useCallback(function (record) {
var overwrite = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var _historyRef$current = historyRef.current,
stack = _historyRef$current.stack,
offset = _historyRef$current.offset;
if (stack.length > 0 && offset > -1) {
// When something updates, drop the redo operations
historyRef.current.stack = stack.slice(0, offset + 1);
// Limit the number of operations to 100
var count = historyRef.current.stack.length;
if (count > HISTORY_LIMIT) {
var extras = count - HISTORY_LIMIT;
historyRef.current.stack = stack.slice(extras, count);
historyRef.current.offset = Math.max(historyRef.current.offset - extras, 0);
}
}
var timestamp = Date.now();
if (overwrite) {
var last = historyRef.current.stack[historyRef.current.offset];
if (last && timestamp - last.timestamp < HISTORY_TIME_GAP) {
var _getLines$pop, _getLines$pop2, _current$;
// A previous entry exists and was in short interval
// Match the last word in the line
var re = /[^\da-z]([\da-z]+)$/i;
// Get the previous line
var previous = (_getLines$pop = getLines(last.value, last.selectionStart).pop()) === null || _getLines$pop === void 0 ? void 0 : _getLines$pop.match(re);
// Get the current line
var current = (_getLines$pop2 = getLines(record.value, record.selectionStart).pop()) === null || _getLines$pop2 === void 0 ? void 0 : _getLines$pop2.match(re);
if (previous !== null && previous !== void 0 && previous[1] && current !== null && current !== void 0 && (_current$ = current[1]) !== null && _current$ !== void 0 && _current$.startsWith(previous[1])) {
// The last word of the previous line and current line match
// Overwrite previous entry so that undo will remove whole word
historyRef.current.stack[historyRef.current.offset] = _objectSpread(_objectSpread({}, record), {}, {
timestamp: timestamp
});
return;
}
}
}
// Add the new operation to the stack
historyRef.current.stack.push(_objectSpread(_objectSpread({}, record), {}, {
timestamp: timestamp
}));
historyRef.current.offset++;
}, []);
var recordCurrentState = useCallback(function () {
var input = inputRef.current;
if (!input) return;
// Save current state of the input
var value = input.value,
selectionStart = input.selectionStart,
selectionEnd = input.selectionEnd;
recordChange({
selectionEnd: selectionEnd,
selectionStart: selectionStart,
value: value
});
}, [recordChange]);
var updateInput = function updateInput(record) {
var input = inputRef.current;
if (!input) return;
// Update values and selection state
input.value = record.value;
input.selectionStart = record.selectionStart;
input.selectionEnd = record.selectionEnd;
onValueChange === null || onValueChange === void 0 || onValueChange(record.value);
};
var applyEdits = function applyEdits(record) {
// Save last selection state
var input = inputRef.current;
var last = historyRef.current.stack[historyRef.current.offset];
if (last && input) {
historyRef.current.stack[historyRef.current.offset] = _objectSpread(_objectSpread({}, last), {}, {
selectionEnd: input.selectionEnd,
selectionStart: input.selectionStart
});
}
// Save the changes
recordChange(record);
updateInput(record);
};
var undoEdit = function undoEdit() {
var _historyRef$current2 = historyRef.current,
stack = _historyRef$current2.stack,
offset = _historyRef$current2.offset;
// Get the previous edit
var record = stack[offset - 1];
if (record) {
// Apply the changes and update the offset
updateInput(record);
historyRef.current.offset = Math.max(offset - 1, 0);
}
};
var redoEdit = function redoEdit() {
var _historyRef$current3 = historyRef.current,
stack = _historyRef$current3.stack,
offset = _historyRef$current3.offset;
// Get the next edit
var record = stack[offset + 1];
if (record) {
// Apply the changes and update the offset
updateInput(record);
historyRef.current.offset = Math.min(offset + 1, stack.length - 1);
}
};
var handleKeyDown = function handleKeyDown(e) {
if (onKeyDown) {
onKeyDown(e);
if (e.defaultPrevented) {
return;
}
}
if (e.key === 'Escape') {
e.currentTarget.blur();
}
var _e$currentTarget = e.currentTarget,
value = _e$currentTarget.value,
selectionStart = _e$currentTarget.selectionStart,
selectionEnd = _e$currentTarget.selectionEnd;
var tabCharacter = (insertSpaces ? ' ' : '\t').repeat(tabSize);
if (e.key === 'Tab' && !ignoreTabKey && capture) {
// Prevent focus change
e.preventDefault();
if (e.shiftKey) {
// Unindent selected lines
var linesBeforeCaret = getLines(value, selectionStart);
var startLine = linesBeforeCaret.length - 1;
var endLine = getLines(value, selectionEnd).length - 1;
var nextValue = value.split('\n').map(function (line, i) {
if (i >= startLine && i <= endLine && line.startsWith(tabCharacter)) {
return line.slice(tabCharacter.length);
}
return line;
}).join('\n');
if (value !== nextValue) {
var startLineText = linesBeforeCaret[startLine];
applyEdits({
// Move the end cursor by total number of characters removed
selectionEnd: selectionEnd - (value.length - nextValue.length),
// Move the start cursor if first line in selection was modified
// It was modified only if it started with a tab
selectionStart: startLineText !== null && startLineText !== void 0 && startLineText.startsWith(tabCharacter) ? selectionStart - tabCharacter.length : selectionStart,
value: nextValue
});
}
} else if (selectionStart === selectionEnd) {
var updatedSelection = selectionStart + tabCharacter.length;
applyEdits({
selectionEnd: updatedSelection,
// Update caret position
selectionStart: updatedSelection,
// Insert tab character at caret
value: value.slice(0, Math.max(0, selectionStart)) + tabCharacter + value.slice(Math.max(0, selectionEnd))
});
} else {
// Indent selected lines
var _linesBeforeCaret = getLines(value, selectionStart);
var _startLine = _linesBeforeCaret.length - 1;
var _endLine = getLines(value, selectionEnd).length - 1;
var _startLineText = _linesBeforeCaret[_startLine];
applyEdits({
// Move the end cursor by total number of characters added
selectionEnd: selectionEnd + tabCharacter.length * (_endLine - _startLine + 1),
// Move the start cursor by number of characters added in first line of selection
// Don't move it if it there was no text before cursor
selectionStart: _startLineText && /\S/.test(_startLineText) ? selectionStart + tabCharacter.length : selectionStart,
value: value.split('\n').map(function (line, i) {
if (i >= _startLine && i <= _endLine) {
return tabCharacter + line;
}
return line;
}).join('\n')
});
}
} else if (e.key === 'Backspace') {
var hasSelection = selectionStart !== selectionEnd;
var textBeforeCaret = value.slice(0, Math.max(0, selectionStart));
if (textBeforeCaret.endsWith(tabCharacter) && !hasSelection) {
// Prevent default delete behaviour
e.preventDefault();
var _updatedSelection = selectionStart - tabCharacter.length;
applyEdits({
selectionEnd: _updatedSelection,
// Update caret position
selectionStart: _updatedSelection,
// Remove tab character at caret
value: value.slice(0, Math.max(0, selectionStart - tabCharacter.length)) + value.slice(Math.max(0, selectionEnd))
});
}
} else if (e.key === 'Enter') {
// Ignore selections
if (selectionStart === selectionEnd) {
// Get the current line
var line = getLines(value, selectionStart).pop();
var matches = line === null || line === void 0 ? void 0 : line.match(/^\s+/);
if (matches !== null && matches !== void 0 && matches[0]) {
e.preventDefault();
// Preserve indentation on inserting a new line
var indent = '\n' + matches[0];
var _updatedSelection2 = selectionStart + indent.length;
applyEdits({
selectionEnd: _updatedSelection2,
// Update caret position
selectionStart: _updatedSelection2,
// Insert indentation character at caret
value: value.slice(0, Math.max(0, selectionStart)) + indent + value.slice(Math.max(0, selectionEnd))
});
}
}
} else if (e.keyCode === KEYCODE_PARENS || e.keyCode === KEYCODE_BRACKETS || e.keyCode === KEYCODE_QUOTE || e.keyCode === KEYCODE_BACK_QUOTE) {
var chars;
if (e.keyCode === KEYCODE_PARENS && e.shiftKey) {
chars = ['(', ')'];
} else if (e.keyCode === KEYCODE_BRACKETS) {
chars = e.shiftKey ? ['{', '}'] : ['[', ']'];
} else if (e.keyCode === KEYCODE_QUOTE) {
chars = e.shiftKey ? ['"', '"'] : ["'", "'"];
} else if (e.keyCode === KEYCODE_BACK_QUOTE && !e.shiftKey) {
chars = ['`', '`'];
}
// If text is selected, wrap them in the characters
if (selectionStart !== selectionEnd && chars) {
e.preventDefault();
applyEdits({
selectionEnd: selectionEnd + 2,
// Update caret position
selectionStart: selectionStart,
value: value.slice(0, Math.max(0, selectionStart)) + chars[0] +
// eslint-disable-next-line unicorn/prefer-string-slice
value.substring(selectionStart, selectionEnd) + chars[1] + value.slice(Math.max(0, selectionEnd))
});
}
} else if ((isMacLike ?
// Trigger undo with ⌘+Z on Mac
e.metaKey && e.keyCode === KEYCODE_Z :
// Trigger undo with Ctrl+Z on other platforms
e.ctrlKey && e.keyCode === KEYCODE_Z) && !e.shiftKey && !e.altKey) {
e.preventDefault();
undoEdit();
} else if ((isMacLike ?
// Trigger redo with ⌘+Shift+Z on Mac
e.metaKey && e.keyCode === KEYCODE_Z && e.shiftKey : isWindows ?
// Trigger redo with Ctrl+Y on Windows
e.ctrlKey && e.keyCode === KEYCODE_Y :
// Trigger redo with Ctrl+Shift+Z on other platforms
e.ctrlKey && e.keyCode === KEYCODE_Z && e.shiftKey) && !e.altKey) {
e.preventDefault();
redoEdit();
} else if (e.keyCode === KEYCODE_M && e.ctrlKey && (isMacLike ? e.shiftKey : true)) {
e.preventDefault();
// Toggle capturing tab key so users can focus away
setCapture(function (prev) {
return !prev;
});
}
};
var handleChange = function handleChange(e) {
var _e$currentTarget2 = e.currentTarget,
value = _e$currentTarget2.value,
selectionStart = _e$currentTarget2.selectionStart,
selectionEnd = _e$currentTarget2.selectionEnd;
recordChange({
selectionEnd: selectionEnd,
selectionStart: selectionStart,
value: value
}, true);
onValueChange(value);
};
useEffect(function () {
recordCurrentState();
}, [recordCurrentState]);
// @ts-ignore
useImperativeHandle(ref, function () {
return {
get session() {
return {
history: historyRef.current
};
},
set session(session) {
historyRef.current = session.history;
}
};
}, []);
return /*#__PURE__*/_jsx(Flexbox, _objectSpread(_objectSpread({
className: cx(styles.container, className),
style: style
}, rest), {}, {
children: /*#__PURE__*/_jsxs("div", {
className: styles.editor,
children: [/*#__PURE__*/_jsx(SyntaxHighlighter, {
className: cx(styles.highlight, classNames === null || classNames === void 0 ? void 0 : classNames.highlight),
language: language,
style: s.highlight,
children: value
}), /*#__PURE__*/_jsx("textarea", {
autoCapitalize: "off",
autoComplete: "off",
autoCorrect: "off",
autoFocus: autoFocus,
className: cx(styles.textarea, classNames === null || classNames === void 0 ? void 0 : classNames.textarea),
"data-gramm": false,
disabled: disabled,
form: form,
id: textareaId,
maxLength: maxLength,
minLength: minLength,
onBlur: onBlur,
onChange: handleChange,
onClick: onClick,
onFocus: onFocus,
onKeyDown: handleKeyDown,
onKeyUp: onKeyUp,
placeholder: placeholder,
readOnly: readOnly
// @ts-ignore
,
ref: function ref(c) {
return inputRef.current = c;
},
required: required,
spellCheck: false,
style: _objectSpread({}, s === null || s === void 0 ? void 0 : s.textarea),
value: value
})]
})
}));
});
export default Editor;