DEV Community

Siddharth
Siddharth

Posted on

Let's Improve the textarea!

I improved the textarea, and I'll show you how I did it here!

Here's the final result:

The base

We are gonna use a textarea to implement this because

  • Textareas support all the native keyboard shortcuts, clicking, and stuff which we don't want to reimplement
  • A11y!
  • Many chrome extensions (like Grammarly) support checking your writing in textareas, so we need that to work
  • It's almost the only option we got

So the first thing we should do is, well, create a textarea! Give it a proper ID and stuff so that we can target it later.



<textarea name="editor" id="editor"></textarea>


Enter fullscreen mode Exit fullscreen mode

Adding two characters when typing one of them

One of the first things I want to do is insert another ' when a ' is typed and position the cursor properly.

First things first, let's create a map of which characters we want to insert



const keymap = {
    // value: the value to insert when the character is typed
    // pos: the number of characters the cursor should move forwards
    '<': {value: '<>', pos: 1},
    '(': {value: '()', pos: 1},
    '{': {value: '{}', pos: 1},
    '[': {value: '[]', pos: 1},
    '\'': {value: '\'\'', pos: 1},
    '"': {value: '"', pos: 1},
    '': {value: '“”', pos: 1},
    '`': {value: '``', pos: 1},
    '': {value: '‘’', pos: 1},
    '«': {value: '«»', pos: 1},
    '': {value: '「」', pos: 1},
    '*': {value: '**', pos: 1},
    '_': {value: '__', pos: 1},
    '>': {value: '> ', pos: 2},
    '~': {value: '~~', pos: 1},
};
```

I've added maps for quotes, smart quotes, some uncommon quotes, and some common markdown syntax. Feel free to add more snippets

Next thing to do is add a keydown listener to the textarea:

```js
const editing = document.getElementById('editor');

editing.addEventListener('keydown', event => {
    // Code...
});
```

Check if there is a matching key in the keymap:

```js
if (keymap[event.key]) {
    // Code..
}
```

And if there is, prevent the default action, which is inserting a character:

```js
event.preventDefault();
```

And insert the correct character:

```js
const pos = editing.selectionStart;
editing.value = editing.value.slice(0, pos) +
    keymap[event.key].value +
    editing.value.slice(editing.selectionEnd);

