Tuesday, July 23, 2024
HomeJavaScriptDetecting Rendered Line Breaks In A Textual content Node In JavaScript

Detecting Rendered Line Breaks In A Textual content Node In JavaScript


At work, I have been constructing a strategy to generate “placeholder” pictures utilizing a fraction of the DOM (Doc Object Mannequin). And, up till now, I have been utilizing the .measureText() methodology, accessible on the Canvas 2D rendering context, to programmatically wrap lines-of-text onto a <canvas> ingredient. However, this method has confirmed to be a bit “glitchy” on the sides. As such, I needed to see if I may discover a strategy to detect the rendered line breaks in a textual content node of the doc, no matter what the textual content within the markup appeared like. Then, I may extra simply render the traces of textual content to the <canvas> ingredient. It seems, the Vary class in JavaScript (properly, within the browser) is likely to be precisely what I would like.

Run this demo in my JavaScript Demos mission on GitHub.

View this code in my JavaScript Demos mission on GitHub.

ASIDE: As a fast observe, I am really making an attempt to recreate a really tiny fraction of what the html2canvas library by Niklas von Hertzen already does. However, as acknowledged in his personal README, the html2canvas library shouldn’t be utilized in a manufacturing utility. As such, I needed to attempt to create one thing over which I had full management (and accountability).

A Vary object represents some fragment of the web page. This could include a sequence of nodes; or, a part of a textual content node. What’s actually cool concerning the Vary object is that it may well return a set of bounding packing containers that symbolize the visible rendering of the objects throughout the vary.

I really appeared on the Vary object as soon as earlier than when drawing packing containers round chosen textual content. I did not actually have a use-case for that exploration on the time; however, performing that experiment 4-years in the past allowed me to see a path ahead in my present downside.

If I’ve a text-node within the DOM, and I create a Vary for the contents of that text-node, the .getClientRects() methodology, on the Vary, will return the bounding field for every line of textual content as it’s rendered for the consumer. Now, this does not inherently inform me which chunk of textual content is on which rendered line; however, it provides us a approach to do this with just a little brute-force magic.

Think about a Vary that has a single character in it – the first character in our text-node. This Vary will solely have a single bounding field. Now, what if we add the second character to that Vary and study the bounding packing containers? If there may be nonetheless a single bounding field, we are able to deduce that the second character is within the first line of textual content. However, if we now have two bounding packing containers, we are able to deduce that the second character belongs within the second line of textual content.

Extending this, if we incrementally broaden the contents of a Vary, one character at a time, the final added character will at all times be within the final line of textual content. And, we are able to decide the “index” of that final line of textual content by utilizing the present depend of the bounding packing containers.

That is positively brute pressure and might be going to be sluggish on very massive chunks of textual content. However, for a single paragraph on a desktop laptop, this brute pressure method feels instantaneous.

Let’s examine this in motion. Within the following demo, I’ve a textual content node with some static textual content in it. Whenever you click on the button, I study the textual content node and brute pressure extract the rendered traces of textual content and log them to the console. The tactic of not right here is named extractLinesFromTextNode() – that is the place we dynamically lengthen the Vary to establish the textual content wrapping:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>
		Detecting Rendered Line Breaks In A Textual content Node In JavaScript
	</title>
	<hyperlink rel="stylesheet" kind="textual content/css" href="https://www.bennadel.com/weblog/./predominant.css" />
