feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createContext } from 'react';
import { type ActionController, type ActionSize } from '../types';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const ActionBarContext = createContext<{
controller: ActionController;
size: ActionSize;
}>({
controller: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
hideActionBar: () => {},
// 重新定位
// eslint-disable-next-line @typescript-eslint/no-empty-function
rePosition: () => {},
},
size: 'small',
});

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useActionBarPreference } from './use-action-bar-perference';

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext } from 'react';
import { type ActionController, type ActionSize } from '../types';
import { ActionBarContext } from '../context';
interface ActionBarPreference {
size: ActionSize;
controller: ActionController;
}
export const useActionBarPreference = (): ActionBarPreference => {
const { size, controller } = useContext(ActionBarContext);
return { size, controller };
};

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ActionBar } from './ui-component';
export { ActionBarContext } from './context';
export { useActionBarPreference } from './hooks/use-action-bar-perference';
export type { ActionController, ActionSize } from './types';
export type { SelectionInfo } from '../types';

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ButtonProps } from '@coze-arch/coze-design';
export interface ActionController {
hideActionBar: () => void;
rePosition: (position?: 'topLeft' | 'bottomRight') => void;
}
export type ActionSize = ButtonProps['size'];

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type PropsWithChildren,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import cls from 'classnames';
import {
PositionMirror,
useEditor,
useInjector,
} from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { Popover } from '@coze-arch/coze-design';
import { drawSelection, EditorView } from '@codemirror/view';
import { type SelectionInfo } from '../types';
import { ThemeExtension } from '../theme';
import { useReadonly } from '../shared/hooks/use-editor-readonly';
import { type ActionController } from './types';
import { ActionBarContext } from './context';
interface ActionBarProps {
className?: string;
size?: 'default' | 'small' | 'large';
visible?: boolean;
onVisibleChange?: (visible: boolean) => void;
trigger?: 'custom' | 'selection';
}
export const ActionBar: React.FC<PropsWithChildren<ActionBarProps>> = props => {
const {
className,
size = 'small',
children,
visible,
onVisibleChange,
trigger = 'selection',
} = props;
const [internalVisible, setInternalVisible] = useState(false);
const [reposKey, setReposKey] = useState('');
const [popoverPosition, setPopoverPosition] = useState(-1);
const containerRef = useRef<HTMLDivElement>(null);
const isReadOnly = useReadonly();
const [selection, setSelection] = useState<SelectionInfo>({
from: 0,
to: 0,
anchor: 0,
head: 0,
});
const editor = useEditor<EditorAPI>();
const injector = useInjector();
const [position, setPosition] = useState<
'topLeft' | 'bottomRight' | undefined
>();
useEffect(() => {
setPosition(
selection?.head > selection?.anchor ? 'bottomRight' : 'topLeft',
);
}, [selection]);
const controller: ActionController = {
hideActionBar: () => {
onVisibleChange?.(false);
setInternalVisible(false);
},
rePosition: (newPosition?: 'topLeft' | 'bottomRight') => {
setReposKey(String(Math.random()));
newPosition && setPosition(newPosition);
},
};
useLayoutEffect(() => injector.inject([drawSelection()]), [injector]);
useEffect(() => {
if (!editor) {
return;
}
function handleMousedown() {
onVisibleChange?.(false);
setInternalVisible(false);
setPopoverPosition(-1);
}
function handleMouseup(e: MouseEvent) {
if (containerRef.current?.contains(e.target as Node)) {
return;
}
const selectionRange = editor.getSelection();
setSelection(selectionRange);
if (!selectionRange) {
onVisibleChange?.(false);
setPopoverPosition(-1);
setInternalVisible(false);
return;
}
const isSelection = selectionRange.from !== selectionRange.to;
setSelection(selectionRange);
onVisibleChange?.(isSelection);
setInternalVisible(isSelection);
setPopoverPosition(selectionRange.head);
}
function handleSelectionChange() {
onVisibleChange?.(false);
setPopoverPosition(-1);
setInternalVisible(false);
}
// function handleBlur() {
// onVisibleChange?.(false);
// setInternalVisible(false);
// editor.$view.dispatch({
// selection: { anchor: editor.$view.state.selection.main.head },
// });
// }
editor.$on('mousedown', handleMousedown);
// 不使用 editor.$on 监听 mouseup 事件,因为鼠标可能不在编辑器内
document.addEventListener('mouseup', handleMouseup);
editor.$on('selectionChange', handleSelectionChange);
// editor.$on('blur', handleBlur);
return () => {
editor.$off('mousedown', handleMousedown);
document.removeEventListener('mouseup', handleMouseup);
editor.$off('selectionChange', handleSelectionChange);
// editor.$off('blur', handleBlur);
};
}, [editor]);
if (isReadOnly) {
return null;
}
return (
<>
<Popover
rePosKey={reposKey}
visible={trigger === 'custom' ? visible : internalVisible}
trigger="custom"
position={position}
autoAdjustOverflow
className="rounded"
content={
<ActionBarContext.Provider value={{ controller, size }}>
<div className={cls('flex gap-1', className)} ref={containerRef}>
{children}
</div>
</ActionBarContext.Provider>
}
>
<PositionMirror
position={popoverPosition}
onChange={() => setReposKey(String(Math.random()))}
/>
</Popover>
<ThemeExtension
themes={[
EditorView.theme({
'.cm-selectionBackground': {
backgroundColor: 'rgba(148, 152, 247, 0.44)',
},
}),
]}
/>
</>
);
};