tab.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import {
  2. EditorFileTab,
  3. TAB_TRANSFER_TYPE,
  4. } from '@/features/ide-react/context/tabs-context'
  5. import FileTreeIcon from '@/features/file-tree/components/file-tree-icon'
  6. import MaterialIcon from '@/shared/components/material-icon'
  7. import { debugConsole } from '@/utils/debugging'
  8. import classNames from 'classnames'
  9. import { throttle } from 'lodash'
  10. import {
  11. memo,
  12. useCallback,
  13. useEffect,
  14. useLayoutEffect,
  15. useMemo,
  16. useRef,
  17. useState,
  18. } from 'react'
  19. import { useTranslation } from 'react-i18next'
  20. type TabProps = {
  21. tab: EditorFileTab
  22. openTab: (id: string) => void
  23. closeTab: (id: string) => void
  24. makeTabPermanent: (id: string) => void
  25. isSelected: boolean
  26. onTabDrop: (
  27. sourceTabId: string,
  28. targetTabId: string,
  29. position: 'left' | 'right'
  30. ) => void
  31. }
  32. function getSideOfTargetFromEvent(
  33. element: Element,
  34. clientX: number
  35. ): 'left' | 'right' {
  36. const rect = element.getBoundingClientRect()
  37. if (rect.left + rect.width / 2 < clientX) {
  38. return 'right'
  39. } else {
  40. return 'left'
  41. }
  42. }
  43. export const Tab = memo(function Tab({
  44. tab,
  45. openTab,
  46. closeTab,
  47. makeTabPermanent,
  48. isSelected,
  49. onTabDrop,
  50. }: TabProps) {
  51. const { t } = useTranslation()
  52. const tabRef = useRef<HTMLDivElement>(null)
  53. const [dropTargetPosition, setDropTargetPosition] = useState<
  54. 'left' | 'right' | null
  55. >(null)
  56. const onDragStart = useCallback(
  57. (e: React.DragEvent) => {
  58. e.stopPropagation()
  59. e.dataTransfer.setData(TAB_TRANSFER_TYPE, tab.id)
  60. e.dataTransfer.effectAllowed = 'move'
  61. },
  62. [tab]
  63. )
  64. const throttledOnDragOver = useMemo(
  65. () =>
  66. throttle((element: Element, clientX: number) => {
  67. setDropTargetPosition(getSideOfTargetFromEvent(element, clientX))
  68. }, 50),
  69. []
  70. )
  71. const onDragOver = useCallback(
  72. (e: React.DragEvent) => {
  73. e.preventDefault()
  74. e.stopPropagation()
  75. e.dataTransfer.dropEffect = 'move'
  76. throttledOnDragOver(e.currentTarget, e.clientX)
  77. },
  78. [throttledOnDragOver]
  79. )
  80. const onDragLeave = useCallback(
  81. (e: React.DragEvent) => {
  82. e.stopPropagation()
  83. throttledOnDragOver.cancel()
  84. setDropTargetPosition(null)
  85. },
  86. [throttledOnDragOver]
  87. )
  88. const onDrop = useCallback(
  89. (e: React.DragEvent) => {
  90. throttledOnDragOver.cancel()
  91. setDropTargetPosition(null)
  92. e.preventDefault()
  93. e.stopPropagation()
  94. const draggedTabId = e.dataTransfer.getData(TAB_TRANSFER_TYPE)
  95. if (!draggedTabId) {
  96. debugConsole.warn('No dragged tab id found in dataTransfer')
  97. return
  98. }
  99. onTabDrop(
  100. draggedTabId,
  101. tab.id,
  102. getSideOfTargetFromEvent(e.currentTarget, e.clientX)
  103. )
  104. },
  105. [onTabDrop, tab, throttledOnDragOver]
  106. )
  107. const onDoubleClick = useCallback(() => {
  108. makeTabPermanent(tab.id)
  109. }, [makeTabPermanent, tab])
  110. const onClick = useCallback(
  111. (e: React.MouseEvent) => {
  112. if (e.button === 0) {
  113. openTab(tab.id)
  114. }
  115. },
  116. [openTab, tab]
  117. )
  118. const onKeyDown = useCallback(
  119. (e: React.KeyboardEvent) => {
  120. if (e.code === 'Enter') {
  121. openTab(tab.id)
  122. }
  123. },
  124. [openTab, tab]
  125. )
  126. const onCloseClick = useCallback(
  127. (e: React.MouseEvent) => {
  128. e.stopPropagation()
  129. closeTab(tab.id)
  130. },
  131. [closeTab, tab]
  132. )
  133. const onMouseUp = useCallback(
  134. (e: React.MouseEvent) => {
  135. if (e.button === 1) {
  136. e.stopPropagation()
  137. closeTab(tab.id)
  138. }
  139. },
  140. [closeTab, tab]
  141. )
  142. useLayoutEffect(() => {
  143. if (isSelected && tabRef.current) {
  144. tabRef.current.scrollIntoView({
  145. block: 'nearest',
  146. inline: 'nearest',
  147. })
  148. }
  149. }, [isSelected])
  150. useEffect(() => {
  151. if (isSelected && tab.lifetime === 'temporary') {
  152. const handler = () => {
  153. makeTabPermanent(tab.id)
  154. }
  155. document.body.addEventListener('keydown', handler)
  156. return () => {
  157. document.body.removeEventListener('keydown', handler)
  158. }
  159. }
  160. }, [isSelected, makeTabPermanent, tab])
  161. return (
  162. <div
  163. ref={tabRef}
  164. onDragStart={onDragStart}
  165. onDragOver={onDragOver}
  166. onDragLeave={onDragLeave}
  167. draggable
  168. onDrop={onDrop}
  169. onDoubleClick={onDoubleClick}
  170. role="tab"
  171. aria-selected={isSelected}
  172. key={tab.id}
  173. onClick={onClick}
  174. onMouseUp={onMouseUp}
  175. onKeyDown={onKeyDown}
  176. tabIndex={0}
  177. className={classNames('editor-file-tab', {
  178. 'tab-selected': isSelected,
  179. 'tab-drop-left': dropTargetPosition === 'left',
  180. 'tab-drop-right': dropTargetPosition === 'right',
  181. 'tab-temporary': tab.lifetime === 'temporary',
  182. })}
  183. >
  184. <span className="editor-file-tab-icon">
  185. <FileTreeIcon isLinkedFile={tab.isLinkedFile} name={tab.name} />
  186. </span>
  187. <div className="editor-file-tab-path">&lrm;{tab.displayPath}</div>
  188. <div className="editor-file-tab-action">
  189. <button
  190. onClick={onCloseClick}
  191. className="editor-file-tab-close-action"
  192. aria-label={t('close_tab')}
  193. >
  194. <MaterialIcon type="close" />
  195. </button>
  196. </div>
  197. </div>
  198. )
  199. })