【PPTist】表格功能

news2024/12/29 0:13:29

前言:这篇文章来探讨一下表格功能是怎么实现的吧!

一、插入表格

我们可以看到,鼠标移动到菜单项上出现的提示语是“插入表格”
在这里插入图片描述
那么就全局搜索一下,就发现这个菜单在 src/views/Editor/CanvasTool/index.vue 文件中

<Popover trigger="click" v-model:value="tableGeneratorVisible" :offset="10">
  <template #content>
    <TableGenerator
      @close="tableGeneratorVisible = false"
      @insert="({ row, col }) => { createTableElement(row, col); tableGeneratorVisible = false }"
    />
  </template>
  <IconInsertTable class="handler-item" v-tooltip="'插入表格'" />
</Popover>

看一下组件 TableGenerator,是用来选择表格的长宽的组件。

src/views/Editor/CanvasTool/TableGenerator.vue

<table 
  @mouseleave="endCell = []" 
  @click="handleClickTable()" 
  v-if="!isCustom"
>
  <tbody>
    <tr v-for="row in 10" :key="row">
      <td 
        @mouseenter="endCell = [row, col]"
        v-for="col in 10" :key="col"
      >
        <div 
          class="cell" 
          :class="{ 'active': endCell.length && row <= endCell[0] && col <= endCell[1] }"
        ></div>
      </td>
    </tr>
  </tbody>
</table>

可以看到主要是通过监听鼠标移入事件和鼠标离开时间。鼠标移入的时候,将鼠标移入的当前的 td 的位置赋值给 endCell,并且高亮在endCell 范围内的 td
点击的时候,创建表格元素并且插入。创建元素的方法在下面的文件中统一管理
关于表格的位置的处理还比较简单,统一放在水平垂直居中的位置。
src/hooks/useCreateElement.ts

/**
 * 创建表格元素
 * @param row 行数
 * @param col 列数
 */
const createTableElement = (row: number, col: number) => {
  const style: TableCellStyle = {
    fontname: theme.value.fontName,
    color: theme.value.fontColor,
  }
  // 创建表格数据 空的二维数组
  const data: TableCell[][] = []
  for (let i = 0; i < row; i++) {
    const rowCells: TableCell[] = []
    for (let j = 0; j < col; j++) {
      rowCells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '', style })
    }
    data.push(rowCells)
  }

  const DEFAULT_CELL_WIDTH = 100
  const DEFAULT_CELL_HEIGHT = 36

  // 创建列宽数组 每个元素的值为1/col
  const colWidths: number[] = new Array(col).fill(1 / col)

  const width = col * DEFAULT_CELL_WIDTH
  const height = row * DEFAULT_CELL_HEIGHT

  // 创建表格元素
  createElement({
    type: 'table',
    id: nanoid(10),
    width,
    height,
    colWidths,
    rotate: 0,
    data,
    left: (VIEWPORT_SIZE - width) / 2,
    top: (VIEWPORT_SIZE * viewportRatio.value - height) / 2,
    outline: {
      width: 2,
      style: 'solid',
      color: '#eeece1',
    },
    theme: {
      color: theme.value.themeColor,
      rowHeader: true,
      rowFooter: false,
      colHeader: false,
      colFooter: false,
    },
    cellMinHeight: 36,
  })
}

以及来看一下公用的 createElement 方法都做了什么

// 创建(插入)一个元素并将其设置为被选中元素
const createElement = (element: PPTElement, callback?: () => void) => {
  // 添加元素到元素列表
  slidesStore.addElement(element)
  // 设置被选中元素列表
  mainStore.setActiveElementIdList([element.id])

  if (creatingElement.value) mainStore.setCreatingElement(null)

  setTimeout(() => {
    // 设置编辑器区域为聚焦状态
    mainStore.setEditorareaFocus(true)
  }, 0)

  if (callback) callback()

  // 添加历史快照
  addHistorySnapshot()
}

以及添加元素的方法 slidesStore.addElement
src/store/slides.ts