</head>
<physique>

	<h1>
		Detecting Rendered Line Breaks In A Textual content Node In JavaScript
	</h1>

	<p class="pattern" type="width: 400px ;">
		You fell sufferer to one of many basic blunders-the most well-known of which is,
		"By no means become involved in a land conflict in Asia" - however solely barely much less well-known
		is that this: "By no means go in opposition to a Sicilian when loss of life is on the road"! Ha ha ha ha ha
		ha ha! Ha ha ha ha ha ha ha!
	</p>

	<p>
		<button class="button">
			Detect Line Breaks
		</button>
	</p>

	<script kind="textual content/javascript">

		var supply = doc.querySelector( ".pattern" ).firstChild;
		var button = doc.querySelector( ".button" );

		// When the consumer clicks the button, course of the textual content node.
		button.addEventListener(
			"click on",
			operate handleClick( occasion ) {

				logLines( extractLinesFromTextNode( supply ) );

			}
		);

		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //

		/**
		* I extract the visually rendered traces of textual content from the given textNode because it
		* exists within the doc at this very second. That means, it returns the traces of
		* textual content as seen by the consumer.
		*/
		operate extractLinesFromTextNode( textNode ) {

			if ( textNode.nodeType !== 3 ) {

				throw( new Error( "Strains can solely be extracted from textual content nodes." ) );

			}

			// BECAUSE SAFARI: Not one of the "trendy" browsers appear to care concerning the precise
			// structure of the underlying markup. Nonetheless, Safari appears to create vary
			// rectangles primarily based on the bodily construction of the markup (even when it
			// makes no distinction within the rendering of the textual content). As such, let's rewrite
			// the textual content content material of the node to REMOVE SUPERFLUOS WHITE-SPACE. This can
			// permit Safari's .getClientRects() to work like the opposite trendy browsers.
			textNode.textContent = collapseWhiteSpace( textNode.textContent );

			// A Vary represents a fraction of the doc which accommodates nodes and
			// elements of textual content nodes. One factor that is actually cool a couple of Vary is that we
			// can entry the bounding packing containers that include the contents of the Vary. By
			// incrementally including characters - from our textual content node - into the vary, and
			// then wanting on the Vary's consumer rectangles, we are able to decide which
			// characters belong during which rendered line.
			var textContent = textNode.textContent;
			var vary = doc.createRange();
			var traces = [];
			var lineCharacters = [];

			// Iterate over each character within the textual content node.
			for ( var i = 0 ; i < textContent.size ; i++ ) {

				// Set the vary to span from the start of the textual content node as much as and
				// together with the present character (offset).
				vary.setStart( textNode, 0 );
				vary.setEnd( textNode, ( i + 1 ) );

				// At this level, the Vary's consumer rectangles will embody a rectangle
				// for every visually-rendered line of textual content. Which suggests, the final
				// character in our Vary (the present character in our for-loop) might be
				// the final character within the final line of textual content (in our Vary). As such, we
				// can use the present rectangle depend to find out the road of textual content.
				var lineIndex = ( vary.getClientRects().size - 1 );

				// If that is the primary character on this line, create a brand new buffer for
				// this line.
				if ( ! traces[ lineIndex ] ) {

					traces.push( lineCharacters = [] );

				}

				// Add this character to the at the moment pending line of textual content.
				lineCharacters.push( textContent.charAt( i ) );

			}

			// At this level, now we have an array (traces) of arrays (characters). Let's
			// collapse the character buffers down right into a single textual content worth.
			traces = traces.map(
				operate operator( characters ) {

					return( collapseWhiteSpace( characters.be a part of( "" ) ) );

				}
			);

			// DEBUGGING: Draw packing containers round our consumer rectangles.
			drawRectBoxes( vary.getClientRects() );

			return( traces );

		}


		/**
		* I normalize the white-space within the given worth such that the quantity of white-
		* area matches the rendered white-space (browsers collapse strings of white-space
		* all the way down to single area character, visually, and that is simply updating the textual content to
		* match that habits).
		*/
		operate collapseWhiteSpace( worth ) {

			return( worth.trim().exchange( /s+/g, " " ) );

		}


		/**
		* I draw purple packing containers on the display screen for the given consumer rects.
		*/
		operate drawRectBoxes( clientRects ) {

			arrayFrom( doc.querySelectorAll( ".field" ) ).forEach(
				operate iterator( node ) {

					node.take away();

				}
			);

			arrayFrom( clientRects ).forEach(
				operate iterator( rect ) {

					var field = doc.createElement( "div" );
					field.classList.add( "field" )
					field.type.prime = ( rect.y + "px" );
					field.type.left = ( rect.x + "px" );
					field.type.width = ( rect.width + "px" );
					field.type.top = ( rect.top + "px" );
					doc.physique.appendChild( field );

				}
			);

		}


		/**
		* I log the given traces of textual content utilizing a grouped output.
		*/
		operate logLines( traces ) {

			console.group( "Rendered Strains of Textual content" );

			traces.forEach(
				operate iterator( line, i ) {

					console.log( i, line );

				}
			);

			console.groupEnd();

		}


		/**
		* I create a real array from the given array-like knowledge. Array.from() in case you are on
		* trendy browsers.
		*/
		operate arrayFrom( arrayLike ) {

			return( Array.prototype.slice.name( arrayLike ) );

		}

	</script>

</physique>
</html>

As you may see, we’re looping over the characters in our text-node, including each the Vary in sequence. Then, after every character has been added, we take a look at the present variety of bounding packing containers in an effort to decide which line of textual content accommodates the just-added character:

var lineIndex = ( vary.getClientRects().size - 1 );

On the finish of the brute-forcing, now we have a two-dimensional array of characters during which the primary dimension is that this lineIndex worth. Then, we merely collapse every character buffer (Array) down right into a single String and now we have our traces of textual content:

Multiple lines of text being extracted from a single text node in the DOM using JavaScript.

As you may see, we took a text-node from the DOM, which has no inherent line-breaks or text-wrapping, and used the Vary object to find out which substrings of that text-node the place on which traces (as seen by the consumer).

This works on my Chrome, Firefox, Edge, and Safari (although, I needed to normalize the white-space within the text-content to ensure that Safari to work persistently with the trendy browsers). And, after all, that is for a textual content node solely. That means, this method wasn’t designed to work with an Factor node which may include mixed-content (comparable to formatting parts). However, such a constraint is ample for my specific use-case.

As soon as I’ve this manufacturing, I would wish to follow-up with a extra in-depth instance of how I am producing the placeholder pictures utilizing the <canvas> ingredient. However, I am hopeful that this method will make it a lot simpler to render multi-line textual content to that picture.

Wish to use code from this put up?
Try the license.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments