forked from sheetjs/docs.sheetjs.com
		
	
		
			
	
	
		
			269 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			269 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|  | --- | ||
|  | title: Merged Cells | ||
|  | sidebar_position: 11 | ||
|  | --- | ||
|  | 
 | ||
|  | <details> | ||
|  |   <summary><b>File Format Support</b> (click to show)</summary> | ||
|  | 
 | ||
|  | By default, no cells are merged. Merge metadata is ignored when exporting to a | ||
|  | file format that does not support merge cells. | ||
|  | 
 | ||
|  | Formats store the actual contents of a merged cell in the "top-left" corner | ||
|  | (first row and first column of the merge range). Some formats can hold data for | ||
|  | cells that are covered by the merge range. | ||
|  | 
 | ||
|  | | Formats   | Merge | Covered | | ||
|  | |:----------|:-----:|:-------:| | ||
|  | | XLSX/XLSM |   ✔   |    ✔    | | ||
|  | | XLSB      |   ✔   |    ✔    | | ||
|  | | XLML      |   ✔   |    ✔    | | ||
|  | | BIFF8 XLS |   ✔   |    ✔    | | ||
|  | | ODS/FODS  |   ✔   |    ✔    | | ||
|  | | NUMBERS   |   ✔   |    ✔    | | ||
|  | | HTML      |   ✔   |         | | ||
|  | 
 | ||
|  | There are multiple representations of merge cells in the NUMBERS file format. | ||
|  | Writers use the simplified `.TST.MergeRegionMapArchive` representation. Parsers | ||
|  | understand the classic form and the modern `.TST.MergeOwnerArchive` form. | ||
|  | 
 | ||
|  | </details> | ||
|  | 
 | ||
|  | Modern spreadsheet software typically allow users to combine blocks of cells | ||
|  | into a single unit. This unit can span multiple columns and rows. As shown in | ||
|  | the following table, HTML TH and TD elements use `colspan` and `rowspan` | ||
|  | attributes to effectuate merging: | ||
|  | 
 | ||
|  | <table><tbody> | ||
|  |   <tr><td colSpan="4"><center>This title spans four columns</center></td></tr> | ||
|  |   <tr><td>SheetJS</td><td>supports</td><td>merge</td><td>cells</td></tr> | ||
|  | </tbody></table> | ||
|  | 
 | ||
|  | :::tip pass | ||
|  | 
 | ||
|  | This feature was expanded in version `0.20.3`. It is strongly recommended to | ||
|  | [upgrade to the latest version](/docs/getting-started/installation/). | ||
|  | 
 | ||
|  | ::: | ||
|  | 
 | ||
|  | ## Storage
 | ||
|  | 
 | ||
