Generating div or span at textarea by each button

I have a page at http://dot.kr/QnA/230612copyButton/01.php

It has two buttons. The one is div button and the other is span button.

if I drag the text “my text” in the textarea and click the div button, the text in the textarea is changing from before to after like the below.
before
This is my text
after
This is <div>my text</div>

if I drag the text “this” in the textarea and click the span button, the text in the textarea is changing from before to after like the below.
before
This is my text
after
<span>This</span> is my text

The page doesn’t actually work but it shows what I want.

Can I make the page to work with your help?

This is not really my area and I can’t really think of a use-case for this (or even if its valid to have div tags in a textarea) but with a lot of help from borrowed code this seems to do what you are asking.

1 Like

To make it a little more DRY you could use one function to wrap a selection and pass in the tag name as a second argument e.g. ‘div’, ‘span’, ‘p’ etc.

// Params textArea (dom element), tagName (string)
function wrapSelection (textArea, tagName) {
  const start = textArea.selectionStart;
  const end = textArea.selectionEnd;
  const text = textArea.value;

  if (start === end) {
    alert("Please select some text.");
    return text; // return the original text.
  }
  
  const selectedText = text.substring(start, end);
  const firstPart = text.substring(0, start);
  const endPart = text.substring(end);
  // using template strings
  return `${firstPart}<${tagName}>${selectedText}</${tagName}>${endPart}`;
}

then for the rest.

function wrapWithDiv() {
  const textArea = document.querySelector('#myTextarea');
  
  textArea.value = wrapSelection(textArea, 'div')
}

function wrapWithSpan() {
  const textArea = document.querySelector('#myTextarea');
  
  textArea.value = wrapSelection(textArea, 'span')
}

// Add event listeners
document
  .getElementById("wrapDivButton")
  .addEventListener("click", wrapWithDiv);

document
  .getElementById("wrapSpanButton")
  .addEventListener("click", wrapWithSpan);
2 Likes

I have two links below.

http://dot.kr/QnA/230627tagGen/paul/index.php

http://dot.kr/QnA/230627tagGen/rpg/index.php

The first one is paul’s code.
The second one is rpg’s code.

The difference is in js.js only.and no defference in index.php and css.css between paul’s code and rpg’s cod.

While paul’s code works fine, rpg’s code does NOT work.

What’s wrong with my applying of rpg’s code?

@joon1 Looking at your souce code

<script src="js.js"></script>

You have missed off the ‘f’ in function

unction wrapSelection (textArea, tagName) { // ← 'f' missing from function
  const start = textArea.selectionStart;
  const end = textArea.selectionEnd;
  const text = textArea.value;

  ...
} 
1 Like

I like to add another tag generating button.
I have 3 buttons at http://dot.kr/qna/230629addButton/
It is extended based on paul’s code and works fine while I failed in making 3 buttons with rpg’s code.
Thank you very much, Paul and rpg.

Ok some slight changes

For the html I have added dataset properties, in this case the classnames

<!-- buttons have dataset properties e.g. data-className='class-01' -->
<button class='addClass' data-classname='class-01'>Add class 01</button>
<button class='addClass' data-classname='class-02'>Add class 02</button>
<button class='addClass' data-classname='class-03'>Add class 03</button>

These dataset properties can be read by the handler function when the button is clicked.

We can loop through the buttons adding eventlisteners. The buttons all have a common classname of ‘addClass’

// Loop through the buttons adding eventlisteners
document.querySelectorAll('.addClass')
  .forEach((button) => button.addEventListener('click', wrapWithClassName))

The wrapWithClassName function

// an event object is automatically passed to the event handler
function wrapWithClassName(event) {
  const button = event.target // event.target is what was clicked on e.g. button
  const className = button.dataset.classname // get the classname from data-className e.g. class-01
  const textArea = document.querySelector('#myTextarea');

  // send three arguments now, the textArea, the opening tag and the closing tag
  textArea.value = wrapSelection(textArea, `<div class="${className}">`, '</div>')
}

The wrapSelection function
As before, but now expects an opening and closing tag

// Params textArea (dom element), openTag (string), closingTag (string)
function wrapSelection (textArea, openTag = '<div>', closingTag = '</div>') {
  const start = textArea.selectionStart;
  const end = textArea.selectionEnd;
  const text = textArea.value;

  if (start === end) {
    alert("Please select some text.");
    return text; // return the original text.
  }
  
  const selectedText = text.substring(start, end);
  const firstPart = text.substring(0, start);
  const endPart = text.substring(end);
  // using template strings
  return `${firstPart}${openTag}${selectedText}${closingTag}${endPart}`;
}

