Ben
Ben

Reputation: 13402

Is there a column break in my paragraph? How many lines in is it?

CSS has a concept of widows and orphans, so the browser has a way of knowing how many lines cross the column (or, if printing, page) boundary.

Is there a way to know this in JavaScript?

It could be done in a very unpleasant & brittle way using the geometry of the elements, but this seems like a pretty undesirable outcome.

For example: https://codepen.io/notionparallax/pen/BvLZeB

document.querySelectorAll("p, h1, h2, h3, h4, h5").forEach(
    (x) => {

      let boxW    = Math.round(x.getBoundingClientRect().width);
      let clientW = Math.round(x.clientWidth);
      if (boxW !== clientW) {
        console.log(x);
        x.classList.add("has-break");
      }

      try {
        if (x.nextElementSibling.offsetLeft !== x.offsetLeft) {
          console.log(x);
          x.classList.add("end-of-column");
        }
      } catch(error) {}

    }
)

In a library like Paged.js they have a concept of a break token that indicates where in the element the break occurs (if indeed there is a break). I'd imagine that this is the kind of thing that might be made possible by Houdini eventually. I'm interested to see if there's way of interrogating an element to see if it wraps to the next page/column, using conventional methods.

Use case

I want to know where the element breaks so that I can see if I ought to walk back up the DOM an put a break in ahead of the preceding header. This is partially handled by widows and orphans for the element itself, but I'd like to be able to have a finer grain of control so that—for instance—a chapter doesn't start right at the bottom of a page. I'd be fine with two lines of a paragraph between two paragraphs, but a paragraph that follows a heading should get a bit more breathing room.

Update

I'm not interested in solutions to the use case, I'm interested in answers to the question in the title. The use case illustrates one possible application of the information that I'm interested in, not the sole reason for the question.

Upvotes: 21

Views: 1005

Answers (4)

Ben
Ben

Reputation: 13402

I'd love benvc's approach to work, but it seems like that idea was dropped by the world.

This is a clumsy method that can tell if a p breaks, and then uses an even more clumsy method to identify the last word in the first column.

I'd be interested to see if there's a more elegant solution.

document.querySelectorAll(".poem_container p").forEach((p) => {
  const rectWidth = p.getBoundingClientRect().width;
  const reportedWidth = p.offsetWidth;
  if (reportedWidth > rectWidth * 0.9 && reportedWidth < rectWidth * 1.1) {
    p.classList.add("single-col");
  } else {
    p.classList.add("multi-col");
    let words = p.innerHTML
      .split(" ")
      .map((word) => `<span class="w">${word}</span>`)
      .join(" ");
    p.innerHTML = words;
    const leftEdge = p.getBoundingClientRect().x;
    let lastOne;
    p.querySelectorAll("span.w").forEach((w) => {
      const r = w.getBoundingClientRect();
      if (r.x + r.width < leftEdge + reportedWidth) {
        w.classList.add("in-col-1");
        if (w.innerHTML.trim() != ""){
            lastOne = w;
        }
      }
    });
    lastOne.classList.add("last-word");
    console.log(lastOne);
  }
});
.poem_container {
  width: 100%;
  height: 250px;
  column-count: 3;
  outline: 1px solid purple;
}

.single-col {
  outline: 1px solid green;
}

.multi-col {
  outline: 1px solid red;
}

.in-col-1 {
  background: orange;
}

.last-word {
  outline: 3px solid red;
}
<div class="poem_container">
  <p>This is a paragraph, it's a short one.</p>
  <p>
    The following is the first paragraph from Pale Fire, it's a 999 line poem. It's not particularly pertinent to this but it was easily to hand.
  </p>
  <p>
    I was the shadow of the waxwing slain / By the false azure in the windowpane; / I was the smudge of ashen fluff&mdash;and I / Lived on, flew on, in the reflected sky. / And from the inside, too, I'd duplicate / Myself, my lamp, an apple on a plate: /
    Uncurtaining the night, I'd let dark glass / Hang all the furniture above the grass, / And how delightful when a fall of snow / Covered my glimpse of lawn and reached up so / As to make chair and bed exactly stand / Upon that snow, out in that crystal
    land!
  </p>
</div>

Upvotes: 0

benvc
benvc

Reputation: 15130

Adding this answer only for future readers in the event that the API mentioned below becomes a standard implementation in major browsers and simplifies the js required to identify column breaks within elements. The below is not currently useful in production code as no major browser I am aware of fully implements the API below at the time of this answer.

There is a CSS Object Model API called GeometryUtils with a getBoxQuads() method that returns objects representing each CSS fragment of a node. The only browser I am aware of where it is mostly implemented at the time of this answer is Firefox Nightly.

The method makes it fairly simple to identify breaks within an element because it returns a DOMQuad object for each CSS fragment of a node, so you could do something like...

const frags = document.querySelector('p').getBoxQuads().length;

...where frags represents the number of CSS fragments for the selected element. If the element has more than 1 fragment, then there is a break. You would still need additional js to determine the number of lines, etc but this would enable you to isolate those efforts to just elements with a break. If you are interested in experimenting with getBoxQuads() as shown above, you will need to download Firefox Nightly.

Upvotes: 4

sultan
sultan

Reputation: 4739

🎈 Simple solution

First, lets fix orphans rule for paragraphs, for example:

p {
  orphans: 2;
}

Then, orphans rule for headers:

h1 + p {
  orphans: 4;
}

.new-chapter {
  break-before: column;
}

Finally we need js to add class new-chapter to h1 to prevent headers being apart from paragraph:

document.querySelectorAll("h1 + p").forEach(p => {
  const h1 = p.previousElementSibling
  if (h1.offsetLeft !== p.offsetLeft) {
    h1.classList.add(`new-chapter`)
    h1.style.opacity = 0.99
  }
  // after inserting css class need to refresh dom
  setTimeout(() => {
    h1.style.opacity = 1
  }, 2000)
})

Check 🧐DEMO

Upvotes: 4

Itay Gal
Itay Gal

Reputation: 10824

You can add page-break-before: always; to a tag, this will set a page break before the tag. For example, adding this CSS to h1 will create a new page before each h1 title

h1 {
  page-break-before: always;
}
<div>
  <h1>Page 1</h1>
  <p>content here</p>
  <h1>Page 2</h1>
  <p>content here</p>
</div>

UPDATE

It sounds like you want to find a way to identify, not set a printing page break, but note that the page size can vary, the scale and also the printing margins can be changed which obviously affect the page's content and the page break location. You don't have a way to find the page break in a generic way.

UPDATE 2

Each browser implements its own logic of how to divide the pages. For example, I tried to print this page from Chrome, Firefox and IE. There is a few line difference between them, and I didn't set anything, just used the default settings

Chrome:

Chrome

Firefox:

Firefox

IE:

IE

Upvotes: 9

Related Questions