Show an addition toolbar after users selects text
Tags: tutorial, javascript, webdev
Published: 2023-12-29 02:07:48

Web applications often offer additional actions when users select text on a page. For instance, when using a web-based document editor, it's helpful to have a toolbar appear when text is selected. This toolbar makes it easy to format text, such as by making it bold, italic, or underlined.

Another example is when you want to share a piece of text on social media. By selecting the text and clicking the share button on the toolbar, you can quickly post it to popular social networks.

In this post, we'll learn how to add this functionality to a web application using JavaScript. We'll use DOM manipulation to detect when the user has selected text, and then display an additional toolbar in the perfect spot. Let's get started!

## Preparing the toolbar

Let's talk about the toolbar. It is made up of a few buttons that should be displayed in the center of the toolbar, both vertically and horizontally. This can be easily achieved using CSS flexbox.

```html
<div id="toolbar" class="toolbar">
    <button class="toolbar__button">...</button>
    <button class="toolbar__button">...</button>
    <button class="toolbar__button">...</button>
    <!-- Other buttons go here ... -->
</div>
```

```css
.toolbar {
    align-items: center;
    display: flex;
    justify-content: center;
}
```

But wait, there's more! The toolbar needs to be repositioned dynamically depending on where we select the text. So, to make sure it's in the right spot, we'll set the `position` style to `absolute` within the document. And to keep it hidden until we need it, we'll set the `opacity` property to zero.

```css
.toolbar {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
}
```

## Detecting text selection

To detect when a user has selected text, we can use the `selection` object provided by the browser. This handy object contains information about the current text selection, such as the selected text, start and end positions of the selection, and the text node containing the selection.

Check out this example of how we can detect a text selection:

```js
document.addEventListener('mouseup', function() {
    const selection = window.getSelection();
    if (selection.toString().length > 0) {
        // The user has selected some text
    }
});
```

In this example, we're using the `mouseup` event to detect when the user has finished selecting text. We then check if the length of the selected text is greater than zero, which means the user has selected some text.

## Positioning the toolbar

When we detect that a user has selected some text, we need to figure out where to position the toolbar. To do that, we first need to determine the rectangle that encloses the selected text.

We typically use the `getClientRect()` function to calculate the bounding rectangle of an element. Here's an example code that will give you the bounding rectangle of the toolbar:

```js
const toolbarEle = document.getElementById('toolbar');
const toolbarRect = toolbarEle.getBoundingClientRect();
```

But when it comes to selected text, we can use a similar function called `getBoundingClientRect()` provided by the Selection APIs. This function returns a set of numbers, including the `top`, `left`, `height`, and `width` of the bounding rectangle.

```js
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
```

The `top` and `left` properties indicate the distance from the top-left corner of the selection to the top and left sides of the document. The `height` and `width` properties tell us how tall and wide the selected text is.

To see this in action, try selecting some text in the paragraphs below. You'll see the bounding rectangle highlighted with a dashed border. Give it a few tries, and you'll get the hang of using `getBoundingClientRect()` to position your toolbar perfectly.

