user3413723
user3413723

Reputation: 12263

Get bounding box of div split by column-count in chromium/puppeteer

When I have a column layout like the following, chrome only generates one bounding box for both columns, and the bounding box is the height that both would be if they weren't in a column. I want to get bounding boxes for both columns separately.

Take a layout like this:

.container{
  column-count: 2;
}
<div class="container">
  <h1>Hello</h1>
  <span>text text text texttext text text text text text text text text text text text text text text text text text text text text texttext texttext text text text text text text text text text text text text text text text text text text text text text text text text text text text text texttext text text texttext texttext text text text text text text text</p>
</span>

Produces text in two columns. This will split up the span into two parts, the part in the left column, and the part in the right column. How can I get the bounding box of only the left part, and the bounding box of only the right part? Then what if it was split into 3 columns?

getBoundingClientRect() only returns the bounding rect as though it were all in one column.

Upvotes: 1

Views: 905

Answers (1)

Kaiido
Kaiido

Reputation: 137084

You can use DOMRange's getBoundingClientRect, which unlike Element's one is able to get the BBox of rendered text directly.

The basic idea is to select each character from the text-content of your element and then check its BBox to compose your columns' BBox manually.

The first idea to check when a new column appeared is to verify if the current character's top position is below the previous one, however this will fail in case of single line elements (that at least Firefox and Safari produce).
So to circumvent this, it seems better to rather check the distance between the right of the previous character and the left of the next one. It's sometime more than 1, so this solution is not perfect either as it requires some empiric value to be used, but it's still the one that seems to work the best based on the few tests I could do.

function getRenderedColumns( node ) {
  // we only deal with TextNodes
  if( !node || !node.parentNode || node.nodeType !== 3 ) {
    return [];
  }
  // our Range object form which we'll get the characters positions
  const range = document.createRange();
  // here we'll store all our columns
  const columns = [];
  // begin at the first character
  range.setStart( node, 0 );

  let str = node.textContent;
  let { top, right, bottom, left } = range.getBoundingClientRect();
  let lastRight = right;
  
  let current = 1; // we already have the first chars's rect

  // iterate over all characters
  while( current <= str.length ) {
    // move our cursor
    range.setStart( node, current );
    if( current < str.length - 1 ) {
      range.setEnd( node, current + 1 ); // wrap it (for Chrome...)
    }
    // get the BBox of this character
    const rect = range.getBoundingClientRect();
    // if we moved by more than 10px on x
    const new_column = rect.left - lastRight > 10;
    lastRight = rect.right;
    if( new_column ) {
      columns.push( {
        top,
        left,
        width: right - left,
        height: bottom - top
      } );
      top = rect.top;
      right = rect.right;
      bottom = rect.bottom;  
      left = rect.left;
    }
    else { // extend our column's rect
      bottom = Math.max( bottom, rect.bottom );
      right = Math.max( right, rect.right );
    }
    current++;
  }
  // push the last column
  columns.push( {
    top,
    left,
    width: right - left,
    height: bottom - top
  } );

  return columns;
}

function test( selector, color ) {
  const target = document.querySelector( selector );
  // note we use the TextNode as input
  const rects = getRenderedColumns( target.childNodes[0] ); 
  console.log( selector );
  console.log( rects );
  // make a visible marker for these rects
  rects.forEach(  rect => {
    const marker = document.createElement( 'div' );
    marker.classList.add( 'position-marker' );
    const style = marker.style;
    Object.entries( rect ).forEach( ([ key, val ]) => 
      style[ key ] = val + 'px'
    );
    style.borderColor = color;
    document.body.append( marker );
  } );
}

test( '.col-2 > span', 'red' );
test( '.single-line', 'green' );
test( '.col-6 > span', 'blue' );
.col-2{
  column-count: 2;
}
.col-6 {
  column-count: 6;
}
.single-line {
  width: 250px;
}

.position-marker {
  border: 1px solid;
  position: absolute;
  pointer-events: none;
}
div[class^=col-] {
  margin-bottom: 12px;
}
body { margin-bottom: 102px; }
body > .as-console-wrapper { max-height: 90px; }
<div class="col-2">
  <h1>Hello</h1>
  <span>text text text texttext text text text text text text text text text text text text text text text text text text text text texttext texttext text text text text text text text text text text text text text text text text text text text text text text text text text text text text texttext text text texttext texttext text text text text text text text
</span>
</div>
<div class="col-2 single-line">single line test for Firefox and Safari</div>
<div class="col-6">
  <span>text text text texttext text text text text text text text text text text text text text text text text text text text text texttext texttext text text text text text text text text text text text text text text text text text text text text text text text text text text text text texttext text text texttext texttext text text text text text text text
</span>
</div>

Upvotes: 2

Related Questions