Kristián Stroka
Kristián Stroka

Reputation: 740

Split HTML into virtual pages

I have long content (multiple content with imgaes, lists, divs, text, etc...). I want to show this content to user as virtual PDF pages. I dont want to generate PDF, just show this HTML content as pages (defined by width/height) with same header/footer. It should look like in image bellow, as you can see on first page, I want to split that text and show in next page:

This is how it should looks like

I´m working this app on React. I dont know what will be in this content, every render will be different (with different content based on user activity).

Do you have any suggestions how to do this? (CSS solutions, or JS, or I dont know maybe some React lib ...)

Upvotes: 3

Views: 5388

Answers (2)

everetttttttttttt
everetttttttttttt

Reputation: 1

As an addition to the other answer by remix23, I was trying to do something along the same lines by formatting an html document before turning into pdf to print out on paper. I found CSS formatting to be my answer https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/

I am not sure if this is the solution you are looking for but using

<style>
    @media print {
        div { break-inside: avoid; }
    }
</style>

was all I needed before I could open the html document in my browser and use its inbuilt ctrl-P to see a "virtual PDF". This method also let me modify margins and specify width/height of the paper.

Upvotes: 0

remix23
remix23

Reputation: 3019

Here is a naive but working implementation.

The idea is to mount the html into an offscreen div which has the same dimensions as the pages we're trying to render.

Then iterate over the elements (ie children from parsed html) of this offscreen div and query the dom using getBoundingClientRect to find the first overflowing element.

We then remove all elements before the overflowing one from the offscreen div and cache them in an array.

Start over for a new chunk until there is no more elements in the offscreen div.

Adapting this to React is just a matter of using dangerouslySetInnerHTML with the html content of each page.

(the display flex is just there to force flowing of elements, but any layout would do provided it's the same in offscreenDiv and in page)

function generateRandomContent() {
  var alph = "abcdefghijklmnopqrstuvwxyz";
  var content = "";
  // we will generate 100 random elements displaying their index to keep track of what's happening
  for (var i = 0; i < 100; i++) {
    var type = parseInt(Math.random() * 2, 10);
    switch (type) {
      case 0: // text, generates and random p block
        content = content + "<p>" + i + " ";
        var numWords = 10 + parseInt(Math.random() * 50, 10);
        for (var j = 0; j < numWords; j++) {
          var numLetters = 2 + parseInt(Math.random() * 15, 10);
          if (j > 0) {
            content = content + " ";
          }
          for (var k = 0; k < numLetters; k++) {
            content = content + alph[parseInt(Math.random() * 26, 10)];
          }
          
        }
        content = content + "</p>";
        break;
      case 1: // colored div, generates a div of random size and color
        var width = 30 + parseInt(Math.random() * 20, 10) * 10;
        var height = 30 + parseInt(Math.random() * 20, 10) * 10;
        var color = "rgb(" + parseInt(Math.random() * 255, 10) + ", " + parseInt(Math.random() * 255, 10) + ", " + parseInt(Math.random() * 255, 10) + ")";
        content = content + '<div style="width: ' + width + 'px; height: ' + height + 'px; background-color: ' + color + '">' + i + '</div>';
        break;
       
    }
  }
  return content;
}

function getNodeChunks(htmlDocument) {
  var offscreenDiv = document.createElement('div');
  offscreenDiv.className = 'page';
  offscreenDiv.style.position = 'absolute';
  offscreenDiv.style.top = '-3000px';
  offscreenDiv.innerHTML = htmlDocument;
  offscreenDiv.display = 'flex';
  offscreenDiv.flexWrap = 'wrap';
  document.body.appendChild(offscreenDiv);
  offscreenRect = offscreenDiv.getBoundingClientRect();
  // console.log('offscreenRect:', offscreenRect);
  var chunks = [];
  var currentChunk = []
  for (var i = 0; i < offscreenDiv.children.length; i++) {
    var current = offscreenDiv.children[i];
    var currentRect = current.getBoundingClientRect();
    currentChunk.push(current);
    if (currentRect.bottom > (offscreenRect.bottom)) {
      // current element is overflowing offscreenDiv, remove it from current chunk
      currentChunk.pop();
      // remove all elements in currentChunk from offscreenDiv
      currentChunk.forEach(elem => elem.remove());
      // since children were removed from offscreenDiv, adjust i to start back at current eleme on next iteration
      i -= currentChunk.length;
      // push current completed chunk to the resulting chunklist
      chunks.push(currentChunk);
      // initialise new current chunk
      currentChunk = [current];
      offscreenRect = offscreenDiv.getBoundingClientRect();
    }
  }
  // currentChunk may not be empty but we need the last elements
  if (currentChunk.length > 0) {
    currentChunk.forEach(elem => elem.remove());
    chunks.push(currentChunk);
  }
  // offscreenDiv is not needed anymore
  offscreenDiv.remove();
  return chunks;
}

function appendChunksToPages(chunks) {
    var container = document.getElementsByClassName('root_container')[0];
    chunks.forEach((chunk, index) => {
      // ex of a page header
      var header = document.createElement('div');
      header.innerHTML = '<h4 style="margin: 5px">Page ' + (index + 1) + '</h4>';
      container.appendChild(header);
      var page = document.createElement('div');
      page.className = 'page';
      chunk.forEach(elem => page.appendChild(elem));
      container.appendChild(page);
    });
}

// generateRandom content outputs raw html, getNodeChunks returns
// an array of array of elements, the first dimension is the set of
// pages, the second dimension is the set of elements in each page
// finally appendChunks to pages generates a page for each chunk 
// and adds this page to the root container
appendChunksToPages(getNodeChunks(generateRandomContent()));
 
 .page {
  border: 1px solid;
  display: flex;
  flex-wrap: wrap;
  height: 700px;
  width: 50%;
  margin-bottom: 20px;
 }
<div class="root_container"></div>

Upvotes: 2

Related Questions