Henry Henry
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)

Henry

小学生中的前端大佬
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Vue

  • ElementUI

  • React

  • AntD

    • 基于 AntD 封装的与原生事件一致的 Input 组件
    • 基于 AntD 封装的 TextEllipsis
    • 基于 AntD 封装的可编辑 tabs
      • 功能
      • 定义 Props
      • 接收 Props,定义 state
      • 监听事件
      • 处理原始 onEdit
      • 渲染 TabPane
      • 重命名逻辑
      • 使用
  • 前端
  • AntD
Henry
2022-08-05
目录

基于 AntD 封装的可编辑 tabs

AntD 的 Tabs 只支持新增/删除页签,而不支持修改,故我们需要扩展一下

# 功能

新增:点击最右侧“+”新增,默认名称为:“未命名”(可通过 defaultTabName 配置),如名称已重复,则命名为“未命名(1)”,括号中数字累加,新增后选中该 tabs

切换和改名:点击页签切换到该页签,聚焦并选中页签文字,并可进行更改页签名称,重名则改名失败,恢复原名称,toast 提示:名称已存在!;如清空页签文字,默认名称为“未命名”,重名与新增处理一致

删除:只有当前选中的页签显示删除图标:“X”。点击“X”可以删除页签

编辑时 Input 默认无边框,可通过传入 inputProps.bordered 配置;默认名称字符个数不限制,可通过传入 inputProps.maxLength 限制;inputProps 支持大部分 Input 属性;

默认名称全部显示,可通过传入 tabPaneWidth 控制宽度,默认超长显示 ...;宽度超出屏幕后,样式与 Tabs 一致

重名处理时,tabs 全部名称默认取当前所有页签名称,可通过传入  tabNames 自定义

页签前可通过传入 icon 展示额外状态:两种方式:

  • 通过传入 statusIcon (type: Dom | Function) 方式,自定义程度高
  • 组件预置几种状态,通过传入 statusIcon (type: string)  方式,受限于预置,自定义程度低

# 定义 Props

import { TabsProps, TabPaneProps } from 'antd/lib/tabs'
import RealInput, { RealInputProps } from '../RealInput'

export interface EditableTabPaneProps extends Omit<TabPaneProps, 'tab'> {
  tab: string
  key: string
  statusIcon?: 'error' | 'success' | React.ReactNode | Function
  style?: React.CSSProperties
}

export interface EditableTabsInputProps extends Omit<RealInputProps, 'value'> {}