addElement(element: PPTElement | PPTElement[]) {
  const elements = Array.isArray(element) ? element : [element]
  const currentSlideEls = this.slides[this.slideIndex].elements
  const newEls = [...currentSlideEls, ...elements]
  this.slides[this.slideIndex].elements = newEls
},

新添加的元素就放在当前的幻灯片的元素列表的最后就行,也不用考虑按顺序摆放,因为元素里面都有各自的位置信息

mainStore.setCreatingElement() 这个方法就是设置一个公用的对象 creatingElement,设置为 null 表示创建结束啦
src/store/main.ts

setCreatingElement(element: CreatingElement | null) {
  this.creatingElement = element
},

mainStore.setEditorareaFocus(true) 聚焦于编辑区域,这个方法也简单
src/store/main.ts

setEditorareaFocus(isFocus: boolean) {
  this.editorAreaFocus = isFocus
},

还有两个方法是以前见过的

mainStore.setActiveElementIdList() 方法见 【PPTist】网格线、对齐线、标尺
addHistorySnapshot() 方法见 【PPTist】历史记录功能
总结来说, createElement 里面都干了这些事情

  • 添加元素到当前幻灯片的元素列表
  • 将这个新的元素设置为被选中的状态
  • creatingElement 置空
  • 将焦点放在编辑区域
  • 执行回调函数(如果有的话)
  • 将创建元素的行为添加到历史快照中

ok,这是表格的创建阶段完成了。

二、表格编辑

接下来要看一下表格右键的一些方法
进入表格的编辑状态,右键出来的菜单长这样
在这里插入图片描述
这个创建出来的表格的组件是 src/views/components/element/TableElement/EditableTable.vue
表格的数据是 tableCells 二维数组。这个文件里的代码有点复杂了。一个一个来吧。

1、右键菜单

菜单由指令 v-contextmenu 添加,这是一个自定义指令,定义在 src/plugins/directive/contextmenu.ts

① 自定义指令

自定义指令定义了两个生命周期函数,一个是 mounted,一个是 unmounted。自定义指令被挂载的时候,会接受一个参数。mounted 的第一个参数是默认参数,表示使用自定义指令的元素,第二个参数是通过自定义指定传递过来的参数。
然后绑定了右键菜单事件 contextmenu,并且将事件记录了一个索引值,便于元素卸载的时候解绑右键菜单时间

// 定义自定义指令
const ContextmenuDirective: Directive = {
  // 在元素挂载时
  mounted(el: CustomHTMLElement, binding) {
    // 保存事件处理器引用,方便后续解绑
    el[CTX_CONTEXTMENU_HANDLER] = (event: MouseEvent) => contextmenuListener(el, event, binding)
    // 绑定右键菜单事件
    el.addEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
  },

  // 在元素卸载时
  unmounted(el: CustomHTMLElement) {
    // 清理事件监听,避免内存泄漏
    if (el && el[CTX_CONTEXTMENU_HANDLER]) {
      el.removeEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
      delete el[CTX_CONTEXTMENU_HANDLER]
    }
  },
}
② 创建右键菜单
// 核心的右键菜单处理函数
const contextmenuListener = (el: HTMLElement, event: MouseEvent, binding: DirectiveBinding) => {
  // 阻止默认右键菜单和事件冒泡
  event.stopPropagation()
  event.preventDefault()

  // 调用指令绑定的值函数,获取菜单配置
  const menus = binding.value(el)
  if (!menus) return

  let container: HTMLDivElement | null = null

  // 清理函数:移除右键菜单并清理相关事件监听
  const removeContextmenu = () => {
    if (container) {
      document.body.removeChild(container)
      container = null
    }
    // 移除目标元素的激活状态样式
    el.classList.remove('contextmenu-active')
    // 清理全局事件监听
    document.body.removeEventListener('scroll', removeContextmenu)
    window.removeEventListener('resize', removeContextmenu)
  }

  // 准备创建菜单所需的配置项
  const options = {
    axis: { x: event.x, y: event.y },  // 鼠标点击位置
    el,                                // 目标元素
    menus,                            // 菜单配置
    removeContextmenu,                // 清理函数
  }

  // 创建容器并渲染菜单组件
  container = document.createElement('div')
  const vm = createVNode(ContextmenuComponent, options, null)
  render(vm, container)
  document.body.appendChild(container)

  // 为目标元素添加激活状态样式
  el.classList.add('contextmenu-active')

  // 监听可能导致菜单需要关闭的全局事件
  document.body.addEventListener('scroll', removeContextmenu)
  window.addEventListener('resize', removeContextmenu)
}

