Reputation: 7337
I'm not sure "orphaned" is appropriate, but it's the best I could come up with here.
I want to be able to detect when there's an additional word or two in the last line at the end of the text in a div.
PLEASE NOTE: I am not trying to eliminate these "orphans" by giving them the company of another word on the final line.
I've got a div with a fixed width that contains text that I need to end evenly at the end of a line. Sometimes a word (or two) creeps down to an additional line. I'd like to be able to detect when this happens (so the code can then adjust the font-size of the div until the orphan word/words pop up to the previous line).
Using text-align-last: justify;
is not a solution to my problem by itself.
I've been trying element.getClientRects()
, figuring I would get a rectangle for each row and then just look at the width of the lowest rectangle given. But for a single div, I get only one rectangle. And if I put the text inside a span inside the div, I get sometimes as many as nine rectangles for six lines of text in the span. Yikes! I have no idea what these rectangles represent.
Anyone know a way I can detect an extra isolated word on a line by itself at the end of the multi-line text in a fixed-width div?
PLEASE NOTE: the optimal number of lines is not known ahead of time; it could be anywhere from one to five lines.
I've been asked to give examples.
Example 1 shows are three lines, but should be two lines:
<div class="chunk" style="text-indent:45ex;">
<div class="bekkerLine">25</div>
<span>τὸ μὲν οὖν εἰ ἓν καὶ ἀκίνητον τὸ ὂν σκοπεῖν οὐ περὶ φύσεος ἐστι σκοπεῖν·</span>
</div>
$0.getClientRects() on the span returns:
DOMRectList {0: DOMRect, 1: DOMRect, 2: DOMRect, 3: DOMRect, length: 4}
0: DOMRect {x: 475.796875, y: 328.875, width: 60.609375, height: 22, top: 328.875, …}
1: DOMRect {x: 536.40625, y: 328.875, width: 48.21875, height: 22, top: 328.875, …}
2: DOMRect {x: 139, y: 352.875, width: 445.546875, height: 22, top: 352.875, …}
3: DOMRect {x: 139, y: 376.875, width: 65.109375, height: 22, top: 376.875, …}
length: 4
__proto__: DOMRectList
Example 2 also shows as three lines and again should be two lines:
<div class="chunk">
<span> οὔ)· ἄνθρωπος γὰρ ἵππου ἕτερον τῷ εἴδει καὶ
τἀναντία ἀλλήλων.
καὶ πρὸς Παρμενίδην δὲ ὁ αὐτὸς τρόπος τῶν λόγων,
<div class="bekkerLine">22</div>
</span></div>
$0.getClientRects() on the span returns:
DOMRectList {0: DOMRect, 1: DOMRect, 2: DOMRect, 3: DOMRect, 4: DOMRect, length: 5}
0: DOMRect {x: 123, y: 319.875, width: 375.5, height: 22, top: 319.875, …}
1: DOMRect {x: 498.5, y: 319.875, width: 70.203125, height: 22, top: 319.875, …}
2: DOMRect {x: 123, y: 343.875, width: 82.40625, height: 22, top: 343.875, …}
3: DOMRect {x: 205.40625, y: 343.875, width: 363.46875, height: 22, top: 343.875, …}
4: DOMRect {x: 123, y: 367.875, width: 53.578125, height: 22, top: 367.875, …}
length: 5
__proto__: DOMRectList
Example 3 I wouldn't want to "fix" because the final line has too many words:
Upvotes: 2
Views: 420
Reputation: 7337
The two solutions that were posted didn't quite fit what I needed. (The way I wrote may be partially to blame. In any event I voted each of them up.) I ended up writing my own method for calculating the number of lines that the div should take up.
I put a span inside the div. This method sums the width of the client rects at each y position in order to get the length of each line of the span (not sure why these are divided up in the first place, but they are!). It then counts the number of these rects that are less than half the width of the div and returns that as the optimal number of lines.
howManyLinesIsBest(divEl) {
const rect0 = (divEl.getClientRects())[0];
const width = rect0.width;
const span = (divEl.getElementsByTagName("span"))[0];
const lines = {};
[].forEach.call(span.getClientRects(),
v => {
if (lines.hasOwnProperty(v.y)) {
lines[v.y] += v.width;
return;
}
lines[v.y] = v.width;
}
);
const wideRects = Object.values(lines).filter(v => v > width/2);
return wideRects.length > 1 ? wideRects.length : 1;
}
It works pretty well!
Upvotes: 0
Reputation: 510
I found a sort of answer to your question from another site:
"wrap every single character in a <span>
and then loop through these <span>
s, checking the offsetTop value for each.".
In your case you only care if the last third element is above the last since you want to know if the last two words are hanging.
I made this little snippet to show how it works: (going from 208 to 209 should produce a change)
var paragraph = document.getElementById("paragraph");
var thirdLast = document.getElementById("third-last");
var last = document.getElementById("last");
var widthSelector = document.getElementById("width");
function handleWidth(){
var fontSize = 16;
paragraph.style.fontSize = fontSize + "px";
var width = widthSelector.value;
paragraph.style.width = width + "px";
while(thirdLast.offsetTop < last.offsetTop && fontSize > 0){
fontSize--;
paragraph.style.fontSize = fontSize + "px";
}
}
<input id="width" type="number" onchange="handleWidth()"/>
<p id="paragraph">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit
anim <span id="third-last">id</span>
est <span id="last">laborum.</span>
</p>
Upvotes: 1
Reputation: 816
UPDATED: Now the solution detects if there's THRESHOLD
number of words on the last line. Here I set THRESHOLD
to 2
. For the second div it ends with three words on the last line and doesn't change. For the third div it ends with one word on the last line and shrinks down. In the fourth div it starts with two words on the last line and shrinks.
const THRESHOLD = 2;
document.querySelectorAll("div.fixed").forEach(el => {
const { height: originalHeight } = el.getClientRects()[0];
let thresholdIndex;
for (let i = el.innerHTML.length, occurences = 0; i > 0; i--) {
if (el.innerHTML[i] === " ") {
occurences++;
}
if (occurences === THRESHOLD) {
thresholdIndex = i;
break;
}
}
if (thresholdIndex) {
const lastWords = el.innerHTML.substring(
thresholdIndex,
el.innerHTML.length
);
const text = el.innerHTML.substring(0, thresholdIndex);
el.innerHTML = text;
const { height: newHeight } = el.getClientRects()[0];
el.innerHTML += lastWords;
if (newHeight < originalHeight) {
let height = originalHeight;
while (height > newHeight) {
const currentFontSize = parseInt(
window.getComputedStyle(el).getPropertyValue("font-size"),
10
);
const newFontSize = currentFontSize - 1;
el.style.fontSize = `${newFontSize}px`;
height = el.getClientRects()[0].height;
}
}
}
});
div {
width: 200px;
padding: 10px;
font-size: 16px;
}
<div class="fixed">Text Text Text Text Text Text</div>
<div class="fixed">Text Text Text Text Text Text Text Text Text</div>
<div class="fixed">Text Text Text Text Text Text Text Text Text Text Text Text Text</div>
<div class="fixed">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</div>
Upvotes: 1