export interface EditableTabsProps extends Omit<TabsProps, 'activeKey' | 'onEdit'> {
  activeKey: string
  inputProps?: EditableTabsInputProps
  defaultTabName?: string
  tabNames?: string[]
  tabPaneWidth?: number
  children: React.ReactElement<EditableTabPaneProps>[]
  onEdit?: (
    e: React.MouseEvent | React.KeyboardEvent | string,
    action: 'add' | 'remove' | 'rename',
    value?: string
  ) => void
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 接收 Props,定义 state

const {
  inputProps,
  defaultTabName = '未命名',
  tabNames,
  tabPaneWidth,
  children,
  activeKey,
  type = 'editable-card',
  onEdit,
  ...res
} = props

const [state, updateState] = useImmer({
  tabNames: [],
  tabKey: Date.now()
})

const inputRef = useRef(null)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 监听事件

useEffect(() => {
  if (tabNames) return
  updateState(draft => {
    draft.tabNames = children.map(item => item.props.tab)
  })
}, [children])

useEffect(() => {
  const { current } = inputRef
  current?.focus()
  current?.select()
}, [activeKey])
1
2
3
4
5
6
7
8
9
10
11
12

# 处理原始 onEdit

/**
 * 编辑
 * @param e event | activeKey
 * @param action add | remove
 */
function handleEdit(
  e: string | React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>,
  action: 'add' | 'remove'
) {
  if (action === 'add') {
    const value = generateName(tabNames || state.tabNames, defaultTabName)
    onEdit?.(e, action, value)
  } else {
    onEdit?.(e, action)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 渲染 TabPane

/**
 * 渲染 TabPane
 * @returns TabPane
 */
function renderTabPane() {
  return children.map(item => {
    const { children, tab, disabled, style, ...res } = item.props
    return (
      <TabPane
        tab={tabDom(item.key as string, item.props)}
        key={item.key}
        closable={activeKey === item.key}
        {...res}>
        {children}
      </TabPane>
    )
  })
}

/**
 * 渲染 tab 标签头
 * @param currentKey tab key
 * @param props EditableTabPaneProps
 * @returns tab 标签头
 */
function tabDom(currentKey: string, props: GITabPaneProps) {
  const { disabled, tab, statusIcon, style } = props
  const active = activeKey === currentKey
  let icon = null
  let iconClass = null
  if (statusIcon) {
    switch (typeof statusIcon) {
      case 'string':
        icon = statusIconMap[statusIcon as string]
        iconClass = statusIcon
        break
      case 'function':
        icon = (statusIcon as Function)()
        break
      default:
        icon = statusIcon
        break
    }
  }
  let result = (
    <div
      className={`ant-tabs-tab-btn-content ant-tabs-tab-btn-${iconClass}`}
      style={{ width: tabPaneWidth ? `${tabPaneWidth}px` : 'auto', ...style }}>
      {icon} <TextEllipsis title={tab} />
    </div>
  )
  if (type === 'editable-card' && active && !disabled) {
    result = (
      <RealInput
        value={tab}
        ref={inputRef}
        bordered={false}
        style={{
          width: tabPaneWidth ? `${tabPaneWidth}px` : 'auto'
        }}
        onKeyDown={e => {
          // Prevent "space" blocking from rc-tabs: https://github.com/react-component/tabs/issues/309
          e.stopPropagation()
        }}
        onChange={(value: string) => {
          handleInputChange(value, currentKey)
        }}
        {...inputProps}
      />
    )
  }
  return result
}

/**
 * input change
 * @param value value
 * @param currentKey tab key
 */
function handleInputChange(value: string, currentKey: string) {
  if (!value) {
    value = generateName(tabNames || state.tabNames, defaultTabName)
    onEdit?.(currentKey, 'rename', value)
  } else {
    if ((tabNames || state.tabNames).includes(value)) {
      message.warning('名称已存在!')
      updateState(draft => {
        draft.tabKey = Date.now()
      })
    } else {
      onEdit?.(currentKey, 'rename', value)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

# 重命名逻辑

/**
 * 生成新增节点名称
 * @param namePool
 * @param addName 新增的名字
 * @returns
 */
export function generateName(namePool: string[], addName: string) {
  if (!namePool || !namePool.length) return addName

  const getName = addName => {
    let result = ''
    const repeatName = namePool.find(item => item === addName)
    if (repeatName) {
      if (/\(\d+\)$/.test(addName)) {
        let addStr = addName.match(/\(([^)]+)\)/)[1]
        result = addName.replace(/\(\d+\)$/, `(${Number(addStr) + 1})`)
      } else {
        result = addName + '(1)'
      }
      return getName(result)
    } else {
      return addName
    }
  }
  return getName(addName)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 使用

const [state, updateState] = useImmer<State>({
  tabs: [
    { tab: 'Tab 1', key: 'first', statusIcon: <WifiOutlined /> },
    { tab: 'Tab 2', key: 'second', style: { width: '50px', color: 'red' } },
    { tab: 'Tab 3', key: 'third', statusIcon: 'error' },
    { tab: 'Tab 4', key: 'fourth' }
  ],
  contents: [1, 2, 3, 4]
})
1
2
3
4
5
6
7
8
9
<GIEditableTabs activeKey="{state.activeKey}" onChange="{handleChange}" onEdit="{handleEdit}">
  {state.tabs.map((item, index) => (
    <div key="{item.key}" {...item}>
      content: {state.contents[index] || '找不到啦~'}
    </div>
  ))}
</GIEditableTabs>
1
2
3
4
5
6
7
编辑 (opens new window)
上次更新: 5/27/2023, 1:02:05 PM
基于 AntD 封装的 TextEllipsis

← 基于 AntD 封装的 TextEllipsis

最近更新
01
version 1.15
07-01
02
version 1.14
06-27
03
version 1.13
06-27
更多文章>
Theme by Vdoing | Copyright © 2017-2023 HenryTSZ | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式