tab.tsx 4.4 KB

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