I’m sure it could be refactored and improved on, but there are some ideas there. The one main idea being to let the code handle repetitive tasks for you, looping through the buttons, using a single handler for all the buttons etc.

1 Like

With your code, I made the page at http://dot.kr/qna/230630addButton/
It works fine.
and I can make another button more than three with the code above.
So far, so good.

I am afraid I found a small thing which I want more.
.
if I write a text “my text” in the textarea.
The text “my text” is written in the textarea.
And I do undo(control + z), the text “my text” will be disappear.

Let’s suppose the following.
if I accidently click the button “Add class 02” instead of the button “Add class 01”
I have to undo “Add class 02”,
but “undo” doesn’t work.

Can I make “undo” work with your help?

Yes that’s is right. The span/div text is added dynamically with JS and isn’t stored for an undo — I’m sure that could be put more accurately.

I seem to remember running into this scenario myself, and creating my own undo button, which is what I have chosen to do in your example.

I am saving the text to localStorage ahead of making any changes e.g. wrapping with a div or span. I then have an undo button that will retrieve the saved text.

I have opted to store that text in an array, so that the undo button can retrieve multiple undos. It is not very sophisticated but seems to work.

An example of an array that will be converted to a JSON format and stored.

// example after making 3 changes
[
  'some text. some more text',
  '<span>some text.</span> some more text',
  '<span>some text.</span> some <div>more</div> text',
]

Storage Functions:
On starting, clear the previous storage.

const TEXTSTORE = "text-store";
// start by clearing previous storage
localStorage.removeItem(TEXTSTORE);

If you are not familiar with JSON, you will want to take a look at JSON.parse and JSON.stringify. The data is stored in string format.

retrieveText function

const retrieveText = () => {
  // will retrieve the stored data, which is in string format
  // and JSON.parse will convert that into a usable array
  const storedText = JSON.parse(localStorage.getItem(TEXTSTORE));

  // if nothing has been stored then return with false
  if (storedText === null) {
    return false;
  }

  // let's just check that we have an array
  if (Array.isArray(storedText)) {
    // retrieve last item stored in array.
    // pop will mutate the storedText array, removing the last item
    const lastText = storedText.pop();
    // now the array has one less item, overwrite the previous store
    localStorage.setItem(TEXTSTORE, JSON.stringify(storedText));
    // return lastText
    return lastText;
  }

  return false;
};

storeText function

const storeText = (text) => {
  const storedText = JSON.parse(localStorage.getItem(TEXTSTORE));

  // if this is the first time storing then create our text store
  // saving an array with our first bit of text
  if (storedText === null) {
    localStorage.setItem(TEXTSTORE, JSON.stringify([text]));
    return; // return early
  }
  // otherwise add new text to array and store
  if (Array.isArray(storedText)) {
    // add new text to end of array
    storedText.push(text);
    // overwrite text store with ammended array
    localStorage.setItem(TEXTSTORE, JSON.stringify(storedText));
  }
};

Buttons
The buttons will now look like this

document
  .querySelectorAll(".wrap-with")
  .forEach((button) =>
    button.addEventListener("click", (event) => {
      const textArea = document.querySelector("#myTextarea");
      // store current text before making a change
      storeText(textArea.value);
      wrapWith(event.target, textArea);
    })
  );

document
  .querySelector(".undo")
  .addEventListener("click", () => {
    const textArea = document.querySelector("#myTextarea");
    // retrieve last text change
    const lastText = retrieveText();
    // if there is a change then replace the current text with last text
    if (lastText) {
      textArea.value = lastText;
    }
  });

Here is a working codepen

Note there are some other changes compared to the last version — had a bit of a play this morning. The HTML now looks like this.

<div class='buttons-container'>
  <button
      class='wrap-with' 
      data-tag='div'
      data-attribs='id="id-01" classname="div-01"'
  >Wrap with div 01</button>
  <button
      class='wrap-with'
      data-tag='div'
      data-attribs='id="id-02" classname="div-02"'
  >Wrap with div 02</button>
  <button
      class='wrap-with'
      data-tag='span'
      data-attribs='id="id-03" classname="span-01"'
  >Wrap with span 01</button>
  <button
      class='wrap-with'
      data-tag='span'
      data-attribs='id="id-04" classname="span-02"'
  >Wrap with span 02</button>
  <button class='undo'>Undo</button>
