| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- import {
- EditorFileTab,
- TAB_TRANSFER_TYPE,
- } from '@/features/ide-react/context/tabs-context'
- import FileTreeIcon from '@/features/file-tree/components/file-tree-icon'
- import MaterialIcon from '@/shared/components/material-icon'
- import { debugConsole } from '@/utils/debugging'
- import classNames from 'classnames'
- import { throttle } from 'lodash'
- import {
- memo,
- useCallback,
- useEffect,
- useLayoutEffect,
- useMemo,
- useRef,
- useState,
- } from 'react'
- import { useTranslation } from 'react-i18next'
- import {
- TAB_USER_EDIT_EVENT,
- tabsEvents,
- } from '@/features/source-editor/extensions/tabs-listener'
- type TabProps = {
- tab: EditorFileTab
- openTab: (id: string) => void
- closeTab: (id: string) => void
- canCloseTab: boolean
- makeTabPermanent: (id: string) => void
- openContextMenu: (
- coords: { top: number; left: number },
- tabId: string
- ) => void
- closeContextMenu: () => void
- isSelected: boolean
- onTabDrop: (
- sourceTabId: string,
- targetTabId: string,
- position: 'left' | 'right'
- ) => void
- }
- function getSideOfTargetFromEvent(
- element: Element,
- clientX: number
- ): 'left' | 'right' {
- const rect = element.getBoundingClientRect()
- if (rect.left + rect.width / 2 < clientX) {
- return 'right'
- } else {
- return 'left'
- }
- }
- export const Tab = memo(function Tab({
- tab,
- openTab,
- closeTab,
- canCloseTab,
- makeTabPermanent,
- openContextMenu,
- closeContextMenu,
- isSelected,
- onTabDrop,
- }: TabProps) {
- const { t } = useTranslation()
- const tabRef = useRef<HTMLDivElement>(null)
- const [dropTargetPosition, setDropTargetPosition] = useState<
- 'left' | 'right' | null
- >(null)
- const onDragStart = useCallback(
- (e: React.DragEvent) => {
- e.stopPropagation()
- e.dataTransfer.setData(TAB_TRANSFER_TYPE, tab.id)
- e.dataTransfer.effectAllowed = 'move'
- },
- [tab]
- )
- const throttledOnDragOver = useMemo(
- () =>
- throttle((element: Element, clientX: number) => {
- setDropTargetPosition(getSideOfTargetFromEvent(element, clientX))
- }, 50),
- []
- )
- const onDragOver = useCallback(
- (e: React.DragEvent) => {
- e.preventDefault()
- e.stopPropagation()
- e.dataTransfer.dropEffect = 'move'
- throttledOnDragOver(e.currentTarget, e.clientX)
- },
- [throttledOnDragOver]
- )
- const onDragLeave = useCallback(
- (e: React.DragEvent) => {
- e.stopPropagation()
- if (e.currentTarget.contains(e.relatedTarget as Node | null)) {
- return
- }
- throttledOnDragOver.cancel()
- setDropTargetPosition(null)
- },
- [throttledOnDragOver]
- )
- const onDrop = useCallback(
- (e: React.DragEvent) => {
- throttledOnDragOver.cancel()
- setDropTargetPosition(null)
- e.preventDefault()
- e.stopPropagation()
- const draggedTabId = e.dataTransfer.getData(TAB_TRANSFER_TYPE)
- if (!draggedTabId) {
- debugConsole.warn('No dragged tab id found in dataTransfer')
- return
- }
- onTabDrop(
- draggedTabId,
- tab.id,
- getSideOfTargetFromEvent(e.currentTarget, e.clientX)
- )
- },
- [onTabDrop, tab, throttledOnDragOver]
- )
- const onDoubleClick = useCallback(() => {
- makeTabPermanent(tab.id)
- }, [makeTabPermanent, tab])
- const onClick = useCallback(
- (e: React.MouseEvent) => {
- if (e.button === 0) {
- openTab(tab.id)
- }
- },
- [openTab, tab]
- )
- const onKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- if (e.code === 'Enter') {
- openTab(tab.id)
- }
- },
- [openTab, tab]
- )
- const onCloseClick = useCallback(
- (e: React.MouseEvent) => {
- e.stopPropagation()
- closeTab(tab.id)
- },
- [closeTab, tab]
- )
- const onMouseUp = useCallback(
- (e: React.MouseEvent) => {
- if (e.button === 1) {
- e.stopPropagation()
- closeTab(tab.id)
- }
- },
- [closeTab, tab]
- )
- const onContextMenu = useCallback(
- (e: React.MouseEvent) => {
- if (e.shiftKey) {
- closeContextMenu()
- return
- }
- e.preventDefault()
- openContextMenu({ top: e.pageY, left: e.pageX }, tab.id)
- },
- [openContextMenu, closeContextMenu, tab]
- )
- useLayoutEffect(() => {
- if (isSelected && tabRef.current) {
- tabRef.current.scrollIntoView({
- block: 'nearest',
- inline: 'nearest',
- })
- }
- }, [isSelected])
- useEffect(() => {
- if (isSelected && tab.lifetime === 'temporary') {
- const handler = () => {
- makeTabPermanent(tab.id)
- }
- tabsEvents.addEventListener(TAB_USER_EDIT_EVENT, handler)
- return () => {
- tabsEvents.removeEventListener(TAB_USER_EDIT_EVENT, handler)
- }
- }
- }, [isSelected, makeTabPermanent, tab])
- return (
- <div
- ref={tabRef}
- onDragStart={onDragStart}
- onDragOver={onDragOver}
- onDragLeave={onDragLeave}
- draggable
- onDrop={onDrop}
- onDoubleClick={onDoubleClick}
- role="tab"
- aria-selected={isSelected}
- key={tab.id}
- onClick={onClick}
- onMouseUp={onMouseUp}
- onKeyDown={onKeyDown}
- onContextMenu={onContextMenu}
- data-tab-id={tab.id}
- tabIndex={0}
- className={classNames('editor-file-tab', {
- 'tab-selected': isSelected,
- 'tab-drop-left': dropTargetPosition === 'left',
- 'tab-drop-right': dropTargetPosition === 'right',
- 'tab-temporary': tab.lifetime === 'temporary',
- })}
- >
- <div className="editor-file-tab-content">
- <span className="editor-file-tab-icon">
- <FileTreeIcon isLinkedFile={tab.isLinkedFile} name={tab.name} />
- </span>
- <div className="editor-file-tab-path">‎{tab.displayPath}</div>
- <div className="editor-file-tab-action">
- <button
- onClick={onCloseClick}
- disabled={!canCloseTab}
- className="editor-file-tab-close-action"
- aria-label={t('close')}
- >
- <MaterialIcon type="close" />
- </button>
- </div>
- </div>
- </div>
- )
- })
|