|  | The `!merges` property of the worksheet object is expected to be an array of | ||
|  | [SheetJS range objects](/docs/csf/general#sheetjs-range). Each range object | ||
|  | corresponds to a merged range in the worksheet. | ||
|  | 
 | ||
|  | The following snippet creates a merge range spanning `A1:B2` : | ||
|  | 
 | ||
|  | ```js title="Merge the range A1:B2 in a worksheet" | ||
|  | ws["!merges"] = [ | ||
|  |   { s: { c: 0, r: 0 }, e: { c: 1, r: 1 } }  // A1:B2 | ||
|  | ]; | ||
|  | ``` | ||
|  | 
 | ||
|  | :::caution pass | ||
|  | 
 | ||
|  | **Overlapping merges are not automatically detected!** | ||
|  | 
 | ||
|  | ::: | ||
|  | 
 | ||
|  | ### Range
 | ||
|  | 
 | ||
|  | The [`decode_range`](/docs/csf/general#cell-ranges-1) method creates range | ||
|  | objects from A1-style range strings. | ||
|  | 
 | ||
|  | The following snippet creates a merge range spanning `A1:B2` : | ||
|  | 
 | ||
|  | ```js title="Merge the range A1:B2 in a worksheet" | ||
|  | ws["!merges"] = [ | ||
|  |   XLSX.utils.decode_range("A1:B2") | ||
|  | ]; | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Overlap
 | ||
|  | 
 | ||
|  | When adding merges to an existing workbook, it is strongly recommended to scan | ||
|  | the merges array and test for collisions: | ||
|  | 
 | ||
|  | ```js title="Add a merged range to a worksheet" | ||
|  | function sheet_add_merge(ws, range) { | ||
|  |   /* if `range` is a string, parse into a range object */ | ||
|  |   var merge = typeof range == "string" ? XLSX.utils.decode_range(range) : range; | ||
|  | 
 | ||
|  |   /* create array merge if it does not exist */ | ||
|  |   if(!ws["!merges"]) ws["!merges"] = []; | ||
|  | 
 | ||
|  |   /* check if the new merge collides with any existing merge */ | ||
|  |   ws["!merges"].forEach(function(range) { | ||
|  |     if(merge.e.r < range.s.r) return; | ||
|  |     if(range.e.r < merge.s.r) return; | ||
|  |     if(merge.e.c < range.s.c) return; | ||
|  |     if(range.e.c < merge.s.c) return; | ||
|  |     throw new Error(XLSX.utils.encode_range(merge)+" overlaps "+XLSX.utils.encode_range(range)); | ||
|  |   }); | ||
|  | 
 | ||
|  |   /* add merge */ | ||
|  |   ws["!merges"].push(merge); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Cells
 | ||
|  | 
 | ||
|  | Spreadsheet tools will store and use the top-left cell of a merge range. For | ||
|  | example, if the range `B2:C5` is merged, the cell corresponding to the range | ||
|  | will be stored in the worksheet in cell `B2`. | ||
|  | 
 | ||
|  | #### Covered Cells
 | ||
|  | 
 | ||
|  | Spreadsheet tools can store cells that are covered by a merged cell. | ||
|  | 
 | ||
|  | The [SheetJS worksheet object](/docs/csf/sheet) can store covered cells. | ||
|  | [API Functions](#functions) may omit or include covered cells. | ||
|  | 
 | ||
|  | ## Live Demo
 | ||
|  | 
 | ||
|  | This example generates a worksheet that matches the following screenshot: | ||
|  | 
 | ||
|  |  | ||
|  | 
 | ||
|  | The merge ranges are `A1:B2`, `C1:C2`, `A3:B3`, `D1:D2`, and `A4:B4`. | ||
|  | 
 | ||
|  | ```jsx live | ||
|  | function SheetJSMergeCellsExport() { return (<button onClick={() => { | ||
|  |   /* write data to the top-left corner of each range */ | ||
|  |   var ws = XLSX.utils.aoa_to_sheet([ | ||
|  |     ["A1:B2",  /* B1 */, "C1:C2",  "Separate blocks"], // row 1 | ||
|  |     [],                                                // row 2 | ||
|  |     ["A3:B3",  /* B3 */, "C3"],                        // row 3 | ||
|  |     ["... are merged separately"],                     // row 4 | ||
|  |   ]); | ||
|  |   /* add merges */ | ||
|  |   ws["!merges"] = [ | ||
|  |     { s: { c: 0, r: 0 }, e: { c: 1, r: 1 } },  // A1:B2 | ||
|  |     { s: { c: 2, r: 0 }, e: { c: 2, r: 1 } },  // C1:C2 | ||
|  |     { s: { c: 0, r: 2 }, e: { c: 1, r: 2 } },  // A3:B3 | ||
|  |     { s: { c: 3, r: 0 }, e: { c: 3, r: 1 } },  // D1:D2 | ||
|  |     { s: { c: 0, r: 3 }, e: { c: 1, r: 3 } }   // A4:B4 | ||
|  |   ]; | ||
|  |   /* export to XLSX */ | ||
|  |   var wb = XLSX.utils.book_new(ws, "Merges"); | ||
|  |   XLSX.writeFile(wb, "SheetJSMergeCells.xlsx"); | ||
|  | }}><b>Click here to Export</b></button>); } | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Functions
 | ||
|  | 
 | ||
|  | #### HTML
 | ||
|  | 
 | ||
|  | [`table_to_sheet` and `table_to_book`](/docs/api/utilities/html#html-table-input) | ||
|  | will generate worksheets that include merged ranges: | ||
|  | 
 | ||
|  | <details> | ||
|  |   <summary><b>Live Demo</b> (click to show)</summary> | ||
|  | 
 | ||
|  | ```jsx live | ||
|  | function SheetJSDOMMergedCells() { | ||
|  |   const ref = React.useRef(null); | ||
|  |   const [ merges, setMerges ] = React.useState([]); | ||
|  | 
 | ||
|  |   React.useEffect(() => { | ||
|  |     if(ref.current) { | ||
|  |       const tbl = ref.current.getElementsByTagName("TABLE"); | ||
|  |       if(!tbl || !tbl[0]) return; | ||
|  |       const ws = XLSX.utils.table_to_sheet(tbl[0]); | ||
|  |       console.log(ws["!merges"]) | ||
|  |       setMerges(ws["!merges"] || []); | ||
|  |     } | ||
|  |   }, [ref]) | ||
|  |   const ws = XLSX.utils.aoa_to_sheet([ | ||
|  |     ["A1:B1 is merged", "This cell is covered" ], | ||
|  |     ["A2 is not merged", "B2 is not merged"] | ||
|  |   ]); | ||
|  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | ||
|  |   const __html = XLSX.utils.sheet_to_html(ws); | ||
|  | 
 | ||
|  |   return ( <> | ||
|  |     <b>Table:</b> | ||
|  |     <div ref={ref} dangerouslySetInnerHTML={{__html}}/> | ||
|  |     <b>Merges:</b> | ||
|  |     <pre>{merges ? merges.map(m =>  XLSX.utils.encode_range(m)).join("\n") : ""}</pre> | ||
|  |   </> | ||
|  |   ); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | </details> | ||
|  | 
 | ||
|  | [`sheet_to_html`](/docs/api/utilities/html#html-table-output) will generate HTML | ||
|  | strings that use `colspan` and `rowspan` for merged ranges: | ||
|  | 
 | ||
|  | <details> | ||
|  |   <summary><b>Live Demo</b> (click to show)</summary> | ||
|  | 
 | ||
|  | ```jsx live | ||
|  | function SheetJSHTMLMergedCells() { | ||
|  |   const ws = XLSX.utils.aoa_to_sheet([ | ||
|  |     ["A1:B1 is merged", "This cell is covered" ], | ||
|  |     ["A2 is not merged", "B2 is not merged"] | ||
|  |   ]); | ||
|  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | ||
|  |   const __html = XLSX.utils.sheet_to_html(ws); | ||
|  |   return ( <div dangerouslySetInnerHTML={{__html}}/> ); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | </details> | ||
|  | 
 | ||
|  | #### Reading Files
 | ||
|  | 
 | ||
|  | [`read` and `readFile`](/docs/api/parse-options) will extract merge metadata | ||
|  | from supported files. | ||
|  | 
 | ||
|  | #### Writing Files
 | ||
|  | 
 | ||
|  | [`write` and `writeFile`](/docs/api/write-options) will attempt to write merge | ||
|  | metadata when exporting to file formats that support merged ranges. | ||
|  | 
 | ||
|  | When writing to CSV and other formats that do not support merged ranges, every | ||
|  | cell in the range will be exported. This includes covered cells! | ||
|  | 
 | ||
|  | <details> | ||
|  |   <summary><b>Live Demo</b> (click to show)</summary> | ||
|  | 
 | ||
|  | ```jsx live | ||
|  | function SheetJSCSVMergedCells() { | ||
|  |   const ws = XLSX.utils.aoa_to_sheet([ | ||
|  |     ["A1:B1 is merged", "This cell is covered" ], | ||
|  |     ["A2 is not merged", "B2 is not merged"] | ||
|  |   ]); | ||
|  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | ||
|  |   const wb = XLSX.utils.book_new(ws, "Sheet1"); | ||
|  |   const csv = XLSX.write(wb, { type: "string", bookType: "csv"}); | ||
|  |   return ( <pre>{csv}</pre> ); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | </details> | ||
|  | 
 | ||
|  | #### Exporting Data
 | ||
|  | 
 | ||
|  | [`sheet_to_csv`](/docs/api/utilities/csv#delimiter-separated-output) and | ||
|  | [`sheet_to_json`](/docs/api/utilities/array#array-output) do not support merged | ||
|  | ranges. The exports will include covered cells: | ||
|  | 
 | ||
|  | <details> | ||
|  |   <summary><b>Live Demo</b> (click to show)</summary> | ||
|  | 
 | ||
|  | ```jsx live | ||
|  | function SheetJSAOAMergedCells() { | ||
|  |   const ws = XLSX.utils.aoa_to_sheet([ | ||
|  |     ["A1:B1 is merged", "This cell is covered" ], | ||
|  |     ["A2 is not merged", "B2 is not merged"] | ||
|  |   ]); | ||
|  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | ||
|  |   const aoa = XLSX.utils.sheet_to_json(ws, {header:1}); | ||
|  |   return ( <pre>{JSON.stringify(aoa,2,2)}</pre> ); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | </details> |