资讯详情

资讯详情

Handsontable 合并单元格(Merge Cells)完整指南:配置、API、虚拟化与边界行为

前端UI组件【免费下载链接】handsontableJavaScript Data Grid / Data Table with a Spreadsheet Look Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡项目地址https://gitcode.com/gh_mirrors/ha/handsontable点击查看免费下载导读合并单元格是表格类应用中高频使用的展示能力把相邻的多个单元格合并为一个跨行跨列的独立区域用于表头分组、脚注说明或数据归并。Handsontable 的 Merge Cells 插件提供了完整的合并能力——既可以通过配置声明预设合并也可以由用户通过CtrlM快捷键或右键菜单即时合并还暴露了merge()/unmerge()API 与四个生命周期钩子供开发者编程控制。读完本文你将掌握该插件的全部配置项与 API 用法、超大合并区域的虚拟化渲染优化、合并对底层数据与视图查询的影响以及它在行列重排、过滤、隐藏等边界场景下的精确行为。合并单元格的核心行为合并的核心是将两个或更多相邻单元格合并为一个跨越若干行、若干列的单元格。Handsontable 采用与 Microsoft Excel 完全一致的数据语义合并后只保留选区左上角的单元格值其余被覆盖单元格的值被清空。这个清空动作发生在底层数据中而不仅仅是屏幕显示层面——也就是说getData()读到的数据同样是清空后的结果详见下文 对底层数据的影响。从实现上看插件在 handsontable/src/plugins/mergeCells/mergeCells.ts 中注册了beforeMergeCells、afterMergeCells、beforeUnmergeCells、afterUnmergeCells四个钩子见该文件 L25-L28并维护一个MergedCellsCollection容器管理所有合并区域。如何合并单元格通过配置声明预设合并启用合并单元格功能只需将mergeCells选项设为truemergeCells: truemergeCells: true只启用能力允许用户通过快捷键/菜单/API 合并不会预置任何合并。要初始化时就有合并区域则以数组形式提供合并细节数组中每个元素描述一个合并区域mergeCells: [ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ]每个对象包含四个属性属性含义row合并区域起始行的视觉索引visual indexcol合并区域起始列的视觉索引rowspan跨行数向下延伸含起始行colspan跨列数向右延伸含起始列row与col使用视觉索引即它们在网格中显示出来的位置坐标与selectCell()、getCellMeta()使用同一套坐标系。注意这与底层数据的物理索引是两回事当行/列被过滤、排序或移动时视觉索引会变化而插件内部通过物理锚点MergeAnchor跟踪合并区域真正覆盖的物理行再在每次索引映射变化时重新推导视觉坐标见 mergeCells.ts 中#reanchorMergesToVisibleRows的实现L1734-L1806。在 React 中mergeCells以 prop 形式传入HotTable data{data} mergeCells{[ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ]} /在 Angular 中配置写在 settings 对象里settings { mergeCells: [ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ], };在 Vue 3 中同样通过 settings 传入const hotSettings { mergeCells: [ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ], };一个可直接运行的 JavaScript 完整示例含 100 行 × 50 列的模拟数据与右键菜单见 example1.js 与对应的 TypeScript 版本 example1.ts各框架版本位于 react/example1.jsx、angular/example1.ts、vue/example1.vue。配置校验无效声明会被拒绝并非任意声明都会生效。插件在 cellCoords.ts 与validateSetting()mergeCells.ts L512-L539中实现了四类校验不合法的合并区域会被拒绝并打印警告不会加入集合包含负值row、col、rowspan、colspan任一为负 → contains negative values, which is not supported。越界合并区域整体或部分超出表格行列范围 → is positioned (or positioned partially) outside of the table range。单格rowspan 1 colspan 1本质只是一个单元格 → makes it a single cell. It cannot be added to the collection.零跨度rowspan或colspan为 0 → has rowspan or colspan declared as 0, which is not supported。另外两个互相重叠的合并区域中后声明的会被filterOverlappingMergeCells过滤掉。优化宽/高合并单元格的渲染当合并区域跨越数千行或数千列时滚动体验会比普通单元格慢。原因是默认情况下只要合并区域碰到视口边缘渲染范围就会被扩展以容纳整个合并区域详见下文 对 viewport getter 方法的影响跨越范围越大一次渲染的单元格越多。为此插件提供了针对合并单元格的虚拟化模式默认关闭。开启方式是在mergeCells对象中设置virtualized: true同时合并区域改用cells数组声明mergeCells: { virtualized: true, cells: [{ row: 1, col: 1, rowspan: 200, colspan: 2 }] }开启后超出视口的合并区域部分不再被渲染渲染范围也不再为容纳合并区域而扩展。从源码看#onAfterViewportRowCalculatorOverride/#onAfterViewportColumnCalculatorOverridemergeCells.ts L2368-L2451在virtualized为true时直接返回跳过视口扩展逻辑同时#onModifyGetCellCoordsL2153-L2196在source render且启用虚拟化时把返回的坐标裁剪到当前已渲染范围内clamp到首末渲染行列之间。DEFAULT_SETTINGSL169-L177给出了该选项的默认形态virtualized: falsecells: []。也就是说mergeCells: true展开后的默认配置等价于{ virtualized: false, cells: [] }。建议同时增大渲染缓冲区以减少滚动时的闪烁。与渲染缓冲相关的配置有viewportRowRenderingOffset/viewportColumnRenderingOffset视口之外额外渲染的行/列数缓冲区大小。viewportRowRenderingThreshold/viewportColumnRenderingThreshold触发重新计算视口范围的边界阈值。一个结合虚拟化与缓冲区配置的完整示例500 列、跨 498 列的合并区域见 example2.js其中设置了viewportColumnRenderingOffset: 15与viewportColumnRenderingThreshold: 5mergeCells: { virtualized: true, cells: [{ row: 1, col: 1, rowspan: 3, colspan: 498 }], }, viewportColumnRenderingOffset: 15, viewportColumnRenderingThreshold: 5,响应合并与取消合并事件当单元格被合并或取消合并时插件会触发四个钩子用于在业务层执行自定义逻辑钩子触发时机参数beforeMergeCells合并即将执行时cellRange受影响的选区afterMergeCells合并完成后cellRange、mergeParent合并结果对象beforeUnmergeCells取消合并即将执行时cellRangeafterUnmergeCells取消合并完成后cellRange其中afterMergeCells额外收到的mergeParent对象包含合并区域的完整几何信息{ row, col, rowspan, colspan }。这四个钩子由插件在 mergeCells.ts L25-L28 统一注册并在mergeRange()L711-L780触发beforeMergeCells/afterMergeCells与unmergeRange()L814-L830触发beforeUnmergeCells/afterUnmergeCells中调用。下面的示例在每次合并/取消合并时无论通过右键菜单还是CtrlM快捷键向页面输出一条日志完整代码见 example3.jsbeforeMergeCells(cellRange) { logEvent(beforeMergeCells: rows ${cellRange.from.row}-${cellRange.to.row}, columns ${cellRange.from.col}-${cellRange.to.col}.); }, afterMergeCells(cellRange, mergeParent) { logEvent(afterMergeCells: merged into ${mergeParent.rowspan} row(s) by ${mergeParent.colspan} column(s).); }, beforeUnmergeCells(cellRange) { logEvent(beforeUnmergeCells: rows ${cellRange.from.row}-${cellRange.to.row}, columns ${cellRange.from.col}-${cellRange.to.col}.); }, afterUnmergeCells(cellRange) { logEvent(afterUnmergeCells: rows ${cellRange.from.row}-${cellRange.to.row}, columns ${cellRange.from.col}-${cellRange.to.col}.); },以编程方式合并与取消合并不需要用户操作时可以调用 MergeCells 插件的merge()与unmerge()方法。两个方法都接收四个视觉索引参数startRow、startColumn、endRow、endColumn与selectCell()使用同一坐标系hot.getPlugin(mergeCells).merge(startRow, startColumn, endRow, endColumn); hot.getPlugin(mergeCells).unmerge(startRow, startColumn, endRow, endColumn);从源码看merge()mergeCells.ts L949-L954与unmerge()L966-L971内部基于这四个坐标构建CellRange后委托给mergeRange()/unmergeRange()因此通过 API 合并与通过 UI、配置合并的数据清空行为完全一致详见下一节。一个典型场景是脚注行用一个按钮把某一行跨所有列合并为脚注再用另一个按钮取消。完整示例见 example4.jsmergeButton.addEventListener(click, () { hot.getPlugin(mergeCells).merge(5, 0, 5, 3); }); unmergeButton.addEventListener(click, () { hot.getPlugin(mergeCells).unmerge(5, 0, 5, 3); });对底层数据的影响合并并不只是视觉上盖住被覆盖的单元格——它会清空底层数据中这些单元格的值。无论是通过 UI、mergeCells配置还是merge()方法合并行为一致合并区域只有左上角单元格保留原值其余所有被覆盖的单元格被设为null。例如在(0, 0)处合并一个rowspan: 2, colspan: 2的区域后hot.getData(); // - [[Top-left value, null], [null, null]] hot.getSourceData(); // - [[Top-left value, null], [null, null]] (the same)getData()与getSourceData()对被覆盖单元格都返回null因为清空动作走的是正常的变更管线它会触发beforeChange与afterChange且source为MergeCells。你可以利用这个source值在beforeChange/afterChange中区分合并触发的清空与普通编辑。在 generateFromSettings()L551-L599中可以看到实现细节插件把需要清空的[row, col, null]变更收集起来通过setDataAtCell(populatedNulls, undefined, undefined, this.pluginName)一次写入this.pluginName即为MergeCells。取消合并不会恢复被清空的值。如果后续需要原始数据请在合并前自行保存副本或在beforeUnmergeCells/afterUnmergeCells处理器中手工恢复。重新应用相同配置时的行为通过updateSettings()重新应用相同的mergeCells值时只会清空仍然持有值的单元格。如果某个区域的所有单元格已经是空值那么重新应用不会产生任何数据变更也就不会触发beforeChange/afterChange事件。但这里有一个前提清空写入必须真正到达数据层。如果你在beforeChange中返回false取消写入或使用了一个拒绝null的校验器同时allowInvalid为false被覆盖的单元格就会保留原值此后每一次重新应用都会再次尝试清空它们。这一点对框架包装器wrapper每次渲染都会重发全部选项的场景至关重要——React 和 Angular 包装器正是如此。假如某应用把beforeChange/afterChange事件写回自己的状态存储而重新应用又反复触发这些事件二者就会互相触发形成死循环。插件通过#appliedMergeKeys集合记录已应用过的合并区域键在重新应用时跳过仍然为空的单元格正是为了规避这个问题相关讨论可追溯至 issue #7555。除此之外清空行为没有任何变化一个区域第一次应用时仍会清空其覆盖的所有单元格——即使这些单元格本来就是空的。如果合并区域覆盖的范围内出现了新值——例如你传入了新的data或者排序、过滤、移动行把其他行带进了该区域——那么下一次重新应用会先清空它们然后保持安静hot.updateSettings({ data: [[SKU-4821, Stainless Steel Water Bottle], [SKU-0093, Wireless Mouse]], mergeCells: [{ row: 0, col: 0, rowspan: 2, colspan: 2 }], // unchanged }); hot.getDataAtCell(0, 1); // - null, cleared as usual在合并单元格上复制与粘贴多单元格粘贴会解除合并当粘贴的是一块多于一个单元格的数据并覆盖到合并区域时该合并区域会被解除所有粘贴的值都会显示出来。Excel 和 Google Sheets 行为一致一个自带行列结构的块无法塞进单个合并单元格因此合并让路。注意粘贴块触及到的所有合并区域都会被解除而不仅仅是你选中的那一个。因为粘贴会填充复制块与选中范围中较大的那个按轴分别取最大可能越过你的选区并切掉相邻的合并。从源码看插件在beforePaste中记录剪贴板是否为单格mergeCells.ts L2857-L2859在beforeChangesource 为CopyPaste.paste中收集粘贴将触及的合并区域快照L2885-L2898数据落库后在afterChange中调用#unmergeAfterPaste()L3003-L3026统一解除。合并几何信息会跟随粘贴自身的撤销条目一起记录因此执行undo()时粘贴的值与被解除的合并会作为一个整体被恢复而不是分两步。若校验器修正了粘贴值它每修正一个单元格会产生一个独立的撤销条目与普通写入一致这些条目只回滚各自的值不会影响合并区域。单值粘贴保留合并粘贴单个值不会破坏合并。单个值不携带任何结构它会落到合并区域左上角的单元格中被覆盖的单元格继续保持为空。实现上单值粘贴时插件会在beforeChange中把针对被覆盖单元格的变更逐条丢弃#discardChangesOnCoveredCellsL2959-L2973只保留对左上角单元格的写入。复制不携带合并一个值得注意的事实复制并不会把合并带过去。合并区域被复制时呈现为左上角值 空单元格粘贴到别处不会产生合并。粘贴 HTML 中的rowspan/colspan属性同样会被展平值落到跨区左上角单元格被覆盖单元格设为null。用 beforePaste 保住合并若想确保某个合并区域不被多格粘贴破坏可以在beforePaste中返回false取消整个粘贴——这样什么都不写入合并也不会被解除new Handsontable(container, { mergeCells: [{ row: 0, col: 0, rowspan: 2, colspan: 2 }], beforePaste(data, coords) { const isSingleValue data.length 1 data[0].length 1; if (isSingleValue) { return; // a single value never breaks a merge, so let it through } const clipboardRows data.length; const clipboardColumns Math.max(...data.map(row row.length)); // Measure the area the paste writes, not the selected area: a paste fills the larger of the // copied block and the selection on each axis, so it can reach past the selection. Scan every // cell of that area, because rowspan and colspan are set only on a merged ranges // top-left cell. const touchesMergedRange coords.some(({ startRow, startCol, endRow, endCol }) { const lastRow startRow Math.max(clipboardRows, endRow - startRow 1) - 1; const lastColumn startCol Math.max(clipboardColumns, endCol - startCol 1) - 1; for (let row startRow; row lastRow; row 1) { for (let col startCol; col lastColumn; col 1) { const { rowspan, colspan } this.getCellMetaTransient(row, col); if (rowspan 1 || colspan 1) { return true; } } } return false; }); if (touchesMergedRange) { return false; } }, });对 viewport getter 方法的影响存在合并单元格时渲染范围会扩展以容纳任何跨越视口边缘的合并单元格——这正是virtualized选项所关闭的行为。其结果是部分已渲染范围查询方法可能返回超出屏幕可见范围的索引getFirstRenderedVisibleRow()、getLastRenderedVisibleRow()、getFirstRenderedVisibleColumn()、getLastRenderedVisibleColumn()AutoRowSize.getFirstVisibleRow()与AutoRowSize.getLastVisibleRow()内部委托给渲染行查询AutoColumnSize.getFirstVisibleColumn()与AutoColumnSize.getLastVisibleColumn()内部委托给渲染列查询例如一个横跨第 0 到第 100 列的合并单元格会让getLastVisibleColumn()返回接近 100 的索引——即使视口中实际只显示了远少于 100 列。如果你需要的是真正可见的视口请使用完全可见 / 部分可见查询方法它们不受合并单元格扩展的影响getFirstFullyVisibleRow()、getLastFullyVisibleRow()、getFirstFullyVisibleColumn()、getLastFullyVisibleColumn()getFirstPartiallyVisibleRow()、getLastPartiallyVisibleRow()、getFirstPartiallyVisibleColumn()、getLastPartiallyVisibleColumn()另外要注意将virtualized设为true虽然也能移除范围扩展但它本质是性能选项——此时已渲染范围方法依然会把视口之外的缓冲行/列计算在内。视口扩展逻辑的具体实现见 mergeCells.ts 中的modifyViewportRowStart/End与modifyViewportColumnStart/EndL2386-L2510。行列重排与列冻结时的行为当合并区域所覆盖的行或列被重排时——通过manualColumnMove、manualRowMove或manualColumnFreeze——Handsontable 会让合并跟随移动到新的视觉位置。这一过程可能产生两个副作用自动拆分Auto-split如果移动把合并区域拦腰截断导致其底层单元格在新的视觉顺序中不再连续合并会被拆分为多个独立的合并区域每个连续段一个。跨轴跨度列移动时的rowspan、行移动时的colspan会保留在每个碎片上。单格碎片的静默丢弃拆分后任何退化为单格rowspan 1 colspan 1的碎片会被移除因为单格不再构成合并。被丢弃的碎片不会触发afterMergeCells钩子。移出视图的行仍计入跨度如果一个碎片只因为其余行被移出视图而显示为单格它会被保留并在那些行恢复显示时重新跨越它们。这对manualRowMove、manualColumnMove、manualColumnFreeze均成立。唯一例外是合并区域的行被排序拆散后的行移动见下一节。实现层面插件在beforeColumnMove/beforeRowMove/beforeColumnFreeze中先对每个合并区域做物理索引快照capturePhysicalSpans移动完成后通过translateAfterAxisMove把快照翻译到新的视觉顺序并拆分见 mergeCells.ts L2677-L2840。undo/redo只恢复行列顺序不恢复合并undo重放的是反向移动因此被拆分/丢弃的合并保持拆分/丢弃状态。如果需要原始合并区域请手动重新合并。合并内部的行被移出视图时的行为有些功能会把行从网格中彻底移除filters过滤、trimRows裁剪行、以及折叠nestedRows的父行。被移除的行在网格中没有位置因此覆盖它的合并区域跨度会变短合并区域会移动到其仍显示的第一行并且只跨越仍然显示的那些行。它永远不会向下覆盖到其下方的行。当它的所有行都不显示时合并区域完全不显示。当这些行恢复显示时合并区域会重新跨越它们。行离开期间合并信息不会丢失。但在行已被移出视图时创建的合并区域只覆盖你当时能看到的行其余行恢复后也不会扩张。当行移动拆分了合并区域、同时其中一些行被移出视图时每条被移除的行会跟随在网格行顺序中紧邻合并区域自身行的那个碎片这个顺序也就是这些行恢复显示时的顺序。被移除的行永远不会跨越合并区域未覆盖的行如果移动把这样的行插到合并区域自身行之间两侧的被移除行各自留在各自一侧无法归位的被移除行会丢失。排序拆散合并区域是例外被排序拆散的合并区域仍然绘制为一个连续块因此这个块会越过合并区域未覆盖的行此时行移动若打断该块系统无法判断哪个碎片拥有哪些行。每个碎片只能覆盖它显示的行被移出视图的行不会恢复。这种情况下单列合并区域会整个消失——因为它留下的每个碎片都是单格。在移动行之前先把排序列恢复原状或清除过滤器。hiddenRows隐藏行则完全不同隐藏行保留其位置因此跨越隐藏行的合并区域会保持其配置的rowspan只是绘制时占用更少的空间。undo在这里有一个限制取消合并一个除了一行外其余全部隐藏的合并区域时只会记录你看到的那个单格——它不是一个合并区域因此撤销这次取消合并不会恢复任何东西。如果希望取消合并可撤销请先展开/取消过滤这些行。合并单元格上的键盘导航合并单元格在导航上表现为位于其左上角的一个单元格当选择移动到合并单元格上方向键或鼠标点击整个合并单元格会被高亮。当你随后横向离开它非 Tab 的水平移动方向键、编辑器的方向键退出、或在enterMoves配置为横向步进时按Enter选择会落到合并单元格的顶行——无论你是从哪一行进入的。从下方进入再横向离开与从上方进入横向离开落在同一行从而保证横向导航的一致性。当顶行被隐藏时选择落在合并单元格最顶部的可见行。纵向导航保持你原本移动的列Tab/ShiftTab保持它们循环的行。二者均不受影响。从源码看这套逻辑由modifyTransformStart钩子mergeCells.ts L1899-L2003与FocusOrder模块focusOrder.ts协作实现横向离开时通过getNearestNotHiddenIndex(mergedParent.row, 1)把高亮行吸附到合并区域的顶行/最顶可见行。相关键盘快捷键WindowsmacOS动作ExcelSheetsCtrlM⌃M合并或取消合并选中的单元格✗✗该快捷键由插件在registerShortcuts()mergeCells.ts L1813-L1836中注册并带有守卫条件仅当未按下 Alt 键且当前选区可访问单元格内容canAccessCellContent时生效。注意 Mac 上某些系统会把右 Alt 触发为 AltCtrl 组合因此修饰键被显式校验。通过右键菜单合并启用contextMenu: true后MergeCells 插件会自动向右键菜单注入合并/取消合并条目见 mergeCells.ts 的#addMergeActionsToContextMenuL2203-L2210以及菜单项实现 toggleMerge.ts。菜单项是智能的当当前选区恰好等于一个合并区域时菜单显示取消合并Unmerge cells否则显示合并单元格Merge cells。当选中区域是单格或点击角部corner时菜单项被禁用。点击后对当前激活选区执行toggleMerge()若选区正好覆盖一个合并区域则取消合并否则合并该选区。总结合并单元格功能让 Handsontable 可以像 Excel 一样处理跨行跨列的数据展示。核心要点回顾启用与声明mergeCells: true启用能力数组形式声明预设合并坐标为视觉索引非法声明负值/越界/单格/零跨度会被拒绝。超大合并优化mergeCells: { virtualized: true, cells: [...] }开启合并虚拟化配合viewport*RenderingOffset/Threshold缓冲配置减少滚动闪烁。事件与 API四个合并/取消合并钩子 merge()/unmerge()方法覆盖全部编程场景。数据语义合并会真实清空底层数据source MergeCells取消合并不恢复重新应用相同配置只在有值可清时触发变更事件避免框架包装器的事件循环。边界行为多格粘贴解除合并、单值粘贴与复制不携带合并渲染范围查询会扩展而可见范围查询不会行/列移动会拆分合并且 undo 不恢复过滤/裁剪/折叠行与隐藏行对合并的影响机制完全不同。深入阅读建议MergeCells 插件完整实现、MergedCellCoords 坐标与校验类、MergedCellsCollection 容器、合并渲染器、焦点顺序模块以及插件的 e2e 测试集 handsontable/src/plugins/mergeCells/tests/。Microsoft 和 Excel 是 Microsoft Corporation 的注册商标。Google Sheets 是 Google LLC 的商标。赞分享前端UI组件【免费下载链接】handsontableJavaScript Data Grid / Data Table with a Spreadsheet Look Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡项目地址https://gitcode.com/gh_mirrors/ha/handsontable点击查看免费下载相关推荐gogcli gog docs table-merge命令行合并 Google Docs 原生表格单元格的完整指南gogcli gog docs table merge 命令行合并 Google Docs 原生表格单元格的完整指南 gog docs table mergeHandsontable 单元格移动Move Cells完整指南拖拽移动、公式联动与编程式操作Handsontable 单元格移动Move Cells完整指南拖拽移动、公式联动与编程式操作 本文基于 Handsontable 18.1.0 引入的前端UI组件gogcli 终端合并 Google Sheets 单元格gog sheets merge 命令完整指南gogcli 终端合并 Google Sheets 单元格gog sheets merge 命令完整指南 本指南围绕 gogcliGoogle Worksp上一篇5个写作痛点CuteMarkEd一站式解决Qt Markdown编辑器的超实用秘籍下一篇如何高效部署LaWGPT法律大模型从零开始的完整实战教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →