├── .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://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 | [](https://github.com/uiwjs/react-tabs-draggable/actions/workflows/ci.yml)
4 | [](https://uiwjs.github.io/npm-unpkg/#/pkg/@uiw/react-tabs-draggable/file/README.md)
5 | [](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 |
--------------------------------------------------------------------------------