DEV Community

Cover image for Show a toolbar after selecting text in a text area
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Show a toolbar after selecting text in a text area

Including a toolbar that appears after selecting text in a text area can significantly enhance the user experience. Not only does it give users access to commonly used formatting options, but it also saves them time and effort by eliminating the need to navigate menus or remember keyboard shortcuts.

With a toolbar available, users can effortlessly apply formatting such as bold, italic, underline, and font size without interrupting their workflow. They can also quickly add hyperlinks or adjust text alignment with just a click of a button.

By providing users with these convenient options, they can focus on their content instead of worrying about formatting mechanics. This results in a more efficient and enjoyable user experience.

In this post, we will create a simple Markdown editor. The primary editor uses a text area that allows users to modify the contents. Once users select text, a toolbar will appear, providing them with the ability to format the selected text quickly.

Creating the layout

Let's talk about how we can create the layout for our text area. We can use the same approach we used to highlight the current line in the text area. Here's an example of how the layout could look:

<div class="container" id="container">
    <div class="container__overlay">
        <div class="container__toolbar" class="container__toolbar"></div>
        <div class="container__mirror"></div>
    </div>
    <textarea id="textarea" class="container__textarea"></textarea>
</div>
Enter fullscreen mode Exit fullscreen mode

The layout consists of two elements: the mirrored element and the toolbar. The toolbar is positioned over the text area using the .container__toolbar class, which has a position: absolute property. This means it's positioned relative to its closest ancestor element, which in this case is the .container element. The top: 0 and left: 0 properties ensure that the toolbar appears at the top left corner of the .container element.

At first, the toolbar is hidden from view with the opacity: 0 property. But when text is selected within the text area, a JavaScript function can be used to change the opacity of the toolbar to 1 and display it on top of the text area. We'll cover this in the next section.

Here are the basic styles for the toolbar:

.container {
    position: relative;
}
.container__toolbar {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
}
Enter fullscreen mode Exit fullscreen mode

Creating the overlay and mirrored elements can be done dynamically, but it's also possible to create the toolbar in the markup and save ourselves from writing a lot of JavaScript code.

<div class="container" id="container">
    <div id="toolbar" class="container__toolbar">
        <!-- Buttons ... -->
    </div>
    <textarea id="textarea" class="container__textarea"></textarea>
</div>
Enter fullscreen mode Exit fullscreen mode

Here's a sample code snippet that shows how we can move the toolbar from being a direct child of the container to being a direct child of the overlay:

const containerEle = document.getElementById('container');
const textarea = document.getElementById('textarea');
const toolbarEle = document.getElementById('toolbar');

const overlayEle = document.createElement('div');
overlayEle.classList.add('container__overlay');
containerEle.prepend(overlayEle);

// Move the toolbar
overlayEle.appendChild(toolbarEle);

const mirroredEle = document.createElement('div');
mirroredEle.textContent = textarea.value;
mirroredEle.classList.add('container__mirror');
overlayEle.appendChild(mirroredEle);
Enter fullscreen mode Exit fullscreen mode

As you can see, it's a simple task that doesn't require anything fancy.

Showing the toolbar when text is selected

First, we need to listen for the mouseup event on the text area. This event will tell us when the user has selected text. Then, we can check if any text is currently selected within the text area by comparing the cursor index of the selected text. To get them, simply access the selectionStart and selectionEnd properties.

