<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: PagePing Support</title>
    <description>The latest articles on DEV Community by PagePing Support (@pagepingapp).</description>
    <link>https://dev.to/pagepingapp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4213373%2Fd456e277-474b-4477-8d44-6dbf7bae63b2.png</url>
      <title>DEV Community: PagePing Support</title>
      <link>https://dev.to/pagepingapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pagepingapp"/>
    <language>en</language>
    <item>
      <title>How I made web-table copying survive rowspan and colspan (and shipped it as a Chrome extension)</title>
      <dc:creator>PagePing Support</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:26:41 +0000</pubDate>
      <link>https://dev.to/pagepingapp/how-i-made-web-table-copying-survive-rowspan-and-colspan-and-shipped-it-as-a-chrome-extension-5fhl</link>
      <guid>https://dev.to/pagepingapp/how-i-made-web-table-copying-survive-rowspan-and-colspan-and-shipped-it-as-a-chrome-extension-5fhl</guid>
      <description>&lt;p&gt;I kept running into the same annoyance: select a table on a web page, paste it into a spreadsheet, and watch the columns drift. Merged header cells shift everything one column to the right, and numbers arrive as text. So I built a small Chrome extension, &lt;a href="https://pagepingapp.com/tablegrab/" rel="noopener noreferrer"&gt;TableGrab&lt;/a&gt;, to fix it. Here is what I learned along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The problem: walking cells in order misaligns columns
&lt;/h2&gt;

&lt;p&gt;The naive approach is to loop over each &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;, then each &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;, and push the text into a 2D array. That works until a cell has &lt;code&gt;rowspan&lt;/code&gt; or &lt;code&gt;colspan&lt;/code&gt;. A cell with &lt;code&gt;rowspan="2"&lt;/code&gt; in row 1 occupies a slot in row 2 too, but row 2's &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt; list does not mention it. Every cell after it lands one column too far left.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The fix: an occupancy grid
&lt;/h2&gt;

&lt;p&gt;Instead of pushing cells in order, keep a grid of occupied slots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tableToGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// first free slot&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowSpan&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colSpan&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each cell is placed at its first free (row, column), and then its whole span is filled. Filling the span with the cell text (rather than leaving blanks) means columns line up and filters and sorting still work in the spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Detecting ARIA grids and div grids
&lt;/h2&gt;

&lt;p&gt;Plenty of modern "tables" are not &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; at all. Two common cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ARIA grids: elements with &lt;code&gt;role="grid"&lt;/code&gt; or &lt;code&gt;role="table"&lt;/code&gt;, containing &lt;code&gt;role="row"&lt;/code&gt; and &lt;code&gt;role="gridcell"&lt;/code&gt; / &lt;code&gt;role="columnheader"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Div grids: repeated rows of sibling elements with the same number of children and a consistent layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I treat the first as straightforward (the roles tell you the structure) and the second as a heuristic, so it is worth showing a preview before copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. CSV gotchas
&lt;/h2&gt;

&lt;p&gt;If you export CSV, three things bite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Quoting: wrap fields containing commas, quotes or newlines in double quotes, and double any embedded quotes.&lt;/li&gt;
&lt;li&gt;Excel and UTF-8: Excel often misreads UTF-8 CSV unless the file starts with a byte order mark (&lt;code&gt;\uFEFF&lt;/code&gt;). Add it and accented names stop turning into garbage.&lt;/li&gt;
&lt;li&gt;Formula injection: a cell starting with &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;@&lt;/code&gt; can be interpreted as a formula when opened in Excel. Prefix such cells with a single quote or apostrophe so they are treated as text.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. MV3 permissions: activeTab only
&lt;/h2&gt;

&lt;p&gt;For a tool that reads page content, I wanted the smallest permission footprint. With Manifest V3 and &lt;code&gt;activeTab&lt;/code&gt;, the extension can only touch a page after the user clicks it. There is no broad host access, no account, and nothing is uploaded. It is a nice side effect that the permission prompt is also easy to explain to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Try it
&lt;/h2&gt;

&lt;p&gt;TableGrab is free for tables up to 100 rows, and Pro is a one-off US$15 (unlimited rows, .xlsx export and more). Details and the Chrome Web Store link are at &lt;a href="https://pagepingapp.com/tablegrab/" rel="noopener noreferrer"&gt;https://pagepingapp.com/tablegrab/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have a table that still breaks it, I would like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