其中的 removeContextmenu 是一个闭包,在闭包内销毁指令创建出来的元素,并且清除自身的监听回调。
菜单配置是通过自定义指令传递过来的方法获取的。
例如表格 v-contextmenu="(el: HTMLElement) => contextmenus(el)",返回的是菜单项的数组。

const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
  // 获取单元格索引
  const cellIndex = el.dataset.cellIndex as string
  const rowIndex = +cellIndex.split('_')[0]
  const colIndex = +cellIndex.split('_')[1]

  // 如果当前单元格未被选中,则将当前单元格设置为选中状态
  if (!selectedCells.value.includes(`${rowIndex}_${colIndex}`)) {
    startCell.value = [rowIndex, colIndex]
    endCell.value = []
  }

  const { canMerge, canSplit } = checkCanMergeOrSplit(rowIndex, colIndex)
  const { canDeleteRow, canDeleteCol } = checkCanDeleteRowOrCol()

  return [
    {
      text: '插入列',
      children: [
        { text: '到左侧', handler: () => insertCol(colIndex) },
        { text: '到右侧', handler: () => insertCol(colIndex + 1) },
      ],
    },
    {
      text: '插入行',
      children: [
        { text: '到上方', handler: () => insertRow(rowIndex) },
        { text: '到下方', handler: () => insertRow(rowIndex + 1) },
      ],
    },
    {
      text: '删除列',
      disable: !canDeleteCol,
      handler: () => deleteCol(colIndex),
    },
    {
      text: '删除行',
      disable: !canDeleteRow,
      handler: () => deleteRow(rowIndex),
    },
    { divider: true },
    {
      text: '合并单元格',
      disable: !canMerge,
      handler: mergeCells,
    },
    {
      text: '取消合并单元格',
      disable: !canSplit,
      handler: () => splitCells(rowIndex, colIndex),
    },
    { divider: true },
    {
      text: '选中当前列',
      handler: () => selectCol(colIndex),
    },
    {
      text: '选中当前行',
      handler: () => selectRow(rowIndex),
    },
    {
      text: '选中全部单元格',
      handler: selectAll,
    },
  ]
}

创建组件使用的是 createVNode 方法,ContextmenuComponentsrc/components/Contextmenu/index.vue 组件
createVNode 方法参数列表:

  1. type
    类型: string | object
    描述: VNode 的类型,可以是一个 HTML 标签名(如 ‘div’、‘span’ 等),也可以是一个组件的定义(如一个 Vue 组件的对象或异步组件的工厂函数)。
  2. props
    类型: object | null
    描述: 传递给组件或元素的属性。对于组件,这些属性会被作为 props 传递;对于 DOM 元素,这些属性会被直接应用到元素上。
  3. children
    类型: string | VNode | Array<VNode | string> | null
    描述: VNode 的子节点,可以是一个字符串(文本节点)、一个 VNode、一个 VNode 数组,或者是 null。如果提供了多个子节点,可以用数组的形式传递。

通过createVNode 方法,会将鼠标点击的位置、目标元素、菜单配置以及清理函数传递给自定义指令的组件。

并且给全局增加了滚动事件的监听和调整大小事件的监听,当滚动鼠标或者调整页面大小的时候,就隐藏右键菜单。

③ 右键菜单组件

右键菜单组件是 src/components/Contextmenu/index.vue,其中的菜单项是 src/components/Contextmenu/MenuContent.vue
菜单里面的具体的菜单项上面已经讲过是咋来的,使用自定义指令的时候,通过方法返回一个对象数组。点击菜单项的时候,执行回调函数

