├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── README.md ├── core ├── README.md ├── package.json ├── src │ ├── Tab.tsx │ ├── Tabs.tsx │ ├── hooks.ts │ ├── index.tsx │ └── store.tsx └── tsconfig.json ├── lerna.json ├── package.json ├── renovate.json ├── tsconfig.json └── www ├── .kktrc.ts ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── index.tsx └── react-app-env.d.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | registry-url: 'https://registry.npmjs.org' 17 | 18 | - run: npm install 19 | - run: npm run build 20 | - run: npm run doc 21 | 22 | - name: Generate Contributors Images 23 | uses: jaywcjlove/github-action-contributors@main 24 | with: 25 | filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\]) 26 | output: www/build/CONTRIBUTORS.svg 27 | avatarSize: 32 28 | 29 | - name: Create Tag 30 | id: create_tag 31 | uses: jaywcjlove/create-tag-action@main 32 | with: 33 | package-path: ./core/package.json 34 | 35 | - name: get tag version 36 | id: tag_version 37 | uses: jaywcjlove/changelog-generator@main 38 | 39 | - name: Deploy 40 | uses: peaceiris/actions-gh-pages@v3 41 | with: 42 | commit_message: ${{steps.tag_version.outputs.tag}} ${{ github.event.head_commit.message }} 43 | github_token: ${{ secrets.GITHUB_TOKEN }} 44 | publish_dir: www/build 45 | 46 | - name: Generate Changelog 47 | id: changelog 48 | uses: jaywcjlove/changelog-generator@main 49 | with: 50 | head-ref: ${{steps.create_tag.outputs.version}} 51 | filter-author: (renovate-bot|Renovate Bot) 52 | filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}' 53 | 54 | - name: Create Release 55 | uses: ncipollo/release-action@v1 56 | if: steps.create_tag.outputs.successful 57 | with: 58 | token: ${{ secrets.GITHUB_TOKEN }} 59 | name: ${{ steps.create_tag.outputs.version }} 60 | tag: ${{ steps.create_tag.outputs.version }} 61 | body: | 62 | [![](https://img.shields.io/badge/Open%20in-unpkg-blue)](https://uiwjs.github.io/npm-unpkg/#/pkg/@uiw/react-tabs-draggable@${{steps.changelog.outputs.version}}/file/README.md) 63 | 64 | Documentation ${{ steps.changelog.outputs.tag }}: https://raw.githack.com/uiwjs/react-tabs-draggable/${{ steps.changelog.outputs.gh-pages-short-hash }}/index.html 65 | Comparing Changes: ${{ steps.changelog.outputs.compareurl }} 66 | 67 | ```shell 68 | npm i @uiw/react-tabs-draggable@${{steps.create_tag.outputs.versionNumber}} 69 | ``` 70 | 71 | ${{ steps.changelog.outputs.changelog }} 72 | 73 | - run: npm publish 74 | name: 📦 @uiw/react-tabs-draggable publish to NPM 75 | working-directory: core 76 | continue-on-error: true 77 | env: 78 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 79 | 80 | outputs: 81 | successful: ${{steps.create_tag.outputs.successful }} 82 | 83 | # github-package: 84 | # runs-on: ubuntu-latest 85 | # needs: build 86 | # steps: 87 | # - uses: actions/checkout@v3 88 | # - uses: actions/setup-node@v3 89 | # with: 90 | # node-version: 16 91 | # registry-url: https://npm.pkg.github.com 92 | # scope: '@uiwjs' 93 | 94 | # - run: npm install 95 | # - run: npm run build 96 | 97 | # - name: Modify package name 98 | # working-directory: core 99 | # shell: bash 100 | # run: | 101 | # node -e 'var pkg = require("./package.json"); pkg.name="@uiwjs/react-tabs-draggable"; require("fs").writeFileSync("./package.json", JSON.stringify(pkg, null, 2))' 102 | 103 | # - run: npm publish 104 | # name: 📦 @uiwjs/react-tabs-draggable publish to NPM 105 | # working-directory: core 106 | # continue-on-error: true 107 | # env: 108 | # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 109 | 110 | # npm-package: 111 | # runs-on: ubuntu-latest 112 | # needs: build 113 | # steps: 114 | # - uses: actions/checkout@v3 115 | # - uses: actions/setup-node@v3 116 | # with: 117 | # node-version: 16 118 | # registry-url: 'https://registry.npmjs.org' 119 | 120 | # - run: npm install 121 | # - run: npm run build 122 | 123 | # - run: npm publish 124 | # name: 📦 @uiw/react-tabs-draggable publish to NPM 125 | # working-directory: core 126 | # continue-on-error: true 127 | # env: 128 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | cjs 4 | esm 5 | node_modules 6 | coverage 7 | npm-debug.log* 8 | package-lock.json 9 | 10 | .eslintcache 11 | .DS_Store 12 | .cache 13 | .rdoc-dist 14 | 15 | *.log 16 | *.bak 17 | *.tem 18 | *.temp 19 | #.swp 20 | *.*~ 21 | ~*.* 22 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx,html,less,md,json}": [ 3 | "prettier --write" 4 | ] 5 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | coverage 3 | dist 4 | build 5 | cjs 6 | esm 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | }, 10 | { 11 | "files": ".lintstagedrc", 12 | "options": { "parser": "json" } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | core/README.md -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # react-tabs-draggable 2 | 3 | [![CI](https://github.com/uiwjs/react-tabs-draggable/actions/workflows/ci.yml/badge.svg)](https://github.com/uiwjs/react-tabs-draggable/actions/workflows/ci.yml) 4 | [![Open in unpkg](https://img.shields.io/badge/Open%20in-unpkg-blue)](https://uiwjs.github.io/npm-unpkg/#/pkg/@uiw/react-tabs-draggable/file/README.md) 5 | [![npm version](https://img.shields.io/npm/v/@uiw/react-tabs-draggable.svg)](https://www.npmjs.com/package/@uiw/react-tabs-draggable) 6 | 7 | Draggable tabs for React. Demo Preview: [@uiwjs.github.io/react-tabs-draggable](https://uiwjs.github.io/react-tabs-draggable/) 8 | 9 | ## Install 10 | 11 | **Not dependent on uiw.** 12 | 13 | ```bash 14 | npm install @uiw/react-tabs-draggable --save 15 | ``` 16 | 17 | ## Base Usage 18 | 19 | ```jsx mdx:preview 20 | import React, { useState } from 'react'; 21 | import Tabs, { Tab } from '@uiw/react-tabs-draggable'; 22 | 23 | function App() { 24 | const [activeKey, setActiveKey] = useState('tab-1'); 25 | return ( 26 |
27 | setActiveKey(id)}> 28 | {activeKey === 'tab-1' && '▶'}Google 29 | {activeKey === 'tab-2' && '▶'}MicroSoft 30 | {activeKey === 'tab-3' && '▶'}Baidu 31 | {activeKey === 'tab-4' && '▶'}Taobao 32 | {activeKey === 'tab-5' && '▶'}JD 33 | 34 |
{activeKey}
35 |
36 | ); 37 | } 38 | export default App; 39 | ``` 40 | 41 | ## Disable Draggable 42 | 43 | The first tab is disabled. 44 | 45 | ```jsx mdx:preview 46 | import React, { useState } from 'react'; 47 | import Tabs, { Tab } from '@uiw/react-tabs-draggable'; 48 | import styled from 'styled-components'; 49 | 50 | const TabItem = styled(Tab)` 51 | background-color: #b9b9b9; 52 | padding: 3px 7px; 53 | border-radius: 5px 5px 0 0; 54 | &.w-active { 55 | color: #fff; 56 | background-color: #333; 57 | } 58 | `; 59 | 60 | const Content = styled.div` 61 | border-top: 1px solid #333; 62 | `; 63 | 64 | function App() { 65 | const [activeKey, setActiveKey] = useState('tab-0-1'); 66 | return ( 67 |
68 | setActiveKey(id)}> 69 | Google 70 | MicroSoft 71 | Baidu 72 | Taobao 73 | JD 74 | 75 | {activeKey} 76 |
77 | ); 78 | } 79 | export default App; 80 | ``` 81 | 82 | ## Add & Close tab 83 | 84 | The first tab is disabled. 85 | 86 | ```jsx mdx:preview 87 | import React, { Fragment, useState, useCallback } from 'react'; 88 | import Tabs, { Tab, useDataContext } from '@uiw/react-tabs-draggable'; 89 | import styled from 'styled-components'; 90 | 91 | const TabWarp = styled(Tabs)` 92 | max-width: 450px; 93 | border-bottom: 1px solid #333; 94 | margin-bottom: -2px; 95 | &:hover::-webkit-scrollbar { 96 | height: 0px; 97 | background-color: red; 98 | } 99 | &:hover::-webkit-scrollbar-track { 100 | background-color: #333; 101 | } 102 | &:hover::-webkit-scrollbar-thumb { 103 | background-color: green; 104 | } 105 | `; 106 | 107 | const TabItem = styled(Tab)` 108 | background-color: #b9b9b9; 109 | padding: 3px 7px; 110 | border-radius: 5px 5px 0 0; 111 | user-select: none; 112 | &.w-active { 113 | color: #fff; 114 | background-color: #333; 115 | } 116 | `; 117 | 118 | function insertAndShift(arr, from, to) { 119 | let cutOut = arr.splice(from, 1)[0]; 120 | arr.splice(to, 0, cutOut); 121 | return arr; 122 | } 123 | 124 | let count = 9; 125 | 126 | function App() { 127 | const [data, setData] = useState([ 128 | { id: 'tab-4-1', children: 'Google' }, 129 | { id: 'tab-4-2', children: 'MicroSoft' }, 130 | { id: 'tab-4-3', children: 'Baidu' }, 131 | { id: 'tab-4-4', children: 'Taobao' }, 132 | { id: 'tab-4-5', children: 'JD' }, 133 | { id: 'tab-4-6', children: 'Apple' }, 134 | { id: 'tab-4-7', children: 'Bing' }, 135 | { id: 'tab-4-8', children: 'Gmail' }, 136 | { id: 'tab-4-9', children: 'Gitter' }, 137 | ]); 138 | const [test, setTest] = useState(1); 139 | const [activeKey, setActiveKey] = useState(''); 140 | 141 | const tabClick = (id, evn) => { 142 | evn.stopPropagation(); 143 | setActiveKey(id); 144 | setTest(test + 1); 145 | }; 146 | const closeHandle = (item, evn) => { 147 | evn.stopPropagation(); 148 | const idx = data.findIndex((m) => m.id === item.id); 149 | 150 | let active = ''; 151 | if (idx > -1 && activeKey) { 152 | active = data[idx - 1] ? data[idx - 1].id : data[idx].id; 153 | setActiveKey(active || ''); 154 | } 155 | setData(data.filter((m) => m.id !== item.id)); 156 | }; 157 | const addHandle = () => { 158 | ++count; 159 | const newData = [...data, { id: `tab-3-${count}`, children: `New Tab ${count}` }]; 160 | setData(newData); 161 | }; 162 | const tabDrop = (id, index) => { 163 | const oldIndex = [...data].findIndex((m) => m.id === id); 164 | const newData = insertAndShift([...data], oldIndex, index); 165 | setData(newData); 166 | }; 167 | return ( 168 | 169 | 170 | tabClick(id, evn)} 174 | onTabDrop={(id, index) => tabDrop(id, index)} 175 | > 176 | {data.map((m, idx) => { 177 | return ( 178 | 179 | {m.children} 180 | 181 | 182 | ); 183 | })} 184 | 185 |
{activeKey}
186 |
187 | ); 188 | } 189 | export default App; 190 | ``` 191 | 192 | ```jsx mdx:preview 193 | import React, { Fragment, useState, useCallback } from 'react'; 194 | import Tabs, { Tab, useDataContext } from '@uiw/react-tabs-draggable'; 195 | import styled from 'styled-components'; 196 | 197 | const TabWarp = styled(Tabs)` 198 | max-width: 450px; 199 | border-bottom: 1px solid #333; 200 | margin-bottom: -2px; 201 | gap: 3px; 202 | `; 203 | 204 | const TabItem = styled(Tab)` 205 | background-color: #b9b9b9; 206 | padding: 3px 7px; 207 | border-radius: 5px 5px 0 0; 208 | user-select: none; 209 | flex-wrap: nowrap; 210 | overflow: hidden; 211 | word-break: keep-all; 212 | align-items: center; 213 | display: flex; 214 | position: relative; 215 | flex-direction: row; 216 | &.w-active { 217 | color: #fff; 218 | background-color: #333; 219 | } 220 | `; 221 | 222 | function insertAndShift(arr, from, to) { 223 | let cutOut = arr.splice(from, 1)[0]; 224 | arr.splice(to, 0, cutOut); 225 | return arr; 226 | } 227 | 228 | let count = 9; 229 | 230 | function App() { 231 | const [data, setData] = useState([ 232 | { id: 'tab-4-1', children: 'Google' }, 233 | { id: 'tab-4-2', children: 'MicroSoft' }, 234 | { id: 'tab-4-3', children: 'Baidu' }, 235 | { id: 'tab-4-4', children: 'Taobao' }, 236 | { id: 'tab-4-5', children: 'JD' }, 237 | { id: 'tab-4-6', children: 'Apple' }, 238 | { id: 'tab-4-7', children: 'Bing' }, 239 | { id: 'tab-4-8', children: 'Gmail' }, 240 | { id: 'tab-4-9', children: 'Gitter' }, 241 | ]); 242 | const [test, setTest] = useState(1); 243 | const [activeKey, setActiveKey] = useState(''); 244 | 245 | const tabClick = (id, evn) => { 246 | evn.stopPropagation(); 247 | setActiveKey(id); 248 | setTest(test + 1); 249 | }; 250 | const closeHandle = (item, evn) => { 251 | evn.stopPropagation(); 252 | setData(data.filter((m) => m.id !== item.id)); 253 | }; 254 | const addHandle = () => { 255 | ++count; 256 | const newData = [...data, { id: `tab-3-${count}`, children: `New Tab ${count}` }]; 257 | setData(newData); 258 | }; 259 | const tabDrop = (id, index, offset) => { 260 | const oldIndex = [...data].findIndex((m) => m.id === id); 261 | const newData = insertAndShift([...data], oldIndex, index); 262 | setData(newData); 263 | }; 264 | return ( 265 | 266 | 267 | tabClick(id, evn)} 269 | onTabDrop={(id, index, offset) => tabDrop(id, index, offset)} 270 | > 271 | {data.map((m, idx) => { 272 | return ( 273 | 274 | {m.children} 275 | 276 | 277 | ); 278 | })} 279 | 280 |
{activeKey}
281 |
282 | ); 283 | } 284 | export default App; 285 | ``` 286 | 287 | ## Props 288 | 289 | ```ts 290 | export interface TabsProps extends React.DetailedHTMLProps, HTMLDivElement> { 291 | activeKey?: string; 292 | onTabClick?: (id: string, evn: React.MouseEvent) => void; 293 | /** 294 | * Optional. Called when a compatible item is dropped on the target. 295 | */ 296 | onTabDrop?: (id: string, index?: number, offset?: XYCoord | null) => void; 297 | } 298 | export interface TabProps extends React.DetailedHTMLProps, HTMLDivElement> { 299 | id: string; 300 | index?: number; 301 | /** Whether the Y axis can be dragged */ 302 | dragableY?: boolean; 303 | } 304 | export declare const Tab: FC>; 305 | ``` 306 | 307 | ## Development 308 | 309 | ```bash 310 | npm run watch # Listen create type and .tsx files. 311 | npm run start # Preview code example. 312 | ``` 313 | 314 | ## Contributors 315 | 316 | As always, thanks to our amazing contributors! 317 | 318 | 319 | 320 | 321 | 322 | Made with [action-contributors](https://github.com/jaywcjlove/github-action-contributors). 323 | 324 | ## License 325 | 326 | Licensed under the MIT License. 327 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uiw/react-tabs-draggable", 3 | "version": "1.0.1", 4 | "description": "Draggable tabs for React.", 5 | "homepage": "https://uiwjs.github.io/react-tabs-draggable", 6 | "author": "kenny wong ", 7 | "license": "MIT", 8 | "main": "./cjs/index.js", 9 | "module": "./esm/index.js", 10 | "types": "./esm/index.d.ts", 11 | "scripts": { 12 | "watch": "tsbb watch src/*.tsx --use-babel", 13 | "build": "tsbb build src/*.tsx --use-babel", 14 | "test": "tsbb test --env=jsdom", 15 | "coverage": "tsbb test --env=jsdom --coverage --bail" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/uiwjs/react-tabs-draggable.git" 20 | }, 21 | "files": [ 22 | "README.md", 23 | "dist", 24 | "src", 25 | "esm", 26 | "cjs" 27 | ], 28 | "peerDependencies": { 29 | "@babel/runtime": ">=7.11.0", 30 | "react": ">=16.8.0", 31 | "react-dom": ">=16.8.0" 32 | }, 33 | "dependencies": { 34 | "@babel/runtime": ">=7.11.0", 35 | "immutability-helper": "^3.1.1", 36 | "react-dnd": "^16.0.1", 37 | "react-dnd-html5-backend": "^16.0.1" 38 | }, 39 | "keywords": [ 40 | "react", 41 | "draggable", 42 | "tabs", 43 | "react-tabs", 44 | "react-tabs-draggable" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /core/src/Tab.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, useRef } from 'react'; 2 | import update from 'immutability-helper'; 3 | import { useDataContext } from './store'; 4 | import { useDrag, useDrop } from 'react-dnd'; 5 | import type { Identifier, XYCoord } from 'dnd-core'; 6 | 7 | export const ItemTypes = { 8 | Tab: 'wtabs', 9 | }; 10 | 11 | export interface TabProps extends React.DetailedHTMLProps, HTMLDivElement> { 12 | id: string; 13 | index?: number; 14 | /** Whether the Y axis can be dragged */ 15 | dragableY?: boolean; 16 | } 17 | 18 | export interface DragItem { 19 | index: number; 20 | id: string; 21 | type: string; 22 | } 23 | 24 | export const Tab: FC> = ({ children, id, index, dragableY = false, ...props }) => { 25 | const { state, onTabClick, onTabDrop, dispatch } = useDataContext(); 26 | const ref = useRef(null); 27 | const [{ handlerId }, drop] = useDrop({ 28 | accept: ItemTypes.Tab, 29 | collect(monitor) { 30 | return { 31 | handlerId: monitor.getHandlerId(), 32 | }; 33 | }, 34 | hover(item, monitor) { 35 | if (!ref.current || !state.data) { 36 | return; 37 | } 38 | const dragIndex = item.index; 39 | const hoverIndex = index || 0; 40 | // 不要用自己替换项目 41 | if (dragIndex === hoverIndex) { 42 | return; 43 | } 44 | // 确定屏幕上的矩形 45 | const hoverBoundingRect = ref.current.getBoundingClientRect(); 46 | // 获取垂直中间 47 | const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2; 48 | // 确定鼠标位置 49 | const clientOffset = monitor.getClientOffset(); 50 | // if (!clientOffset) return; 51 | // 将像素移到顶部 52 | const hoverClientX = (clientOffset as XYCoord).x - hoverBoundingRect.left; 53 | // Only perform the move when the mouse has crossed half of the items height 54 | // When dragging downwards, only move when the cursor is below 50% 55 | // When dragging upwards, only move when the cursor is above 50% 56 | // Dragging downwards 57 | if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX && dragableY !== true) { 58 | return; 59 | } 60 | // Dragging upwards 61 | if (dragIndex > hoverIndex && hoverClientX > hoverMiddleX) { 62 | return; 63 | } 64 | const newdata = update(state.data, { 65 | $splice: [ 66 | [dragIndex, 1], 67 | [hoverIndex, 0, state.data[dragIndex]], 68 | ], 69 | }); 70 | dispatch!({ data: [...newdata] }); 71 | item.index = hoverIndex; 72 | }, 73 | }); 74 | const [{ isDragging }, drag] = useDrag( 75 | () => ({ 76 | type: ItemTypes.Tab, 77 | item: () => { 78 | return { id, index }; 79 | }, 80 | end: (item, monitor) => { 81 | const clientOffset = monitor.getClientOffset(); 82 | onTabDrop && onTabDrop(id, item.index, clientOffset); 83 | }, 84 | collect: (monitor) => { 85 | return { 86 | data: monitor.getItem(), 87 | targetIds: monitor.getTargetIds(), 88 | isDragging: monitor.isDragging(), 89 | }; 90 | }, 91 | }), 92 | [id, index], 93 | ); 94 | 95 | const opacity = isDragging ? 0.001 : 1; 96 | 97 | if (props.draggable !== false) { 98 | drag(drop(ref)); 99 | } 100 | const handleClick = (evn: React.MouseEvent) => { 101 | dispatch!({ activeKey: id }); 102 | onTabClick && onTabClick(id, evn); 103 | }; 104 | return ( 105 |
113 | {children} 114 |
115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /core/src/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, isValidElement, PropsWithChildren, useEffect, useLayoutEffect } from 'react'; 2 | import { useDrop } from 'react-dnd'; 3 | import { useDataContext, InitialState } from './store'; 4 | import { ItemTypes } from './Tab'; 5 | import { TabsProps } from './'; 6 | 7 | export const Tabs: FC> = ({ children, activeKey, ...props }) => { 8 | const { state, dispatch } = useDataContext(); 9 | const [, drop] = useDrop(() => ({ 10 | accept: ItemTypes.Tab, 11 | })); 12 | 13 | useEffect(() => dispatch!({ activeKey }), [activeKey]); 14 | 15 | useLayoutEffect(() => { 16 | if (children) { 17 | const data: InitialState['data'] = []; 18 | React.Children.toArray(children).forEach((item) => { 19 | if (isValidElement(item)) { 20 | data.push({ ...item.props, element: item }); 21 | } 22 | }); 23 | dispatch!({ data }); 24 | } 25 | }, [children]); 26 | 27 | return ( 28 |
34 | {state.data && 35 | state.data.length > 0 && 36 | state.data.map(({ element, ...child }, idx) => { 37 | if (isValidElement(element)) { 38 | return React.cloneElement(element, { ...child, index: idx }); 39 | } 40 | })} 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /core/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useMemo, useRef } from 'react'; 2 | 3 | type Fn = (...args: ARGS) => R; 4 | export const useEventCallback = (fn: Fn): Fn => { 5 | let ref = useRef>(fn); 6 | useLayoutEffect(() => { 7 | ref.current = fn; 8 | }); 9 | return useMemo( 10 | () => 11 | (...args: A): R => { 12 | const { current } = ref; 13 | return current && current(...args); 14 | }, 15 | [], 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /core/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { DndProvider } from 'react-dnd'; 2 | import { FC, PropsWithChildren } from 'react'; 3 | import { HTML5Backend } from 'react-dnd-html5-backend'; 4 | import { Tabs } from './Tabs'; 5 | import { Provider } from './store'; 6 | import { useEventCallback } from './hooks'; 7 | import type { XYCoord } from 'dnd-core'; 8 | 9 | export * from './Tab'; 10 | export * from './hooks'; 11 | 12 | export interface TabsProps extends React.DetailedHTMLProps, HTMLDivElement> { 13 | activeKey?: string; 14 | onTabClick?: (id: string, evn: React.MouseEvent) => void; 15 | /** 16 | * Optional. Called when a compatible item is dropped on the target. 17 | */ 18 | onTabDrop?: (id: string, index?: number, offset?: XYCoord | null) => void; 19 | } 20 | 21 | const TabContainer: FC> = ({ activeKey, onTabClick, onTabDrop, ...props }) => { 22 | const tabClick = useEventCallback(onTabClick!); 23 | const tabDrop = useEventCallback(onTabDrop!); 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default TabContainer; 35 | -------------------------------------------------------------------------------- /core/src/store.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, createContext, PropsWithChildren, useContext, useReducer } from 'react'; 2 | import { TabsProps } from './'; 3 | export interface InitialState extends Pick { 4 | activeKey?: string; 5 | data?: Array<{ 6 | id: string; 7 | children: React.ReactElement; 8 | element: HTMLElement; 9 | }>; 10 | } 11 | 12 | export const initialState: InitialState = { 13 | activeKey: '', 14 | data: [], 15 | }; 16 | 17 | export const reducer = (state: Partial, action: Partial) => { 18 | return { 19 | ...state, 20 | ...action, 21 | }; 22 | }; 23 | 24 | export interface CreateContext { 25 | state: Partial; 26 | dispatch?: React.Dispatch; 27 | } 28 | 29 | export const Context = createContext({ 30 | state: initialState, 31 | dispatch: () => null, 32 | }); 33 | 34 | export const Provider: FC> = ({ children, init }) => { 35 | const [state, dispatch] = useReducer(reducer, init || initialState); 36 | return {children}; 37 | }; 38 | 39 | export function useDataContext() { 40 | const { state, dispatch } = useContext(Context); 41 | return { ...state, state, dispatch }; 42 | } 43 | -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "./cjs", 6 | "baseUrl": ".", 7 | "noEmit": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.1", 3 | "packages": ["core", "www"] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "lerna exec --scope @uiw/* --ignore www -- npm run build", 5 | "⬇️⬇️⬇️⬇️⬇️ package ⬇️⬇️⬇️⬇️⬇️": "▼▼▼▼▼ package ▼▼▼▼▼", 6 | "watch": "npm run-script watch --workspace @uiw/react-tabs-draggable", 7 | "doc": "npm run-script build --workspace www", 8 | "start": "npm run-script start --workspace www", 9 | "⬆️⬆️⬆️⬆️⬆️ package ⬆️⬆️⬆️⬆️⬆️": "▲▲▲▲▲ package ▲▲▲▲▲", 10 | "prepare": "husky install", 11 | "version": "lerna version --exact --force-publish --no-push --no-git-tag-version", 12 | "prettier": "prettier --write '**/*.{js,jsx,ts,tsx,html,less,md,json}'", 13 | "remove": "npm run clean && lerna exec \"rm -rf package-lock.json\" --scope react-code-preview-layout --scope website", 14 | "clean": "lerna clean --yes" 15 | }, 16 | "workspaces": [ 17 | "core", 18 | "www" 19 | ], 20 | "engines": { 21 | "node": ">=16.0.0" 22 | }, 23 | "lint-staged": { 24 | "*.{js,jsx,ts,tsx,html,less,md,json}": [ 25 | "prettier --write" 26 | ] 27 | }, 28 | "devDependencies": { 29 | "@kkt/ncc": "^1.0.15", 30 | "@kkt/less-modules": "^7.5.2", 31 | "husky": "~8.0.3", 32 | "kkt": "^7.5.2", 33 | "lerna": "^7.1.4", 34 | "lint-staged": "^13.2.3", 35 | "prettier": "^3.0.0", 36 | "tsbb": "^4.1.14" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "packageRules": [ 4 | { 5 | "matchPackagePatterns": ["*"], 6 | "rangeStrategy": "replace" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "declaration": true, 16 | "baseUrl": ".", 17 | "jsx": "react-jsx", 18 | "noFallthroughCasesInSwitch": true, 19 | "noEmit": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /www/.kktrc.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import { LoaderConfOptions, WebpackConfiguration } from 'kkt'; 3 | import { disableScopePlugin } from '@kkt/scope-plugin-options'; 4 | import { mdCodeModulesLoader } from 'markdown-react-code-preview-loader'; 5 | import pkg from './package.json'; 6 | 7 | export default (conf: WebpackConfiguration, env: 'production' | 'development', options: LoaderConfOptions) => { 8 | conf = mdCodeModulesLoader(conf); 9 | conf = disableScopePlugin(conf); 10 | // Get the project version. 11 | conf.plugins!.push( 12 | new webpack.DefinePlugin({ 13 | VERSION: JSON.stringify(pkg.version), 14 | }), 15 | ); 16 | conf.module!.exprContextCritical = false; 17 | if (env === 'production') { 18 | conf.optimization = { 19 | ...conf.optimization, 20 | splitChunks: { 21 | cacheGroups: { 22 | reactvendor: { 23 | test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, 24 | name: 'react-vendor', 25 | chunks: 'all', 26 | }, 27 | refractor: { 28 | test: /[\\/]node_modules[\\/](refractor)[\\/]/, 29 | name: 'refractor-vendor', 30 | chunks: 'all', 31 | }, 32 | codemirror: { 33 | test: /[\\/]node_modules[\\/](@codemirror)[\\/]/, 34 | name: 'codemirror-vendor', 35 | chunks: 'all', 36 | }, 37 | }, 38 | }, 39 | }; 40 | conf.output = { ...conf.output, publicPath: './' }; 41 | } 42 | return conf; 43 | }; 44 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "private": true, 4 | "version": "1.0.1", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "kkt build", 8 | "start": "kkt start" 9 | }, 10 | "devDependencies": { 11 | "@kkt/raw-modules": "^7.5.2", 12 | "@kkt/scope-plugin-options": "^7.5.2", 13 | "@types/react": "~18.2.0", 14 | "@types/react-dom": "~18.2.0", 15 | "kkt": "^7.5.2", 16 | "markdown-react-code-preview-loader": "^2.1.2" 17 | }, 18 | "dependencies": { 19 | "@uiw/react-markdown-preview-example": "^1.5.3", 20 | "@uiw/react-tabs-draggable": "1.0.1", 21 | "react": "~18.2.0", 22 | "react-dom": "~18.2.0", 23 | "react-router-dom": "^6.14.2" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiwjs/react-tabs-draggable/250d209231f71d31508b0189b036f0742c117a06/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | react-tabs-draggable 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /www/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import MarkdownPreviewExample from '@uiw/react-markdown-preview-example'; 3 | import data from '@uiw/react-tabs-draggable/README.md'; 4 | 5 | const Github = MarkdownPreviewExample.Github; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container!); 9 | root.render( 10 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /www/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare var VERSION: string; 4 | 5 | declare module '*.md' { 6 | import { CodeBlockData } from 'markdown-react-code-preview-loader'; 7 | const src: CodeBlockData; 8 | export default src; 9 | } 10 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "emitDeclarationOnly": true, 7 | "noEmit": false 8 | } 9 | } 10 | --------------------------------------------------------------------------------