</div>

I have added a tag type and the attribute text that will go inside of the opening tag. Just playing :slight_smile:

Edit: Added semi-colons to line-ends

With your code, I makde a page at http://dot.kr/QnA/230701undo.
It works fine.

In order to understand the page above,
I like to ask one of basic questions of javascript.
What do semi colon do for in javascript?

 localStorage.removeItem(TEXTSTORE)

The code above has NOT semi colon at the end.
The code below has semi colon at the end.

const start = textArea.selectionStart ;

In js what is the defference between having semi colon or not having semi colon?

This is a contentious issue, I don’t use semi colons (I probably should here on sitepoint). It’s a habit and the linter I generally use standardjs removes them automatically, which I prefer.

However if you’re new to JS, then it is probably a best policy to use them. For instance a line like this will throw a type error

const x = 5
[1,2,3].forEach((y) => console.log(x * y))

// Uncaught TypeError: Cannot read properties of undefined

The above code will be interpretted as one line

const x = 5[1,2,3].forEach((y) => console.log(x * y))

It could be fixed with

const x = 5;
[1,2,3].forEach((y) => console.log(x * y))

I have edited my last codepen and put in the semi-colons for you. Fortunately codepen has a format javascript option (top right of the JS editior v), which does this automatically.

edit: The lines with semi-colons were left in from your code, I believe. Inconsistent and sloppy of me. Apologies.

On the way of getting your help, I feel like, all of a sudden, I try to study javascript from the first and basics.
That is the reason why I ask about semi colon.
I just wanted to know about how to declare variable or contant and how to use semi colon.
I think I made it complex.
I really do Apologies …

@joon1, I didn’t realise you were learning the basics. If you have any question just ask :slight_smile:

I makde a page at http://dot.kr/QnA/230703removeNull/

Firstly, I put semi colon as possible as I can.
I don’t know which way is better habit between “yes semi colon” and “no semi colon” but I just put it at the moment.

Secondly, I don’t want the alert “Please select some text”

So I remove the following code.

if (storedText === null) { return false }
and I also remove the following code.

if (start === end) {
alert("Please select some text.");
return text ;
}

It works fine as I expected. Thank you.

That is good. The Array.isArray(storedText) does that job for us. :+1:

Currently if you click on say the Wrap with span 01 a few times without making a selection you end up with.

<span id=><span id=></span>Lorem ipsum temporibus cum<span id=></span><span id=></span><span id=></span></span>

That doesn’t make much sense to me. I would put your if (start === end) code back in without the alert("Please ... ");

if (start === end) {
  return text;
}

That way clicks on the buttons without a selection will leave the text area unchanged.

Linting

Regarding use of commas and other good coding practices you could look at using a linter. I have just followed this tutorial. Beware it is quite involved to setup, but will be very helpful for your coding.

Note: I am using PC, so ignored the ‘sudo’ command at 7:39 e.g. I used

npm i -g eslint

I also didn’t install express @ 11:35, as we aren’t using nodeJS and routing.

For my .eslintrc.json I have gone for the following configuration.

{
    "extends": ["airbnb", "prettier"],
    "plugins": ["prettier"],
    "globals": {
        "window": true,
        "document": true,
        "localStorage": true
    },
    "rules": {
        "prettier/prettier": [
            "error",
            {
              "endOfLine": "auto"
            }
        ],
        "no-unused-vars": "warn",
        "no-console": "warn"
    }
}

With this setup it will then warn you of errors in your code e.g. missing semi-colons(;), and fix issues on saving.

If anyone here has a simpler setup it would be good to know!!

A Simpler Tutorial

I think I might have found a simpler tutorial for you, but first I am presuming you have nodejs installed.

If not you can download the installer here

Once installed, in vscode open a new terminal from the Terminal menu at the top.

Inside of the terminal type node -v and enter. You should see an output similar to this, in my case I have version v18.10.0

node -v
v18.10.0

Here is the tutorial

At the 2:15 mark the tutor types in npm init @eslint/config and then selects a series of options.
https://youtu.be/SYSZi_nQzxk?t=136