const handleClickMenuItem = (item: ContextmenuItem) => {
  if (item.disable) return
  if (item.children && !item.handler) return
  if (item.handler) item.handler(props.el)
  props.removeContextmenu()
}

2、插入列

// 插入一列
const insertCol = (colIndex: number) => {
  tableCells.value = tableCells.value.map(item => {
  	// 每一行都要在 colIndex 的地方添加一个元素
    const cell = {
      colspan: 1,
      rowspan: 1,
      text: '',
      id: nanoid(10),
    }
    item.splice(colIndex, 0, cell)
    return item 
  })
  colSizeList.value.splice(colIndex, 0, 100)
  emit('changeColWidths', colSizeList.value)
}

在模版中,表格项遍历的时候,会给每一个 td 元素添加一个属性 :data-cell-index="KaTeX parse error: Expected group after '_' at position 11: {rowIndex}_̲{colIndex}"
插入列的时候,如果是向左插入,colIndex 直接取元素上绑定的值,如果是向右插入,需要取 colIndex + 1
输出一下 colSizeList.value,它记录的是所有列的宽度,所以这里插入的是 100,即默认插入列的宽度是 100px
在这里插入图片描述

3、插入行

行的数据就复杂那么一丢丢

// 插入一行
const insertRow = (rowIndex: number) => {
  const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))

  const rowCells: TableCell[] = []
  for (let i = 0; i < _tableCells[0].length; i++) {
    rowCells.push({
      colspan: 1,
      rowspan: 1,
      text: '',
      id: nanoid(10),
    })
  }

  _tableCells.splice(rowIndex, 0, rowCells)
  tableCells.value = _tableCells
}

插入的时候需要创建一个数组

我们看一下里面的几个数据分别长什么样子
如图下面这个表格,我在第一行的下面增加一行的时候
在这里插入图片描述
新的一行的数据如下:
在这里插入图片描述

_tableCells 的数据如下:
是一个二维数组
在这里插入图片描述

在模版中,表格是遍历二维数组 tableCells 创建的。至于单元格的宽度,是通过 colgroup标签,循环 colSizeList 制定的。

<colgroup>
  <col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
</colgroup>

这个标签主要用来指定列的宽度。span 属性我看官网说已经禁用了。

删除行或列类似,主要通过 splice 方法进行数组元素的剪切

4、合并单元格

这是比较复杂的功能了。它会修改最小的坐标处的单元格的 colSpanrowspan ,表示当前这个单元格占多少单位行或者单位列。但是后面的单元格,不会删除,会隐藏掉。也就是说,二维数组的结构不变,只是其中的合并单元格的开头单元格的 rowSpancolSpan 变了
下面这个单元格,就是被合并的效果,第一个单元格撑宽,第二个单元格 display: none
在这里插入图片描述
看一下被合并的单元格的数据,只有第一个格格的数据会被修改
在这里插入图片描述

// 合并单元格
const mergeCells = () => {
  const [startX, startY] = startCell.value
  const [endX, endY] = endCell.value

  const minX = Math.min(startX, endX)
  const minY = Math.min(startY, endY)
  const maxX = Math.max(startX, endX)
  const maxY = Math.max(startY, endY)

  const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  
  // 更新坐标最小的单元格的rowspan和colspan
  _tableCells[minX][minY].rowspan = maxX - minX + 1
  _tableCells[minX][minY].colspan = maxY - minY + 1

  tableCells.value = _tableCells
  removeSelectedCells()
}

起始的单元格 startCell 在鼠标落下的时候会更新它的值

const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {
  if (e.button === 0) {
    endCell.value = []
    isStartSelect.value = true
    startCell.value = [rowIndex, colIndex]
  }
}

结束的单元格在鼠标移入单元格的时候就会更新

const handleCellMouseenter = (rowIndex: number, colIndex: number) => {
  if (!isStartSelect.value) return
  endCell.value = [rowIndex, colIndex]
}