editing.selectionStart = editing.selectionEnd = pos + keymap[event.key].pos;
```
<br>
![double-char](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3nug2gae2pjmdvxh9fu.gif)
<figcaption>There it is! Double characters inserted!</figcaption>

<br>
I gotta say, it feels very useful for creating emoticons `¯\_()_/¯`

## Inserting a tab character when we enter tab

It's so irritating to have to type four characters when we mean to insert a tab... we _have to_ fix it!

We can listen to a Tab key in our `eventListener`, and the rest of the code is pretty much the same as our previous snippet example:

```js
if (event.key === 'Tab') {
    event.preventDefault();
    const pos = editing.selectionStart;
    editing.value = editing.value.slice(0, pos) +
    // Please don't start a tabs vs spaces debate
            '   ' + editing.value.slice(editing.selectionEnd);

    editing.selectionStart = editing.selectionEnd = pos + 1;
}
```

And I also wanna change the tab width to 4:

```css
#editor {
    tab-size: 4;
}
```

<br>
![tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gabkgd55ogwt0dkfx064.gif)
<figcaption>TAB TAB TAB TAB TAB</figcaption>

<br>
That looks nice, only thing being we can no longer use tab to go to the next element. Well, I'll fix that someday. 

## Tab-to-expand snippets

Yeah! Snippets! 
I don't really have any ideas on snippets, but, we'll manage...

Once again, we are going to create a map with our keyboard shortcuts:

```js
const snipmap = {
    // These make no sense but I'll add them for completeness
    '1#': '# ',
    '2#': '## ',

    // These make sense
    '3#': '### ',
    '4#': '#### ',
    '5#': '##### ',
    '6#': '###### ',

    // Lorem ipsum
    'Lorem': 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum!',

    // Might be a good idea to add a snippet for a table sometime.
};
```

Next, we gotta do a bit of refactoring on out `Tab` section. We should make an if statement:

```js
if (event.key === 'Tab') {
    if (snippet exists) {

    } else {
        event.preventDefault();
        const pos = editing.selectionStart;
        editing.value = editing.value.slice(0, pos) +
                '   ' + editing.value.slice(editing.selectionEnd);

        editing.selectionStart = editing.selectionEnd = pos + (snipmap[word].length - 1);
    }
}
```

So that we can add a snippet if it exists, else just insert a Tab character.

First thing to do is check if a given snippet exists. So we have to
- Get the word behind the cursor
- Check if it is in the `snipmap`
  - If it is, then remove the snippet text and insert the snippet

First we'll define our `getWord` function:

```js
function getWord(text, caretPos) {
    let preText = text.substring(0, caretPos);
    let split = preText.split(/\s/);
    return split[split.length - 1].trim();
}
```

Then use it in the if statement:

```js
const word = getWord(editing.value, editing.selectionStart);
if (word && snipmap[word]) {
    event.preventDefault();
    const pos = editing.selectionStart;
    // Subtract the word's length because we need to remove the snippet from the original text
    editing.value = editing.value.slice(0, pos - word.length) +
            snipmap[word].value +
            editing.value.slice(editing.selectionEnd);
    editing.selectionStart = editing.selectionEnd = pos + snipmap[word].pos;
} else {
    // Tab code
}
```

<br>
![tab-snip](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pisoc03bs5s4ujf8wkl1.gif)  
<figcaption>*type* TAB TAB *type* *type* TAB</figcaption>

<br>
And now we have our Tab snippets working. 

## Bonus: A Bookmarklet

Well, If we could just have this work for every textarea, that would be awesome!! So, I just made a bookmarklet out of it, and here's the pen:




That bookmarklet works on almost every website, even on DEV!

--- 

That's it! Stay tuned for part 2 of this post, I have a few features planned and also a few ideas which I don't know how to implement ;)
Enter fullscreen mode Exit fullscreen mode

Top comments (17)

Collapse
 
ianwijma profile image
Ian Wijma

I've made many "improved" text fields in the past. But I always bump info many edge cases. But it's a really fun and educative process I think. Good luck!

PS: ctrl-z / undo has weird behaviour when writing some thing like <test>. You remove the content, write some more and than hit undo / ctrl-z.

Collapse
 
siddharthshyniben profile image
Siddharth

Yeah, inserting the extra characters messes up the undo tree

Collapse
 
wintercounter profile image
Victor Vincent

You can fix that by fireing keypress events instead of overwriting values.

Thread Thread
 
siddharthshyniben profile image
Siddharth

Nice tip! Will definitely try it

Collapse
 
siddharthshyniben profile image
Siddharth

That sounds intresting, could just send me the links to them if you have them on GitHub or something?

Collapse
 
siddharthshyniben profile image
Siddharth • Edited

Hey there! If you have any more ideas for more improvements, just comment down below. Here's my current Todo list:

  • Get this project on GitHub as a library or something
  • List improvements
    • Add support for -[ ] lists
  • Auto resize textareas
  • Smarter quotes – If there is a word before a quote, just insert one quote.
  • Wrap selected stuff with <insert character>
  • (no) Word wrap
  • (Highly unlikely) Find a way to not mess up the undo tree (try to use keypress events)
  • (Super highly unlikely) Multi cursor editing
Collapse
 
terabytetiger profile image
Tyler V. (he/him) • Edited
  • A11y!

How do you keyboard navigate out of the textarea when tab inserts a tab proper?

Collapse
 
siddharthshyniben profile image
Siddharth

I think I already mentioned it in the post, right?

Collapse
 
terabytetiger profile image
Tyler V. (he/him)

I'm asking because you have making the textarea more accessible as one of your motivations, but then removed tab functionality from the user - which actually causes the post to not be accessible because trying to navigate the first codepen traps the user.

Even if you move the functionality to another key, you're moving the functionality away from where the user is expecting it, and I'm not sure how you would notify the user that they now need to push something other than tab to navigate out of the box.

Thread Thread
 
siddharthshyniben profile image
Siddharth

Yeah, that's right. I've thought about it but I got no ideas, so I didn't mention it too much in the post.

Do you have any ideas on how to fix it?

Thread Thread
 
terabytetiger profile image
Tyler V. (he/him)

I think the solution is to not modify the tab functionality and include a snippet shortcut for adding tab literals?

Thread Thread
 
siddharthshyniben profile image
Siddharth

Good idea! I think what we could do is add another keybinding for inserting a tab, maybe alt + tab

Collapse
 
siddharthshyniben profile image
Siddharth

I know 😅, Maybe we could just use an alternate key? Maybe.

Collapse
 
fagnerbrack profile image
Fagner Brack • Edited

I use an international keyboard where I can press ' and then "a" to have "á". If I want to use double quotes I press " and then space. In the textarea above, it creates """" instead of "".

Nice idea but there are so many edge cases that the cost to develop something like this would be simply not worth it for the majority of the projects out there.

Collapse
 
the_one profile image
Roland Doda

Nice article. It would be great to see how you implement the "@" feature when you tag someone.

Collapse
 
siddharthshyniben profile image
Siddharth

That's a DEV specific thing, so that's a bit far on my list

Collapse
 
siddharthshyniben profile image
Siddharth

Hmm, I could add that sometime. Nice suggestion!