His options are geared towards using node, as you are not using node I think these are more appropriate selections.

  1. How would you like to use ESLint? - To check syntax, find problems, and enforce code styles.
  2. What type of modules does your project use? - Javascript Modules (import/export)
  3. Which framework does your project use? - None of these
  4. Does your project use TypeScript? - No
  5. Where does your code run? - Browser
  6. Use a popular style guide? - Hit Enter
  7. Select the first default option - Airbnb:https://github.com/airbnb/javascript
  8. What format do you want your config file to be in? - JSON
  9. Would you like to install them now? - Yes
  10. Which package manager do you want to use? - npm

At the 7.00 minute mark, he then sets up the fix on save option.
https://youtu.be/SYSZi_nQzxk?t=419

I had an issue with a typo, so this is the code for you to put in workspace settings.json

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": ["javascript"]
}

One issue I ran into was that in my sample code text I was getting the following errors:

Expected linebreaks to be 'LF' but found 'CRLF'

These were not being auto fixed. If you have the same issue here is the fix for this.

[https://stackoverflow.com/questions/64222494/js-expected-linebreaks-to-be-lf-but-found-crlf-eslintlinebreak-style-issue](https://stackoverflow.com/questions/64222494/js-expected-linebreaks-to-be-lf-but-found-crlf-eslintlinebreak-style-issue}

I opted to edit the .eslintrc.json file adding the line break option into my rules

"rules": {
    "linebreak-style": ["error", "windows"]
}

You should then be good to go.

Here is test I did with some sample code. You can see errors being highlighted and the issue under the problems tab.

Hitting ctrl + s fixes most of these issues

You can see there are a few issue that still need fixing, which would need to be fixed manually. It is helpful that these issues are highlighted though.

I hope this helps.

I have a page at http://dot.kr/QnA/230703-18customize/
It is more customized for my own use.

After I make the word “myText” hilight,

when I click the 1st button “mySquare”, it becomes the following.

This is <div id="id-01" class="mySquare">myText</div>

when I click the 2nd button “myRound”, it becomes the following.

This is <div id="id-02" class="myRound">myText</div>

when I click the 3rd button “my 3rd class”, it becomes the following.

This is <div id="id-03" class="my3rdClass">myText</div>

So far, so good.
Now I can make the attribue name of the class more intuitive.
However, the 4th and 5th buttons doen’t work as I want.

When I put my mouse before the word “myText” and click the 3rd button “open div and class”, it becomes the following.

This is <div id="id-04" class=""></div>myText

I want the result below.

This is <div id="id-04" class="">myText

When I put my mouse after the word “myText” and click the 4th button “close div”, it becomes the following.

This is myText<div ></div>

I want the result below.

This is myText</div>

And I have one more thing, i.e, I like to remove all “id=*” like the following because I don’t need it.

This is <div class="mySquare">myText</div> 
This is class="myRound">myText</div>
This is class="my3rdClass">myText</div>
This is <div class="">myText
This is myText</div>

Hi @joon1, it is easier to look at these problems if you post you code on codepen.

Could you do that?

Just thinking on the fly, you could give the data tag a dash separated unique name for these options e.g.

<button class='wrap-with' data-tag='div-opening' data-attribs='id="id-04" class=""'>
<button class='wrap-with' data-tag='div-closing' data-attribs='id="id-05" class=""'>

You could then maybe use String.split to split on the dash to get the two parts. Split returns an array of the parts.

const exampleTag01 = 'div';
const exampleTag02 = 'div-opening';
const exampleTag03 = 'div-closing';

exampleTag01.split('-') // ['div'] // nothing to split
exampleTag02.split('-') // ['div', 'opening']
exampleTag03.split('-') // ['div', 'closing']

To make it easier to work with you could use destructuring on the array that split returns e.g.

const [tag, type] = exampleTag01.split('-'); // ['div']
console.log(tag, type) // 'div', undefined

const [tag, type] = exampleTag02.split('-'); // ['div', 'opening']
console.log(tag, type) // 'div', 'opening'

Then some sort of conditional logic

if (type === 'opening') {
  return `${firstPart}<${tag} ${attributes}>${endPart}`;
} else if (type === 'closing') {
  ...
} else {
  ...
}

I joined codePen this moring.
I did tour of it as they guided me.
I made 1st codePen page of mine. It says now " :wave: Hello World!"
I don’t find how to post the 1st codePen page on this sitepoint post like you(rpg) and Paul?