隐藏后面的单元格是怎么实现的呢?是通过 td 标签上的 v-show="!hideCells.includes(KaTeX parse error: Expected group after '_' at position 11: {rowIndex}_̲{colIndex})" 这个判断实现的。
hideCells 的计算在 src/views/components/element/TableElement/useHideCells.ts 文件中,它是个计算属性,但是竟然也分成一个文件写,所以这代码管理的层级很好哦

// 这是一个组合式函数 (Composable),用于处理表格合并时的单元格隐藏逻辑
export default (cells: Ref<TableCell[][]>) => {
  // computed 会创建一个响应式的计算属性
  const hideCells = computed(() => {
    const hideCells: string[] = []
    
    // 双重循环遍历表格的每一个单元格
    for (let i = 0; i < cells.value.length; i++) {      // 遍历行
      const rowCells = cells.value[i]
      for (let j = 0; j < rowCells.length; j++) {       // 遍历列
        const cell = rowCells[j]
        
        // 如果当前单元格设置了合并
        if (cell.colspan > 1 || cell.rowspan > 1) {
          // 遍历被合并的区域
          for (let row = i; row < i + cell.rowspan; row++) {
            for (let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) {
              // 将被合并的单元格位置添加到数组
              // 例如:如果是第2行第3列的单元格,会生成 "2_3"
              hideCells.push(`${row}_${col}`)
            }
          }
        }
      }
    }
    return hideCells
  })

  return {
    hideCells, // 返回需要隐藏的单元格位置数组
  }
}

5、拆分单元格

这个方法就挺简单的了,之前我们合并单元格的时候,是把坐标最小的单元格的 rowSpancolSpan 修改成合并单元格选中的横向格数和纵向格数。那么拆分单元格,直接把单元格的 rowSpancolSpan 都变回 1 就可以了。

// 拆分单元格
const splitCells = (rowIndex: number, colIndex: number) => {
  const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  _tableCells[rowIndex][colIndex].rowspan = 1
  _tableCells[rowIndex][colIndex].colspan = 1

  tableCells.values
  removeSelectedCells()
}

修改表格数据的方法,基本上都使用的是 tableCells.value 重新给表格数据赋值的方法。这也就确保上面的计算属性 hideCells 能触发更新。

6、选中当前列/行、选中全部单元格

选中这个操作,处理起来很简单,只是修改两个表示范围的响应式数据 startCellendCell

// 选中指定的列
const selectCol = (index: number) => {
  const maxRow = tableCells.value.length - 1
  startCell.value = [0, index]
  endCell.value = [maxRow, index]
}

另外两个也类似,就不粘贴了。
然后选中的单元格会有高亮效果,在模版中 td 标签上

:class="{
         'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1,
         'active': activedCell === `${rowIndex}_${colIndex}`,
       }"

选中的单元格是计算属性 selectedCells

// 当前选中的单元格集合
const selectedCells = computed(() => {
  if (!startCell.value.length) return []
  const [startX, startY] = startCell.value

  if (!endCell.value.length) return [`${startX}_${startY}`]
  const [endX, endY] = endCell.value

  if (startX === endX && startY === endY) return [`${startX}_${startY}`]

  const selectedCells = []

  const minX = Math.min(startX, endX)
  const minY = Math.min(startY, endY)
  const maxX = Math.max(startX, endX)
  const maxY = Math.max(startY, endY)

  for (let i = 0; i < tableCells.value.length; i++) {
    const rowCells = tableCells.value[i]
    for (let j = 0; j < rowCells.length; j++) {
      if (i >= minX && i <= maxX && j >= minY && j <= maxY) selectedCells.push(`${i}_${j}`)
    }
  }
  return selectedCells
})


然后捏,选中的单元格修改的时候,还需要触发一个自定义函数

watch(selectedCells, (value, oldValue) => {
  if (isEqual(value, oldValue)) return
  emit('changeSelectedCells', selectedCells.value)
})

在父组件中监听这个函数,更新全局的 selectedTableCells 属性

// 更新表格当前选中的单元格
const updateSelectedCells = (cells: string[]) => {
  nextTick(() => mainStore.setSelectedTableCells(cells))
}

7、删除列/行

删除列/行的代码差不多,都是使用 splice 方法,将删除的单元格截取掉。

