Show an addition toolbar after users selects text

phuocng

Phuoc Nguyen

Posted on December 29, 2023

Show an addition toolbar after users selects text

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.

<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>
Enter fullscreen mode Exit fullscreen mode
.toolbar {
    align-items: center;
    display: flex;
    justify-content: center;
}
Enter fullscreen mode Exit fullscreen mode

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.

.toolbar {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

const toolbarEle = document.getElementById('toolbar');
const toolbarRect = toolbarEle.getBoundingClientRect();
Enter fullscreen mode Exit fullscreen mode

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.

const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
Enter fullscreen mode Exit fullscreen mode

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.

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:

const distanceFromTop = window.scrollY;
let top = selectionRect.top + distanceFromTop - toolbarRect.height;
let left = selectionRect.left + (selectionRect.width - toolbarRect.width) / 2;
Enter fullscreen mode Exit fullscreen mode

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.

if (top < 0) {
    top = selectionRect.top + distanceFromTop + selectionRect.height;
    left = selectionRect.left;
}
Enter fullscreen mode Exit fullscreen mode

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.

toolbarEle.style.transform = `translate(${left}px, ${top}px)`;
toolbarEle.style.opacity = 1;
Enter fullscreen mode Exit fullscreen mode

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.

.toolbar::after {
    content: '';
    position: absolute;
    height: 0px;
    width: 0px;
}
Enter fullscreen mode Exit fullscreen mode

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.

.toolbar::after {
    top: 100%;
    left: 50%;
    transform: translate(-50%, 0%);
}
Enter fullscreen mode Exit fullscreen mode

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.

.toolbar {
    background: var(--toolbar-background);
}
.toolbar::after {
    border-color: var(--toolbar-background) transparent transparent;
    border-width: 0.5rem 0.5rem 0;
}
Enter fullscreen mode Exit fullscreen mode

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.

.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;
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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}`);
Enter fullscreen mode Exit fullscreen mode

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!

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

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.

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}`;
Enter fullscreen mode Exit fullscreen mode

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.

window.open(url, 'Share', 'width=500, height=400');
Enter fullscreen mode Exit fullscreen mode

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

// 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}`;
Enter fullscreen mode Exit fullscreen mode

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:

<button class="toolbar__button" data-service="facebook">Facebook</button>
Enter fullscreen mode Exit fullscreen mode

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:

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;
    }
});
Enter fullscreen mode Exit fullscreen mode

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!

See also


It's highly recommended that you visit the original post to play with the interactive demos.

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

💖 💪 🙅 🚩
phuocng
Phuoc Nguyen

Posted on December 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related