textarea.addEventListener('mouseup', () => {
    const cursorPos = textarea.selectionStart;
    const endSelection = textarea.selectionEnd;
    if (cursorPos !== endSelection) {
        // Users selected text ...

        // Build the mirrored element ...
        mirroredEle.append(pre, caretEle, post);
    }
Enter fullscreen mode Exit fullscreen mode

If there is selected text, we can build a mirrored element from three elements: two text nodes representing the text before and after the selected text, and an empty span element representing the starting cursor. This is the same technique you've already become familiar with in this series.

Now, let's talk about positioning the toolbar. We want it to appear above the selected text and centered horizontally with respect to the text area. To do this, we need to calculate the top and left properties of the toolbar element by calculating the bounding rectangles of both the caret and toolbar elements.

Here's a sample code to help you get started:

const rect = caretEle.getBoundingClientRect();
const toolbarRect = toolbarEle.getBoundingClientRect();
const left = (textarea.clientWidth - toolbarRect.width) / 2;
const top = rect.top + textarea.scrollTop - toolbarRect.height - 8;
Enter fullscreen mode Exit fullscreen mode

In this example, the rect object gives us info about where the caret element is in relation to the viewport. We can use this, along with textarea.scrollTop, to figure out where to put the toolbar. To make sure the toolbar shows up right above the selected text, we subtract toolbarRect.height (the height of the toolbar element) and a small constant (like 8) from rect.top.

Now, to center the toolbar horizontally with respect to the textarea, we need to calculate how much space is available on either side of the textarea. We do this by subtracting toolbarRect.width (the width of the toolbar element) from textarea.clientWidth (the width of the textarea). Then, we divide this number by two to get half of the remaining space. Finally, we set left equal to this value, and voila! The toolbar is perfectly centered.

To move the toolbar element to a new position, we use the transform property. Specifically, we use the translate() function to adjust the toolbar's position. By setting the left and top values to calculated values, we can center the toolbar perfectly above the selected text.

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

Good practice

The translate() function takes two arguments: the horizontal and vertical distance to move. We pass in pixel values for these arguments to move the toolbar horizontally by a certain amount (left) and vertically by another amount (top).
Using transform gives us smooth animations and transitions, thanks to hardware acceleration in modern browsers. Plus, we can avoid any layout recalculations that might happen if we tried to change the top or left properties directly.

Now that we have these calculations in place, our toolbar will always be optimally positioned when text is selected within our text area. Once we move the toolbar to the desired position, we can make it visible by setting the opacity and visibility properties to 1 and visible, respectively. To achieve this, we create the showToolbar() function.

const showToolbar = () => {
    toolbarEle.style.opacity = 1;
    toolbarEle.style.visibility = 'visible';
};
Enter fullscreen mode Exit fullscreen mode

Hiding the toolbar when no text is selected

Let's talk about how to hide the toolbar when there's no text selected. We can make this happen by detecting when the user clears their text selection. To do this, we listen for the selectionchange event on the document object. This event is triggered whenever the user changes their text selection.

Inside the event handler, we first check if any text is currently selected by calling window.getSelection().toString(). If no text is selected, we simply call the hideToolbar() function.

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

This function hides our toolbar by setting its opacity and visibility properties to 0 and hidden, respectively. It's worth noting that we set both properties to ensure that the toolbar doesn't interfere with users selecting text in a text area below it.

const hideToolbar = () => {
    toolbarEle.style.opacity = 0;
    toolbarEle.style.visibility = 'hidden';
};
Enter fullscreen mode Exit fullscreen mode

By doing this, we ensure that our toolbar is hidden from view whenever there's no text selected within our textarea element.

Simplifying text formats

In this post, we'll make the editor simpler by providing only three buttons in the toolbar. These buttons allow users to make selected text bold, italic, or strike through. The toolbar contains only three buttons, as follows:

<div id="toolbar" class="container__toolbar">
    <button class="toolbar__button" data-format="bold">...</button>
    <button class="toolbar__button" data-format="italic">...</button>
    <button class="toolbar__button" data-format="strike">...</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Each button comes with a special data-format attribute. When users click a button, we can determine which format they want to apply to the selected text.

To handle the format buttons, we'll query all of them and add a click event listener to each one. Inside the event listener, we'll use the getAttribute() method to retrieve the value of the data-format attribute and determine which format button was clicked.

[...toolbarEle.querySelectorAll('.toolbar__button')].forEach((button) => {
    button.addEventListener('click', (e) => {
        const format = button.getAttribute('data-format');
        console.log(format);
    });
});
Enter fullscreen mode Exit fullscreen mode

To loop through all of our format buttons and attach a click event listener to each one, we use a combination of querySelectorAll() and forEach(). Once clicked, we retrieve the value of the data-format attribute by calling getAttribute(). This will give us either "bold", "italic", or "strike" depending on which button was clicked.

Making text bold in Markdown

To make text bold in markdown, simply wrap the selected text with double asterisks (**). This tells markdown to render the enclosed text as bold.

To add this functionality to our toolbar, we need to get the start and end positions of the selected text using textarea.selectionStart and textarea.selectionEnd. We also need to get the current value of the textarea using textarea.value.

Once we have this information, we can use the setRangeText() method to insert the double asterisks before and after the selected text. This method takes three arguments: a string representing the new text to be inserted, an integer representing the starting cursor position, and an integer representing the ending cursor position.

For example, to make a selection bold, we could use the following code:

const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const currentValue = textarea.value;

// Insert double asterisks before and after selected text
textarea.setRangeText(`**${currentValue.slice(start, end)}**`, start, end);
Enter fullscreen mode Exit fullscreen mode

After formatting text, we can enhance the user experience by automatically placing the cursor at the end of the newly formatted text. To do this, we simply call focus() on the textarea element to shift focus back to it, and then set the selectionStart property to be just after the end of our newly formatted text.

For example, if we make a selection bold in our toolbar, we can add the following code to move the cursor to just after our newly formatted text:

textarea.focus();
textarea.selectionStart = end + 4;
Enter fullscreen mode Exit fullscreen mode

This code calls focus() on our textarea element to shift focus back to it. We then set selectionStart equal to end + 4, where end is the original end position of our selection. The number 4 represents the length of the double asterisks that were added during formatting.

By setting selectionStart in this way, we effectively place the cursor just after our newly formatted text. This ensures that users can continue typing or making further changes without having to manually reposition their cursor.

Implementing this feature helps make our toolbar more user-friendly and intuitive, ultimately improving the overall user experience.

Making text italic in Markdown

To make text italic in Markdown, simply wrap the selected text within a pair of asterisks (*). It's that easy! Here's an example:

textarea.setRangeText(`*${currentValue.slice(start, end)}*`, start, end);
textarea.focus();
textarea.selectionStart = end + 2;
Enter fullscreen mode Exit fullscreen mode

Striking through text in Markdown

Just like making text italic, you can easily strike through text in Markdown. All you need to do is wrap the selected text within a pair of tildes, like this: selected text.

textarea.setRangeText(`~~${currentValue.slice(start, end)}~~`, start, end);
textarea.focus();
textarea.selectionStart = end + 4;
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the final demo!

See also


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

If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks ๐Ÿ˜. Your support would mean a lot to me!

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

Top comments (0)