// 删除一行
const deleteRow = (rowIndex: number) => {
  const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))

  const targetCells = tableCells.value[rowIndex]
  const hideCellsPos = []
  for (let i = 0; i < targetCells.length; i++) {
    if (isHideCell(rowIndex, i)) hideCellsPos.push(i)
  }
  
  for (const pos of hideCellsPos) {
    for (let i = rowIndex; i >= 0; i--) {
      if (!isHideCell(i, pos)) {
        _tableCells[i][pos].rowspan = _tableCells[i][pos].rowspan - 1
        break
      }
    }
  }

  _tableCells.splice(rowIndex, 1)
  tableCells.value = _tableCells
}
// 删除一列
const deleteCol = (colIndex: number) => {
  const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))

  const hideCellsPos = []
  for (let i = 0; i < tableCells.value.length; i++) {
    if (isHideCell(i, colIndex)) hideCellsPos.push(i)
  }

  for (const pos of hideCellsPos) {
    for (let i = colIndex; i >= 0; i--) {
      if (!isHideCell(pos, i)) {
        _tableCells[pos][i].colspan = _tableCells[pos][i].colspan - 1
        break
      }
    }
  }

  tableCells.value = _tableCells.map(item => {
    item.splice(colIndex, 1)
    return item
  })
  colSizeList.value.splice(colIndex, 1)
  emit('changeColWidths', colSizeList.value)
}

8、快捷键

快捷键是上下左右箭头,以及 ctrl + 上下左右箭头。代码看起来还是比较好理解的

// 表格快捷键监听
const keydownListener = (e: KeyboardEvent) => {
  if (!props.editable || !selectedCells.value.length) return

  const key = e.key.toUpperCase()
  if (selectedCells.value.length < 2) {
    if (key === KEYS.TAB) {
      e.preventDefault()
      tabActiveCell()
    }
    else if (e.ctrlKey && key === KEYS.UP) {
      e.preventDefault()
      const rowIndex = +selectedCells.value[0].split('_')[0]
      insertRow(rowIndex)
    }
    else if (e.ctrlKey && key === KEYS.DOWN) {
      e.preventDefault()
      const rowIndex = +selectedCells.value[0].split('_')[0]
      insertRow(rowIndex + 1)
    }
    else if (e.ctrlKey && key === KEYS.LEFT) {
      e.preventDefault()
      const colIndex = +selectedCells.value[0].split('_')[1]
      insertCol(colIndex)
    }
    else if (e.ctrlKey && key === KEYS.RIGHT) {
      e.preventDefault()
      const colIndex = +selectedCells.value[0].split('_')[1]
      insertCol(colIndex + 1)
    }
    else if (key === KEYS.UP) {
      const range = getCaretPosition(e.target as HTMLDivElement)
      if (range && range.start === range.end && range.start === 0) {
        moveActiveCell('UP')
      }
    }
    else if (key === KEYS.DOWN) {
      const range = getCaretPosition(e.target as HTMLDivElement)
      if (range && range.start === range.end && range.start === range.len) {
        moveActiveCell('DOWN')
      }
    }
    else if (key === KEYS.LEFT) {
      const range = getCaretPosition(e.target as HTMLDivElement)
      if (range && range.start === range.end && range.start === 0) {
        moveActiveCell('LEFT')
      }
    }
    else if (key === KEYS.RIGHT) {
      const range = getCaretPosition(e.target as HTMLDivElement)
      if (range && range.start === range.end && range.start === range.len) {
        moveActiveCell('RIGHT')
      }
    }
  }
  else if (key === KEYS.DELETE) {
    clearSelectedCellText()
  }
}

关于 moveActiveCell() 方法,里面的主要做的事情,就是调整 startCell ,起始单元格的位置。

9、快捷键bug

然后这里发现了一个小bug。我使用的是搜狗输入法。如果我正在输入中文,然后点击了上下左右箭头,想选择输入法中的目标文字,焦点就会直接跳转到目标单元格,编辑器的快捷键覆盖了输入法的快捷键。所以应该判断一下,如果当前正在编辑,就不进行单元格的跳转了。
使用 KeyboardEvent.isComposing 事件的 isComposing 属性判断是否在进行输入法输入即可。

