---
title: Weekly Widget 4 - Show More with $.Range
description: Weekly Widget 4 - Show More with $.Range
image: https://www.bitovi.com/hubfs/canjs-banner-background.jpg
---

- ![AI implementation](https://www.bitovi.com/hubfs/AIConsultingIcon.svg)
  
  [AI implementation](https://www.bitovi.com/services/ai-consulting)
- ![Systems engineering](https://www.bitovi.com/hubfs/icon%20-%20backend.svg)
  
  [Systems engineering](https://www.bitovi.com/services/systems-engineering-consulting)
- ![Project Management](https://www.bitovi.com/hubfs/icon%20-%20PM.svg)
  
  [Project Management](https://www.bitovi.com/services/agile-project-management-consulting)
- ![Product Design](https://www.bitovi.com/hubfs/icon%20-%20design.svg)
  
  [Product Design](https://www.bitovi.com/services/product-design-consulting)
- ![Frontend development](https://www.bitovi.com/hubfs/icon%20-%20frontend.svg)
  
  [Frontend development](https://www.bitovi.com/services/frontend-development-consulting)
- [View more
  
  →
  
  ](https://www.bitovi.com/digital-consulting-services)

We're Experts in...

- [JavaScript](https://www.bitovi.com/services/frontend/javascript-consulting)
- [AI training](https://www.bitovi.com/ai-training-for-software-engineers)
- [Angular](https://www.bitovi.com/services/frontend/angular-consulting)
- [Design systems](https://www.bitovi.com/services/axure-figma-migration)
- [React](https://www.bitovi.com/services/frontend/react-consulting)
- [Temporal](https://www.bitovi.com/services/backend/temporal-consulting)
- [React Native](https://www.bitovi.com/services/frontend/react-consulting/react-native)
- [Node.js](https://www.bitovi.com/services/backend/nodejs-consulting)

Showcase

![Yum! Brands](https://www.bitovi.com/hubfs/yum-showcase-link-1.png)

[View case study](https://www.bitovi.com/en/bitovi-yum-case-study)

More Projects

- [![Levi's](https://www.bitovi.com/hubfs/levis.svg)](https://www.bitovi.com/web-application-consulting-work/levis-ecommerce-responsive-redesign)
- [![Christie's International Real Estate](https://www.bitovi.com/hubfs/christies.svg)](https://design.bitovi.com/christies)
- [![BAFS](https://www.bitovi.com/hubfs/bafs.svg)](https://www.bitovi.com/ux-design-consulting/ux-case-studies/bafs-ppp)
- [View more
  
  →
  
  ](https://www.bitovi.com/our-software-consulting-work)

Open Source Tools

We build powerful tools and open source them to support the community.

[See what we've built →](https://www.bitovi.com/open-source)

- [![Blog](https://www.bitovi.com/hubfs/icon%20-%20blog.svg)
  
  BlogWe post about delivering products and solving problems.
  
  ](https://www.bitovi.com/blog)
- [![Partnerships](https://www.bitovi.com/hubfs/Handshake-1.svg)
  
  PartnershipsLearn about Bitovi's technology partners
  
  ](https://www.bitovi.com/partnerships)
- [![Academy](https://www.bitovi.com/hubfs/icon%20-%20academy%20(4).svg)
  
  AcademyFree courses to build delivery skills
  
  ](https://www.bitovi.com/academy)
- [![Open source tools](https://www.bitovi.com/hubfs/icon%20-%20open%20source.svg)
  
  Open source toolsUse or contribute to our community
  
  ](https://www.bitovi.com/open-source)

Let's Connect

- [![Discord](https://www.bitovi.com/hubfs/DiscordLogo.svg)
  
  Discord
  
  ](https://discord.gg/J7ejFsZnJ4)
- [![LinkedIn](https://www.bitovi.com/hubfs/LinkedinLogo.svg)
  
  LinkedIn
  
  ](https://www.linkedin.com/company/bitovi/)
- [![GitHub](https://www.bitovi.com/hubfs/GithubLogo.svg)
  
  GitHub
  
  ](https://github.com/bitovi/)

![Eggbot](https://www.bitovi.com/hubfs/build_assets/bitovi-limbo-cms-react/338/js_client_assets/assets/eggbot-LTGhdSGL.png)

Name *

Work Email *

Phone

What's your project?

Send

### Contact Us

(312) 620-0386contact@bitovi.com

 February 7, 2013

# Weekly Widget 4 - Show More with $.Range

 Weekly Widget 4 - Show More with $.Range

![Justin Meyer](https://www.bitovi.com/hubfs/justin.jpeg)

 Justin Meyer

Share:

[![Twitter](https://www.bitovi.com/hubfs/limbo-generated/_astro/twitter-white.os3xLc3C_Z2nW4or.svg) ](https://twitter.com/intent/tweet?text=) [![Reddit](https://www.bitovi.com/hubfs/limbo-generated/imgs/icons/reddit.png) ](http://reddit.com/submit?url=)

This week's widget demonstrates the awesome power of jQuery++'s [range](http://jquerypp.com/#range) helper. Text ranges are notoriously a pain in the butt, with major differences in API and implementation across browsers. Similar to jQuery with DOM elements, `$.Range` provides a simpler API and cross browser methods to create, move, and compare text ranges. If you need to create a custom text editor, text highlighter, or other functionality that understands text, `$.Range` can be a huge help. For example:

```
// Get a text range for #text
var range = $('#text').range();
// Move the start 5 characters to the right
range.start('+5');
// Move the end 5 characters to the left
range.end('-5');
// Return the range text
range.toString(); // is some
// Select the current range
range.select();
```

Before reading this article it will be very helpful to read the API overview of [range](http://jquerypp.com/#range).

### The widget

The widget hides text past a certain number of lines. It replaces that text with a [+](https://www.bitovi.com/blog/weekly-widget-4-show-more-with-range#) button, allowing the user to see the hidden content:

JSFiddle

## What it does

The widget is called on an element whose text should be hidden like:

```
$('article').more({
    moreHTML: "<a href='javascript://' class='more'>...</a>",
    moreWidth: 30,
    lessHTML: " <a href='javascript://' class='less'>less</a>",
    lines: 4
})
```

Where:

- **moreHTML** - the html of the button to expand the text
- **moreWidth** - the width of the more button on the page
- **lessHTML** - the html of the button to hide the text
- **lines** - the number of lines to show

You might be thinking that this widget should be relatively simple and could be done without `$.Range` ... **WRONG**!

`$.Range` is almost certainly necessary because:

- The content's lines are not uniform in height. The 5th line might end at 100px in one element, at 110px in another.
- We remove just enough of the last line, if necessary, to make room for the **more** button.
- Not every piece of content has **lines** number of lines.

## How it works

First, I create a jQuery widget by adding a `more` method to `$.fn` and normalizing the options passed to `more`:

```
$.fn.more = function(options){
  options = $.extend({
      lessHTML: " <a href='javascript://' class='less'>-</a>",
      moreHTML: " <a href='javascript://' class='more'>+</a>",
      moreWidth: 50,
      lines: 2
  }, options || {});
})
```

This allows someone to call `$("article").more()` without options and have relatively sane defaults in place.

Next, I iterate through each item in the collection, save the original html for showing later, hide the right lines, and set up the toggling behavior:

```
this.each(function(el){
  var $el = $(this);
  $el.data('originalHTML', $el.html());

  // Hide lines
  // Set up toggling
})
```

I'll show how to hide the lines and set up toggling in the next two sections.

### Hiding the right lines

The `more` widget needs to find the last character on line number **lines** where adding **moreHTML** will not create another line. It then needs to remove all content within the container (`this` or `$el`) after that point and insert **moreHTML** in its place. The algorithm is roughly:

1. Go through each character, check it's vertical position. If the character is on a new line, keep going until you've reached **lines+1** lines.
2. Find the last character that can be visible and also have room for `moreHTML`.
3. Remove all text after `range`.

Lets break each of those three steps down:

#### Go through each character; check it's vertical position. If the character is on a new line, keep going until you've reached **lines+1** lines.

To accomplish this, I first create the range I'll be moving through each character, a range on the last character in the container and a range on the first non-whitespace character within the container:

```
var range = $el.range(),
    end = range.clone().collapse(false).start("-1"),
    start = nextChar( range.collapse().end("+1"), end ).clone(),
```

Outside `$.fn.more` I created `nextChar` and `prevChar` that move a range character by character until either a text character is hit or a boundary. When dealing with ranges, you need to make sure you don't move the range past your container!

Next, I maintain the number of lines we've seen and the current position of the line:

```
prevRect = start.rect(),
lines = 0;
```

Finally, move range through each character, checking its position against the previous line's position until **lines** is equal to `options.lines` or we've reached the end of the container.

```
while(range.compare("START_TO_START",end) != 0){
  range.end("+1").start("+1");

  var rect = range.rect();

  if( rect && (rect.top -prevRect.top  > 4) ) {
    lines++;

    if(lines == options.lines) break;

    prevStart = range.clone()
    prevRect = rect;
  }
}
```

*Note: For this widget, a new line has to be at least 4 pixels lower than the previous character.*

#### Find the last character that can be visible and also have room for `moreHTML`.

If we've seen **lines** number of lines, `range` represents the first character of that following line. So, I move `range` to the last character on the previous line and then to the last non-whitespace character:

```
if(lines === options.lines){
  range.end('-1').start('-1');
}
prevChar(range, start)
```

Next, I start moving the range right again until there's enough room between the range's character and the right side of the container:

```
var movedLeft = false,
    offset = $el.offset(),
    width = $el.width();

while(range.compare("START_TO_START",start) != -1 ){
  if( range.rect(true).left <= (offset.left+width-options.moreWidth) ) {
     break;
  }
  movedLeft = true;
  range.end("-1").start("-1")
}
```

The plugin exits and does nothing if a **moreHTML** button does not need to be added:

```
if(!movedLeft && (lines < options.lines ) ) {
  return
}
```

Past this point, `range` reprsents the last character that should be displayed.

#### Remove all text after `range`.

I start by removing all the text after `range` in the current text node.

```
var parent = range.start().container;
if( parent.nodeType === Node.TEXT_NODE ) {
  parent.nodeValue = 
    parent.nodeValue.slice(0,range.start().offset+1)
}
```

Next, I remove all the DOM nodes after the `parent` node I just found. I start by removing all siblings after the current node, then I walk up the DOM tree, removing all its parents siblings after the current node also, until I reach the container.

```
var removeAfter =  parent;

while(removeAfter !== this){
  var parentEl = removeAfter.parentNode,
      childNodes = parentEl.childNodes,
      index = $.inArray(removeAfter,childNodes );

  for(var i = parentEl.childNodes.length-1; i > index; i--){
    parentEl.removeChild( childNodes[i] );
  }
  removeAfter = parentEl;
}
```

### Set up Toggling

To set up the toggling behavior, I add **moreHTML** immediately after the HTMLElement that `range` was within:

```
if( parent.nodeType === Node.TEXT_NODE ||
  parent.nodeType === Node.CDATA_SECTION_NODE ) {
  parent = parent.parentElement
}
$(parent).append(options.moreHTML);
```

I save the shortened HTML content so we don't have to recalculate it if someone clicks the **showLess** button:

```
$el.data('shortenedHTML',$el.html())
```

Finally, I listen to clicks on more or less and update the container's html accordingly:

```
.on("click","a.more",function(){
  $el.html($el.data('originalHTML')+options.lessHTML)
})
.on("click","a.less",function(){
  $el.html($el.data('shortenedHTML'))
});
```

## Conclusion

A few random concluding thoughts:

- `$.Range` is a great way to understand the text layout of a page.
- `$.Range` would be a great low-level tool for creating a custom, cross-browser rich text editor.
- Checking the position of each character could be time consuming. A binary search could make things faster, same with moving the range word by word instead of character by character.

Lets hear some suggestions for next week!

[![Tag for open source](https://www.bitovi.com/hubfs/limbo/icons/tag.svg) open source ](https://www.bitovi.com/blog/topic/open-source)[![Tag for development](https://www.bitovi.com/hubfs/limbo/icons/tag.svg) development ](https://www.bitovi.com/blog/topic/development)

 Previous Post

![canjs-banner-background](https://www.bitovi.com/hs-fs/hubfs/canjs-banner-background.jpg?height=117&name=canjs-banner-background.jpg) [ Weekly Widget 3 - Paginated Grid ](https://www.bitovi.com/blog/weekly-widget-3-paginated-grid)

  

 Next Post

![](https://www.bitovi.com/hs-fs/hubfs/Imported_Blog_Media/zoolander.jpeg?height=117&name=zoolander.jpeg) [ Weekly Widget 5 - Real-Time Chat ](https://www.bitovi.com/blog/weekly-widget-5-real-time-chat)

```json
{
  "@context" : "http://schema.org",
  "@type" : "Organization",
  "address" : {
    "@type" : "PostalAddress",
    "addressCountry" : "United States",
    "addressLocality" : "Libertyville",
    "addressRegion" : "IL",
    "postalCode" : "60048",
    "streetAddress" : "1134 Pine Tree Lane "
  },
  "alternateName" : "Bitovi",
  "areaServed" : {
    "@type" : "GeoCircle",
    "geoMidpoint" : {
      "@type" : "GeoCoordinates",
      "latitude" : "41.8781",
      "longitude" : "87.6298"
    },
    "geoRadius" : "5000 km"
  },
  "description" : "Bitovi is a UX, UI design and front-end JavaScript development consulting company",
  "email" : "contact@bitovi.com",
  "image" : "https://www.bitovi.com/hubfs/bitovi-logo-x2.png",
  "logo" : "https://www.bitovi.com/hubfs/bitovi-logo-23-1.svg",
  "mainEntityOfPage" : {
    "@id" : "https://www.bitovi.com/blog/weekly-widget-4-show-more-with-range",
    "@type" : "WebPage",
    "description" : "Weekly Widget 4 - Show More with $.Range"
  },
  "naics" : "541511",
  "name" : "Bitovi Web App Consulting",
  "sameAs" : [ "https://www.facebook.com/BitoviLLC/", "https://twitter.com/bitovi", "https://www.linkedin.com/company/bitovi" ],
  "telephone" : "312-620-0386",
  "url" : "http://bitovi.com"
}
```

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Justin Meyer"
  },
  "dateModified" : "November 29, 2018, 5:52:24 PM",
  "datePublished" : "2013-02-07 20:33:53",
  "description" : "Weekly Widget 4 - Show More with $.Range",
  "headline" : "Weekly Widget 4 - Show More with $.Range",
  "image" : {
    "@type" : "ImageObject",
    "url" : "https://www.bitovi.com/hubfs/canjs-banner-background.jpg"
  },
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "url" : "https://www.bitovi.com/hubfs/bitovi-logo-23-1.svg"
    },
    "name" : "Bitovi"
  }
}
```