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>
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;
}
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>
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);
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);
}
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;
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)`;
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
).
Usingtransform
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 thetop
orleft
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';
};
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();
}
});
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';
};
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>
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);
});
});
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);
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;
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;
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;
Let's take a look at the final demo!
See also
- Insert text into a text area at the current position
- Show an addition toolbar after users selects text
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)