const keydownListener = (e: KeyboardEvent) => {
  if (!props.editable || !selectedCells.value.length) return
  // 添加输入法检查
  if (e.isComposing) return
  const key = e.key.toUpperCase()
  // ... 
}

表格功能确实是很复杂啊,细节太多了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2267165.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web安全攻防入门教程——hvv行动详解

Web安全攻防入门教程 Web安全攻防是指在Web应用程序的开发、部署和运行过程中&#xff0c;保护Web应用免受攻击和恶意行为的技术与策略。这个领域不仅涉及防御措施的实现&#xff0c;还包括通过渗透测试、漏洞挖掘和模拟攻击来识别潜在的安全问题。 本教程将带你入门Web安全攻防…

游戏开发线性空间下PS工作流程

前言 使用基于物理的渲染&#xff0c;为了保证光照计算的准确&#xff0c;需要使用线性空间&#xff1b; 使用线性空间会带来一个问题&#xff0c;ui 在游戏引擎中的渲染结果与 PS 中的不一致&#xff1a; PS&#xff08;颜色空间默认是sRGB伽马空间&#xff09;&#xff1a…

Segment Routing Overview

大家觉得有意义和帮助记得及时关注和点赞!!! Segment Routing (SR) 是近年来网络领域的一项新技术&#xff0c;“segment” 在这里 指代网络隔离技术&#xff0c;例如 MPLS。如果快速回顾网络设计在过去几十年的 发展&#xff0c;我们会发现 SR 也许是正在形成的第三代网络设计…

【连续学习之随机初始化算法 】2024Nature期刊论文Loss of plasticity in deep continual learning

1 介绍 年份&#xff1a;2024 期刊&#xff1a;Nature Dohare S, Hernandez-Garcia J F, Lan Q, et al. Loss of plasticity in deep continual learning[J]. Nature, 2024, 632(8026): 768-774. 本文提出的算法是“持续反向传播”&#xff08;continual backpropagation&a…

【NODE】01-fs和path常用知识点

前言 最近在使用express-generator知识进行搭建前后端通信&#xff0c;其中有些知识点涉及到nodejs的fs和path核心模块&#xff0c;因此另写一篇文章进行介绍和代码案例练习。 fs&#xff08;文件系统&#xff09;和 path 是 Node.js 的核心模块&#xff0c;用于文件操作和路径…

两分钟解决:vscode卡在设置SSH主机,VS Code-正在本地初始化VSCode服务器

问题原因 remote-ssh还是有一些bug的&#xff0c;在跟新之后可能会一直加载初始化SSH主机解决方案 1.打开终端2.登录链接vscode的账号&#xff0c;到家目录下3.找到 .vscode-server文件,删掉这个文件4.重启 vscode 就没问题了

干货ScottPlot4向ScottPlot5迁移

干货ScottPlot4向ScottPlot5迁移 干货满满1.背景2.需求的引出3.先说结论1.好消息2.坏消息 4.迁移的部分笔记ColorScottPlot.PlottableScottPlot.Plottables中的对象如何定义添加 ScottPlot.Plottable.ScatterPlot 对象ScatterPolygonMarker也类似 Scatter的marker formsPlot1Re…

Github优质项目推荐(第九期)

文章目录 Github优质项目推荐&#xff08;第九期&#xff09;一、【tldraw】&#xff0c;37.1k stars - 在 React 中创建无限画布体验的库二、【zapret】&#xff0c;9.1k stars - 独立&#xff08;无需第三方服务器&#xff09;DPI 规避工具三、【uBlock】&#xff0c;48.3k s…

学习threejs,THREE.PlaneGeometry 二维平面几何体

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️HREE.PlaneGeometry 二维平…

力扣矩阵-算法模版总结

lc-73.矩阵置零-(时隔14天)-12.27 思路&#xff1a;(23min22s) 1.直接遍历遇0将行列设0肯定不行&#xff0c;会影响后续判断&#xff0c;题目又要求原地算法&#xff0c;那么进一步考虑是否可以将元素为0&#xff0c;其行列需要设为0的位置给存储下来&#xff0c;最后再遍历根据…