{% codepen https://codepen.io/phuocng/pen/RwdPxpL %}

We can now calculate the position and size of both the selected text and the toolbar element, making it easy to display the toolbar exactly where we want it.

For instance, if we want to center the toolbar horizontally and position it at the top of the selected text, we can use the following formulas:

```js
const distanceFromTop = window.scrollY;
let top = selectionRect.top + distanceFromTop - toolbarRect.height;
let left = selectionRect.left + (selectionRect.width - toolbarRect.width) / 2;
```

Notice that we need to factor in the current scrollbar position (`distanceFromTop`) when calculating the top distance, to ensure that the toolbar appears in the correct vertical position.

There are some tricky cases that you might have to handle on your own. For instance, if the `top` value is negative, the toolbar may end up outside of the visible area. This can happen when users select the first line of a page, which is often too close to the top edge.

One solution is to position the toolbar at the bottom of the selected text, instead of at the top. To do this, you'll need to tweak the position calculations a bit.

```js
if (top < 0) {
    top = selectionRect.top + distanceFromTop + selectionRect.height;
    left = selectionRect.left;
}
```

Once you have the top and left distances, moving the toolbar to the right position is a piece of cake. Just remember to reset the `opacity` so that users can see the toolbar.

```js
toolbarEle.style.transform = `translate(${left}px, ${top}px)`;
toolbarEle.style.opacity = 1;
```

## Adding an arrow

To help users locate the toolbar, we can add an arrow at the bottom, centered horizontally. We can create this arrow using the `::after` pseudo-element without adding a new element to the page. By setting the `position` property to `absolute`, we can place the arrow in the toolbar.

Even though the arrow doesn't have any content, we still need to set the `content` property to an empty string so that it appears on the page. Don't worry about the zero `height` and `width`, because the arrow's size will be defined by its border.

```css
.toolbar::after {
    content: '';
    position: absolute;
    height: 0px;
    width: 0px;
}
```

Next, we need to move the arrow to the bottom of the toolbar and center it horizontally. We can easily achieve this by defining the `top` and `left` properties and using the `translate()` function.

```css
.toolbar::after {
    top: 100%;
    left: 50%;
    transform: translate(-50%, 0%);
}
```

To create an arrow shape, you can use borders of different colors and widths. It's important to note that the arrow's borders should match the color of the toolbar's background.

```css
.toolbar {
    background: var(--toolbar-background);
}
.toolbar::after {
    border-color: var(--toolbar-background) transparent transparent;
    border-width: 0.5rem 0.5rem 0;
}
```

Wait a minute! What happens if the toolbar is below the selected text? In that case, the arrow will appear on top of the toolbar. We'll need to adjust the arrow's position and borders.

To sum it up, we'll create two separate classes for the toolbar, depending on the position of both the toolbar and the arrow.

```css
.toolbar--bottom::after {
    top: 100%;
    left: 50%;
    transform: translate(-50%, 0%);
    border-color: var(--toolbar-background) transparent transparent;
    border-width: 0.5rem 0.5rem 0;
}
.toolbar--top::after {
    top: 0%;
    left: 50%;
    transform: translate(-50%, -100%);
    border-color: transparent transparent var(--toolbar-background);
    border-width: 0 0.5rem 0.5rem;
}
```

We can select the corresponding class depending on the position of the toolbar. In this example, we use the `remove()` and `add()` functions to remove and add a CSS class to the toolbar element.

```js
// Inside the mouseup event handler ...
let arrowDirection = 'bottom';

if (top < 0) {
    // Recalculate the top and left distances ...
    arrowDirection = 'top';
}

toolbarEle.classList.remove('toolbar--bottom');
toolbarEle.classList.remove('toolbar--top');
toolbarEle.classList.add(`toolbar--${arrowDirection}`);
```

## Hiding the toolbar

We need to hide the toolbar when there is no selected text. So, when does this happen?

Well, the document triggers the `selectionchange` event whenever users select text or not. We can use the event handler to easily detect whether users have made a text selection or not. If there's no selection, we can hide the toolbar automatically. It's as simple as that!

```js
document.addEventListener('selectionchange', () => {
    const selection = window.getSelection().toString();
    if (!selection) {
        toolbarEle.style.opacity = '0';
    }
});
```

## Sharing the selected text

Most social networks provide URLs that let us share information on their platform. These URLs can have different parameters, which allow us to pass along the target URL and the text we want to share.

It's important to remember to encode the passed parameters, so the final sharing URL has a valid format. Luckily, we can use the built-in `encodeURIComponent()` function for this purpose.

If we want to share selected text on Twitter, we can pass that text and the current URL to Twitter's sharing URL.

```js
const userName = '...';
const pageUrl = encodeURIComponent(window.location.href);
const selectedText = encodeURIComponent(window.getSelection().toString());
const url = `https://twitter.com/intent/tweet?text=${selectedText}&url=${pageUrl}&via=${userName}`;
```

We can now open the sharing URL in a new window using the `open` function, which has three parameters. The first parameter is for the sharing URL, the second one is for the window title, and the last one is for the window size in pixels.

```js
window.open(url, 'Share', 'width=500, height=400');
```

To find the correct format for the sharing URL of each social network API, refer to their official documentation. Here are some examples:

```js
// Facebook
const url = `https://www.facebook.com/sharer/sharer.php?u=${pageUrl}&quote=${selectedText}`

// Linkedin
const url = `https://www.linkedin.com/sharing/share-offsite/?url=${pageUrl}`;

// Pinterest
const url = `http://pinterest.com/pin/create/button/?url=${pageUrl}`;

// Reddit
const url = `https://reddit.com/submit?url=${pageUrl}`;
```

Now, we can handle the click event of each button inside the toolbar to perform its job. First, we attach the name of the corresponding social network to each button using a `data` attribute, like this:

```html
<button class="toolbar__button" data-service="facebook">Facebook</button>
```

This `data` attribute can then be used to determine the social network from the clicked button. Imagine that each button will handle the click event like this:

```js
button.addEventListener('click', (e) => {
    const service = button.getAttribute('data-service');
    const selectedText = encodeURIComponent(window.getSelection().toString());
    const pageUrl = encodeURIComponent(window.location.href);

    let serviceUrl = '';
    switch (service) {
        case 'twitter':
            serviceUrl = `https://twitter.com/intent/tweet?text=${selectedText}&url=${pageUrl}&via=nghuuphuoc`;
            break;
        case 'facebook':
            serviceUrl = `https://www.facebook.com/sharer/sharer.php?u=${pageUrl}&quote=${selectedText}`;
            break;
        default:
            break;
    }
});
```

The `service` variable represents the name of the social network where we want to share the information. Using a `switch` statement, we can open a new window to share the selected text depending on which social network was chosen.

In this example, I'm only storing the sharing URL value without taking any further action. In reality, you can open the sharing URL in a new window, as mentioned previously.

And now, for the grand finale, let's check out the last demonstration!

{% codepen https://codepen.io/phuocng/pen/NWJqXpO %}

## See also

-   [Show a toolbar after selecting text in a text area](https://phuoc.ng/collection/mirror-a-text-area/show-a-toolbar-after-selecting-text-in-a-text-area/)
-   [Triangle buttons](https://phuoc.ng/collection/css-layout/triangle-buttons/)

---

It's highly recommended that you visit the [original post](https://phuoc.ng/collection/html-dom/show-an-addition-toolbar-after-users-select-text/) to play with the interactive demos. 

If you want more helpful content like this, feel free to follow me:

-   [DEV](https://dev.to/phuocng)
-   [GitHub](https://github.com/phuocng)