面向对象的设计原则与设计模式

目的 设计模式的目的是提高代码的重用性&#xff0c;可读性、可扩展性、可靠性&#xff0c;使程序呈现高内聚&#xff0c;低耦合的特性 原则 单一职责原则 假设有一个class负责两个职责&#xff0c;一旦发生需求变更&#xff0c;修改其中一个职责的逻辑代码&#xff0c;有可能…

网络安全词云图与技术浅谈

网络安全词云图与技术浅谈 一、网络安全词云图生成 为了直观地展示网络安全领域的关键术语&#xff0c;我们可以通过词云图&#xff08;Word Cloud&#xff09;的形式来呈现。词云图是一种数据可视化工具&#xff0c;它通过字体大小和颜色的差异来突出显示文本中出现频率较高…

Jsonlizer,一个把C++各类数据转成 Json 结构体的玩意儿

这段时间突发奇想&#xff0c;觉得可以弄一个Json和C各种数据类型互转的工具&#xff0c;因为Json在进行数据储存的时候&#xff0c;有一些先天的优势&#xff0c;传统的C的序列化方式是将数据序列化到流数据里面&#xff0c;而流数据是典型的串行结构&#xff08;或则说是一维…

删除拼排序链表中的重复元素(最优解)

题目来源 82. 删除排序链表中的重复元素 II - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head…

OpenHarmony-5.PM 子系统(2)

电池服务组件OpenHarmony-4.1-Release 1.电池服务组件 Battery Manager 提供了电池信息查询的接口&#xff0c;同时开发者也可以通过公共事件监听电池状态和充放电状态的变化。电池服务组件提供如下功能&#xff1a; 电池信息查询。充放电状态查询。关机充电。 电池服务组件架…

深入浅出 Linux 操作系统

深入浅出 Linux 操作系统 引言 在当今数字化的时代&#xff0c;Linux 操作系统无处不在。从支撑互联网巨头庞大的数据中心&#xff0c;到嵌入智能家居设备的微型芯片&#xff0c;Linux 都发挥着关键作用。然而&#xff0c;对于许多人来说&#xff0c;Linux 仍笼罩着一层神秘的…

uniapp 文本转语音

uniapp 文本转语音 基于 Minimax API 的 UniApp 文本转语音工具&#xff0c;支持文本分段、队列播放、暂停恢复等功能。目前只内置了 Minimax文本转语音Minimax 的语音生成技术以其自然、情感丰富和实时性强而著称 API_KEY、GroupId 获取方法 https://platform.minimaxi.com…

前端图像处理(二)

目录 一、上传 1.1、文件夹上传以及进度追踪 1.2、拖拽上传 1.3、图片裁剪上传原理 二、图片布局 2.1、渐进式图片 2.2、图片九宫格 2.3、轮播图(Js) 2.3.1、3D动画轮播图 2.3.2、旋转切换的轮播图 2.4、卡片移入翻转效果 2.5、环绕式照片墙 一、上传 1.1、文件夹…

3.BMS系统原理图解读

一、BMS电池板 (1)电池的连接关系&#xff1a;串联 (2)采样控制点&#xff1a;CELL0 - CELL5 (3)端子P1和P3&#xff1a;BAT和BAT- (4)开关S1&#xff1a;控制充放电回路的机械开关 二、BMS控制板 (1)主控MCU 电源 复位 晶振 (2)LED指示灯&#xff1a;4电量指示 1调试指…

用于汽车碰撞仿真的 Ansys LS-DYNA

使用 Ansys LS-DYNA 进行汽车碰撞仿真汽车碰撞仿真 简介 汽车碰撞仿真是汽车设计和安全工程的一个关键方面。这些仿真使工程师能够预测车辆在碰撞过程中的行为&#xff0c;从而有助于改进安全功能、增强车辆结构并符合监管标准。Ansys LS-DYNA 是一款广泛用于此类仿真的强大工具…