Why won't JavaScript function create an HTML element? - javascript

I have a method, which is meant to create divs within another div... However it won't work...
Here is the method:
populateSquares(){
let relatedTagToPopulation = document.getElementById("questionp");
let whatTextIs = relatedTagToPopulation.textContent;
for (let u=0;u<this.stemQuestions.length;u++){
if (this.stemQuestions[u]==whatTextIs){
var populateSquaresPertinentInt = u;
}
}
for (let i=0;i<this.stemAnswers.length;i++){
if (i==populateSquaresPertinentInt){
let numberOfSquaresToAdd = this.stemAnswers[i].length;
for (let j=0;j<numberOfSquaresToAdd;j++){
let elToAdd = document.createElement("<div id='ans"+j+"' class='lans'></div>");
let elToAddInto = document.getElementById("answeri");
elToAddInto.appendChild(elToAdd);
}
}
}
}
It gives out this error...
Uncaught DOMException: Failed to execute 'createElement' on 'Document': The tag name provided ('<div id='ans0' class='lans'></div>') is not a valid name.

If you are using JavaScript, you should follow the document: https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement, and here: CreateElement with id?
let elToAdd = document.createElement('div')
// then call `elToAdd.xxxx` to add attributes or do other operation on the element
elToAdd.setAttribute("id", "ans" + i);
// ... more
If you are using jQuery, you can use:
let elToAdd = jQuery("<div id='ans"+j+"' class='lans'></div>")

Three Ways to Create Tags
The following examples all do the same thing:✱
Create an <article> tag with the class: .post and add it to the <main id="main"> tag.
✱ There's an exception see #2 .innerHTML
document.createElement(tagName)
The only parameter is a tagName (ex. "DIV", "SPAN", "IFRAME", etc.). Once created it needs to be added to the DOM:
const post = document.createElement("ARTICLE");
post.className = "post";
document.getElementById('main').appendChild(post);
This is an old yet stable method but it takes two lines to create one barebones tag. More code is necessary to assign attributes and content.
.innerHTML += htmlString
This property will parse a tag(s) out of a given String within the targeted tag. If an = operator is used all content of the targeted tag is overwritten with the htmlString. If += operator is used the htmlString will be appended to the content within the targeted tag.
document.querySelector('main').innerHTML += `<article class='post'></article>`;
This pattern is simple and versatile. In one line multiple tags can be created with attributes and content. It's limited to either overwriting content: = or appending to content: +=.
✱Edit: Kaiido has informed me that .innerHTML will replace everything so if you are concerned about event bindings or references don't use it. See comments below.
.insertAdjacentHTML(position, htmlString)
This is .innerHTML on steroids. It will insert before/after/inside/outside a given htmlString of a targeted tag. The first parameter is one of four strings that determine the position of insertion relative to the targeted tag:
"beforebegin" <div id="target"> "afterbegin" text content "beforeend" </div> "afterend"
The second parameter is the htmlSting to be inserted.
document.getElementsByTagName('MAIN')[0].insertAdjacentHTML('afterbegin', `
<article class='post'></article>
`);
I couldn't follow your code but it's supposed to be a method? So the demo has an object called populate and there's a factory function called documentSection() that creates objects and inherits the method .createSection() from populate.
Demo
let text = ['post 1', 'post 2', 'post 3'];
let archives = ['document 1', 'document 2', 'document 3'];
const populate = content => ({
createSections: () => {
let idx = 0;
const target = document.querySelector("main");
/*
Pattern 1 - document.createElement(tagName)
*/
const section = document.createElement('SECTION');
section.id = content.title;
target.appendChild(section);
/*
Pattern 2 - .innerHTML += htmlString
*/
section.innerHTML += `<h2>${content.title}</h2>`;
for (let text of content.text) {
idx++;
/*
Pattern 3 - .insertAdjacentHTML(position, htmlString)
*/
section.insertAdjacentHTML('beforeend', `<article id="${content.title}-${idx}" class="t">${text}</article>`);
}
}
});
const documentSection = (title, text) => {
let content = {
title: title,
text: text
};
return Object.assign(content, populate(content));
};
const post = documentSection('Post', text);
const archive = documentSection('Archive', archives);
post.createSections();
archive.createSections();
main {
display: table;
border: 3px ridge grey;
padding: 2px 10px 10px;
}
h1 {
font: 900 small-caps 1.5rem/1 Tahoma;
margin-bottom: 8px
}
h2 {
font: 700 small-caps 1.2rem/1 Tahoma;
margin-bottom: 8px
}
section {
border: 2px solid #000;
padding: 2px 8px 8px;
}
article {
font: 400 1rem/1.25 Arial;
}
.t::before {
content: attr(id)': ';
font-weight: 700;
}
<main>
<h1>Documents</h1>
</main>

Related

Add and Remove Buttons JavaScript

I've tried this code that was posted but I can't get it to work the way I need. I can get it to create both fields but I can't make them look the same as the format of the first fields. How can I Add the entire section and make it look the same? and control the maximum number of activities inserted.
TIA
This is what I'm trying to acomplish
const addActivity = document.getElementById("add");
var i = 0;
const activityDiv = document.getElementById("Activity");
addActivity.addEventListener("click", function() {
i++;
const newspan = document.createElement('div');
newspan.className = "activityGroup";
const removeButton = document.createElement('button');
removeButton.addEventListener('click', function(e) {
e.target.closest(".activityGroup").remove();
});
removeButton.className = "delbtn";
removeButton.innerHTML = "X";
//
const txtfield = document.createElement('input');
const txtarea = document.createElement('textarea');
//
txtfield.id = 'activity_' + i;
txtfield.placeholder = "Activity " + i;
newspan.appendChild(txtfield);
//
newspan.appendChild(txtarea);
txtarea.id = 'activity_description_' + i;
txtarea.placeholder = "Activity Description " + i;
newspan.appendChild(removeButton);
activityDiv.appendChild(newspan);
});
.delbtn{color:red;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="Activity">
<div class="activityGroup">
<input placeHolder="Type your activity" />
<br>
<br>
<textarea rows="5" cols="50" placeHolder="Type your descvription"></textarea>
<button class="delbtn">X</button>
</div>
<!-- Remove button -->
</div>
<button id="add">Add Activity</button>
First, the problems:
I can get it to create both fields but I can't make them look the same as the format of the first fields...
This is likely because in your original .activityGroup there are a number of <br> elements between the <input> and the <textarea> elements – incorrectly – used for spacing which you haven't inserted into the copy.
Also, two of the attributes of the original <textarea> (rows="5", and cols="50") are omitted from the one you create within the function, not to mention the line-breaks and other white-space.
This can be rectified by simply creating, and appending, those other elements:
// first, some utility functions to simplify life:
const D = document,
get = (sel, ctx = D) => ctx.querySelector(sel),
getAll = (sel, ctx = D) => [...ctx.querySelectorAll(sel)],
create = (tag, props) => Object.assign(D.createElement(tag), props),
// renamed variables for consistency between JavaScript
// and the HTML upon which it acts:
addBtn = get("#addBtn"),
activity = get("#activity"),
// creating a generator function to use as a counter;
// fundamentally this is little different from the
// original i = 0 and then incrementing in the function
// body, but the generator can't be affected by an
// accidental decrement:
iterator = function*() {
// initial value:
let count = 1;
// a deliberate infinite loop, but the
// generator function exits/pauses at
// every yield so while this is an 'infinite
// function' it doesn't cause any blocking:
while (true) {
// yields value and then increments it:
yield count++
}
},
// getting a reference to the generator function:
counter = iterator(),
remove = (e) => e.currentTarget.closest('div').remove();
addBtn.addEventListener("click", function() {
// getting the current count of all elements matching
// the selector within the document:
const currentCount = getAll('.activityGroup').length,
// retrieving the next value from the
// generator function:
i = counter.next().value;
// if the currentCount is greater than 4 (again, using
// a Yoda condition):
if (4 < currentCount) {
// exit the function:
return false;
}
// creating a new <div>:
const activityGroup = create('div', {
// assigning the class-name of 'activityGroup':
className: 'activityGroup'
}),
// creating a new <button>:
removeButton = create('button', {
// assigning the class-name of 'delBtn',
className: 'delBtn',
// and the text-content of 'X':
textContent: 'X'
}),
input = create('input', {
type: 'text',
id: `activity_${i}`,
placeholder: `Activity ${i}`
}),
textarea = create('textarea', {
id: `activity_description_${i}`,
placeholder: `Activity description ${i}`
});
// binding the remove() function as the'click' event-handler
// on the removeButton:
removeButton.addEventListener('click', remove);
// using the HTMLElement.dataset API to set the
// custom data-count attribute to the current
// value of the i variable (for use in the CSS):
activityGroup.dataset.count = i;
// using Element.append() to append multiple elements
// in one call:
activityGroup.append(input, textarea, removeButton);
activity.appendChild(activityGroup);
});
/* caching common values (useful for theming and
maintenance): */
:root {
--bg-primary: hsl(0 0% 0% / 0.3);
--columns: 2;
--fs: 16px;
--spacing: 0.5em;
}
/* a simple CSS reset to remove default
margins and padding, and to have all
elements sized in a way that includes
their padding and border-widths in the
assigned size: */
*,::before,::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-size: var(--fs);
}
#activity {
/* using CSS Grid for layout: */
display: grid;
/* setting a space - held in the custom
property - between adjacent elements: */
gap: var(--spacing);
/* creating a number of columns (equal to the value held
in the --columns property) of equal width, each of one
fraction of the remaining space) */
grid-template-columns: repeat(var(--columns), 1fr);
padding: var(--spacing);
}
.activityGroup {
/* aligning an element in the grid to the start of that
grid (alignment is on the cross-axis of the inline-
axis, the inline-axis being the writing direction
of the language of the user's browser): */
align-self: start;
border: 0.2em solid var(--bg-primary);
display: grid;
gap: inherit;
padding: var(--spacing);
}
.activityGroup::before {
background-color: var(--bg-primary);
/* setting the content of the ::before pseudo element to the
string of 'Activity ' concatenated with the attribute-value
of the data-count attribute: */
content: "Activity " attr(data-count);
display: block;
/* spanning all three grid-columns: */
grid-column: span 3;
/* applying padding to the start of the inline axis: */
padding-inline-start: var(--spacing);
/* moving the element outwards with negative margins,
to place the content against the outer edges of the
element, despite the spacing on that element: */
margin-block-start: calc(-1 * var(--spacing));
margin-inline: calc(-1 * var(--spacing));
}
input,
textarea {
grid-column: span 3;
padding: 0.5em;
}
textarea {
min-block-size: 5em;
resize: vertical;
}
.delBtn {
color: red;
grid-column: 3;
}
/* using a simple media query to modify the values of the CSS Custom
properties as required to improve the experience on different
devices: */
#media screen and (max-width: 700px) {
:root {
--columns: 1;
--fs: 18px;
}
}
<!-- we don't appear to be using jQuery, so I removed that <script> element -->
<!-- switching id and classes to use camelCase rather than
having arbitrary mixed-case format; choose a style you
prefer, but remember that consistency matters more than
the choice you make: -->
<div id="activity">
<div class="activityGroup">
<input placeHolder="Type your activity" />
<!-- removed the <br> elements, as well as the "rows" and "cols"
attributes from the <textarea>: -->
<textarea placeHolder="Type your description"></textarea>
<button class="delBtn">X</button>
</div>
<!-- Remove button -->
</div>
<!-- changed the id from "add" to "addBtn," purely for consistency
but, again, make your own choice on that: -->
<button id="addBtn">Add Activity</button>
As to your question:
How can I [add an] entire section and...control the maximum number of activities inserted[?]
Controlling the number of total activities/sections on the page is relatively easy and simply requires you to check the number of existing sections before adding a new one:
const addActivity = document.getElementById("add");
var i = 0;
const activityDiv = document.getElementById("Activity");
addActivity.addEventListener("click", function() {
// using document.querySelectorAll() to retrieve a NodeList of all
// elements matching the supplied CSS selector, and then retrieving
// the length of that NodeList:
let currentCount = document.querySelectorAll('.activityGroup').length;
// using a "Yoda condition" to see if the currentCount is greater than
// 4 (this approach guards against the most likely error of an assessment,
// that of accidentally assigning a value instead of comparing); if it is:
if (4 < currentCount) {
// we exit the function here:
return false;
}
i++;
const newspan = document.createElement('div');
newspan.className = "activityGroup";
// creating a <br> element:
const br = document.createElement('br');
const removeButton = document.createElement('button');
removeButton.addEventListener('click', function(e) {
e.target.closest(".activityGroup").remove();
});
removeButton.className = "delbtn";
removeButton.innerHTML = "X";
//
const txtfield = document.createElement('input');
const txtarea = document.createElement('textarea');
//
txtfield.id = 'activity_' + i;
txtfield.placeholder = "Activity " + i;
newspan.appendChild(txtfield);
// appending a clone of the <br> element:
newspan.appendChild(br.cloneNode());
// appending the <br> element:
newspan.appendChild(br);
//
newspan.appendChild(txtarea);
txtarea.id = 'activity_description_' + i;
txtarea.placeholder = "Activity Description " + i;
// setting the rows and cols attributes:
txtarea.setAttribute('rows', 5);
txtarea.setAttribute('cols', 50);
// appending white-space (approximating what's in the HTML):
newspan.append(document.createTextNode('\n '));
newspan.appendChild(removeButton);
activityDiv.appendChild(newspan);
});
.delbtn {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="Activity">
<div class="activityGroup">
<input placeHolder="Type your activity" />
<br>
<br>
<textarea rows="5" cols="50" placeHolder="Type your descvription"></textarea>
<button class="delbtn">X</button>
</div>
<!-- Remove button -->
</div>
<button id="add">Add Activity</button>
Now, I'd like to improve the above functions as follows with explanatory comments in the code itself:
const addActivity = document.getElementById("add");
var i = 0;
const activityDiv = document.getElementById("Activity");
addActivity.addEventListener("click", function() {
// using document.querySelectorAll() to retrieve a NodeList of all
// elements matching the supplied CSS selector, and then retrieving
// the length of that NodeList:
let currentCount = document.querySelectorAll('.activityGroup').length;
// using a "Yoda condition" to see if the currentCount is greater than
// 4 (this approach guards against the most likely error of an assessment,
// that of accidentally assigning a value instead of comparing); if it is:
if (4 < currentCount) {
// we exit the function here:
return false;
}
i++;
const newspan = document.createElement('div');
newspan.className = "activityGroup";
// creating a <br> element:
const br = document.createElement('br');
const removeButton = document.createElement('button');
removeButton.addEventListener('click', function(e) {
e.target.closest(".activityGroup").remove();
});
removeButton.className = "delbtn";
removeButton.innerHTML = "X";
//
const txtfield = document.createElement('input');
const txtarea = document.createElement('textarea');
//
txtfield.id = 'activity_' + i;
txtfield.placeholder = "Activity " + i;
newspan.appendChild(txtfield);
// appending a clone of the <br> element:
newspan.appendChild(br.cloneNode());
// appending the <br> element:
newspan.appendChild(br);
//
newspan.appendChild(txtarea);
txtarea.id = 'activity_description_' + i;
txtarea.placeholder = "Activity Description " + i;
// setting the rows and cols attributes:
txtarea.setAttribute('rows', 5);
txtarea.setAttribute('cols', 50);
// appending white-space (approximating what's in the HTML):
newspan.append(document.createTextNode('\n '));
newspan.appendChild(removeButton);
activityDiv.appendChild(newspan);
});
.delbtn {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="Activity">
<div class="activityGroup">
<input placeHolder="Type your activity" />
<br>
<br>
<textarea rows="5" cols="50" placeHolder="Type your descvription"></textarea>
<button class="delbtn">X</button>
</div>
<!-- Remove button -->
</div>
<button id="add">Add Activity</button>
JS Fiddle demo.
References:
CSS:
align-self.
box-sizing.
content.
CSS custom properties.
CSS logical properties.
display.
gap.
grid-column.
grid-template-columns.
grid-template-rows.
inline-size.
margin.
margin-block.
margin-inline.
min-block-size.
padding.
padding-block.
padding-inline.
repeat().
var().
JavaScript:
document.createElement().
document.querySelector().
document.querySelectorAll().
Element.append().
Element.querySelector().
Element.querySelectorAll().
Element.remove().
Event.target.
Event.currentTarget.
EventTarget.addEventListener().
Generator functions.
HTMLElement.dataset.
Node.appendChild().
Object.assign().
yield.
Do you mean "If I have Activity 1, 2, 3, 4, 5 and Delete 3 I want to be able to get Activities 1, 2, 4, 5?"
From what I've gathered, you want a button that adds and deletes activities but you only want it to work when previous activity isn't empty.
In the javascript, when you handle the buttons click event check previous activity, if it's empty, return, if it isn't empty, execute the add/delete code.

How to programmatically append a newly created div to an existing one?

I have some trouble with appending a new div to an existing parent that I just created. After creation of the parent I check it's existence. But when I want to append the child to it after selecting the parent via it's id I get an error. What do I do wrong?
var uiDiv = document.createElement("div");
uiDiv.id = "f_jsonuiDiv";
uiDiv.innerHTML = "jsonUI controls";
console.log("uiDiv");
console.dir(uiDiv); //shows uiDiv object
//select container div
const parentId = "f_jsonuiDiv"; //which is the id of the newly created container div
console.log("parentId: ",parentId);
var parElement = document.getElementById(parentId);
console.log("parElement: ");
console.dir(parElement); //says: null !
//create directly
//const newDiv = parElement.createElement("div"); //throws error as parElement does not exist ......
//create first, then append
const newDiv = document.createElement("div");
newDiv.innerHTML = "NEW DIV";
//parElement.appendChild(newDiv); //throws error as parElement does not exist ......
uiDiv.appendChild(newDiv); //does not throw an error ```
Seems like you need to add uiDiv to body (or any other parent) first, in order to get it with getElementById
document.body.appendChild(uiDiv);
// This should be valid now
const parElement = document.getElementById(parentId);
You need to put the script after the body let the DOM be created .
Or warp your code with
window.addEventListener('DOMContentLoaded', (event) => { //put your code here });
it will run after the page is loaded
A would advise to use insertAdjacentElement and insertAdjacentHTML. It makes your life easier.
// insertAdjacentElement returns the newly created element
const uiDiv = document.body.insertAdjacentElement(`beforeend`,
Object.assign(document.createElement("div"), {
id: "f_jsonuiDiv",
innerHTML: "jsonUI controls" })
);
// so now you can inject it wit some html
uiDiv.insertAdjacentHTML(`beforeend`,`<div>HI, I am the NEW DIV in town</div>`);
#f_jsonuiDiv div {
color: red;
padding: 2px 1em;
border: 1px solid #AAA;
max-width: 400px;
text-align: center;
}

JavaScript replace works in console, but not on the website

I've been trying to create a list of tags and separate them using commas. I'm using Webflow and sadly it's not possible in their cms.
I thought of a workaround where I would replace the commas with code using JavaScript.
Here's the code:
function tags() {
var tag = document.getElementById("tag__wrap").innerHTML;
str = tag.replace(/,/g, '</p><p class="tag__wrap">');
}
tags();
console.log(str);
For some reason the code works fine when I look it up in the console, but doesn't actually show anything on the actual website.
Any thoughts?
If your goal is to create multiple elements in place of the single element (that has the comma separated tags), then you need to manipulate the DOM. It is not enough to assign HTML to a string variable.
There are many ways to do this. Here is one:
function tags() {
var elem = document.getElementById("tag__wrap");
var tags = elem.textContent.match(/[^,\s]+/g) || [];
elem.textContent = tags.shift();
for (let text of tags) {
var newElem = elem.cloneNode();
newElem.textContent = text;
elem.insertAdjacentElement("afterend", newElem);
elem = newElem;
}
}
// Delay the change, so you can see before & after in the snippet
setTimeout(tags, 1000);
#tag__wrap {
background: lightblue;
display: inline-block;
margin: 5px;
padding: 3px 8px;
border-radius: 4px;
}
<p id="tag__wrap">algorithm,javascript,html,css</p>

How do I restrict the style of the text pasted in a contenteditable area?

I came across this Stack Overflow post where was discussed the exact thing I needed: to be able to paste text into a contenteditable area, retaining only a few styles. I ran the code snippet there and it work fine. However, when I tried it on my page, all styles were being removed, including those I wanted to keep, like bold and italic. After comparing the codes and a few experimentations, I realized that the reason it was not working was because I was using external CSS, instead of inline.
Is there any way I can make it work with external CSS? I will never know the origin of the text users will post in that contenteditable, and how was style applied to it, so I am looking to address all possibilities.
Also, is there a way to make it work with dragged and dropped text, instead of just pasted text? I tried replacing the event it is listening to from "paste" to "drop", but I get the error e.clipboardData is undefined
const el = document.querySelector('p');
el.addEventListener('paste', (e) => {
// Get user's pasted data
let data = e.clipboardData.getData('text/html') ||
e.clipboardData.getData('text/plain');
// Filter out everything except simple text and allowable HTML elements
let regex = /<(?!(\/\s*)?(b|i|em|strong|u)[>,\s])([^>])*>/g;
data = data.replace(regex, '');
// Insert the filtered content
document.execCommand('insertHTML', false, data);
// Prevent the standard paste behavior
e.preventDefault();
});
.editable {
width: 100%;
min-height: 20px;
font-size: 14px;
color: black;
font-family: arial;
line-height: 1.5;
border: solid 1px black;
margin-bottom: 30px;
}
.big {
font-size: 20px;
}
.red {
color: red;
}
.bold {
font-weight: bold;
}
.italic {
text-decoration: italic;
}
<p class="editable" contenteditable></p>
<p class="notEditable">
Try pasting this paragraph into the contenteditable paragraph above. This text includes <b>BOLD</b>, <i>ITALIC</i>, <s>STRIKE</s>, <u>UNDERLINE</u>, a <a href='#'>LINK</a>, and <span style="font-size:30px; color:red; font-family:Times New Roman">a few other styles.</span> All styles are inline, and it works as expected.
</p>
<p>Now, try pasting this paragraph with external styles. <span class="big">Big</span > <span class="red">red</span> <span class="bold">bold</span> <span class="italic">italic</span>. It no longer works.</p>
As other answer pointed out I don't know any way of getting CSS styles out of other pages using clipboard. . But at your own you could do something like this:
Get getComputedStyle (CSS only) of all elements filter out wanted style, in this example fontStyle and fontWeight. Then you can condition if fontStyle==="italic" or fontweight==="700" (bold), textDecoration==="underline rgb(0, 0, 0)" and wrap that elements into its HTML tags.
You do this because your regex function is only targeting tags, not even inline CSS property font-style: italic;. Witch is a shame, it would make things a bit easier as you could just read every elements CSS class style and apply it inline, but this way you need to condition it and apply HTML tag.
if ( style.fontStyle==="italic"){
element.innerHTML = "<i>" + element.innerHTML + "</i>";
;}
if ( style.fontWeight==="700"){
element.innerHTML = "<b>" + element.innerHTML + "</b>";
;}
if (style.textDecoration==="underline rgb(0, 0, 0)"){
element.innerHTML = "<u>" + element.innerHTML + "</u>";
;}
In example below if you copy Now, try pasting this paragraph with external styles. Big red bold italic. It no longer works. you will get bold,underline and italic. You can do the same for rest of your filtering options.
const el = document.querySelector('p');
el.addEventListener('paste', (e) => {
// Get user's pasted data
let data = e.clipboardData.getData('text/html') ||
e.clipboardData.getData('text/plain');
//console.log(data)
// Filter out everything except simple text and allowable HTML elements
let regex = /<(?!(\/\s*)?(b|i|em|strong|u)[>,\s])([^>])*>/g;
data = data.replace(regex, '');
//console.log(data)
// Insert the filtered content
document.execCommand('insertHTML', false, data);
// Prevent the standard paste behavior
e.preventDefault();
});
[...document.querySelectorAll('body *')].forEach(element=>{
const style = getComputedStyle(element)
if ( style.fontStyle==="italic"){
element.innerHTML = "<i>" + element.innerHTML + "</i>";
;}
if ( style.fontWeight==="700"){
element.innerHTML = "<b>" + element.innerHTML + "</b>";
;}
if (style.textDecoration==="underline rgb(0, 0, 0)"){
element.innerHTML = "<u>" + element.innerHTML + "</u>";
;}
});
.editable {
width: 100%;
min-height: 20px;
font-size: 14px;
color: black;
font-family: arial;
line-height: 1.5;
border: solid 1px black;
margin-bottom: 30px;
}
.big {
font-size: 20px;
}
.red {
color: red;
}
.bold {
font-weight: bold;
}
.underline{
text-decoration: underline;
}
.italic {
font-style: italic;
}
<p class="editable" contenteditable></p>
<p class="notEditable">
Try pasting this paragraph into the contenteditable paragraph above. This text includes <b>BOLD</b>, <i>ITALIC</i>, <s>STRIKE</s>, <u>UNDERLINE</u>, a <a href='#'>LINK</a>, and <span style="font-size:30px; color:red; font-family:Times New Roman">a few other styles.</span> All styles are inline, and it works as expected.
</p>
<p id="container"><span class="underline">Now</span>, try pasting this paragraph with external styles. <span class="big">Big</span > <span class="red">red</span> <span class="bold" >bold</span> <span class="italic">italic</span>. It no longer works.</p>
Unfortunately, there is no way to keep the properties of a class from an external source. If you would print the content of the clipboard, you will see that you receive the raw HTML content as it is on the external page, for example:
<div class="some-class">this is the text</div>
The class properties would not be inlined by the browser! And as the content is from an external source, you have no power over it.
On the other hand, if the content is from your page (so the class is defined), you could parse the received HTML and filter the CSS properties, keeping only what you want. Here you have a code sample using vanilla Javascript, no libraries required (also available on Codepen):
const targetEditable = document.querySelector('p');
targetEditable.addEventListener('paste', (event) => {
let data = event.clipboardData.getData('text/html') ||
event.clipboardData.getData('text/plain');
// Filter the string using your already existing rules
// But allow <p> and <div>
let regex = /<(?!(\/\s*)?(div|b|i|em|strong|u|p)[>,\s])([^>])*>/g;
data = data.replace(regex, '');
const newElement = createElementFromHTMLString(data);
const cssContent = generateFilteredCSS(newElement);
addCssToDocument(cssContent);
document.execCommand('insertHTML', false, newElement.innerHTML);
event.preventDefault();
});
// Scan the HTML elements recursively and generate CSS classes containing only the allowed properties
function generateFilteredCSS(node) {
const newClassName = randomString(5);
let content = `.${newClassName}{\n`;
if (node.className !== undefined && node.className !== '') {
// Get an element that has the class
const elemOfClass = document.getElementsByClassName(node.className)[0];
// Get the computed style properties
const styles = window.getComputedStyle(elemOfClass);
// Properties whitelist, keep only those
const propertiesToKeep = ['font-weight'];
for (const property of propertiesToKeep) {
content += `${property}: ${styles.getPropertyValue(property)};\n`;
}
}
content += '}\n';
node.className = newClassName;
for (const child of node.childNodes) {
content += generateFilteredCSS(child);
}
return content;
}
function createElementFromHTMLString(htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div;
}
function addCssToDocument(cssContent) {
var element = document.createElement("style");
element.innerHTML = cssContent;
var header = document.getElementsByTagName("HEAD")[0];
header.appendChild(element);
}
function randomString(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
.editable {
width: 100%;
min-height: 20px;
font-size: 14px;
color: black;
font-family: arial;
line-height: 1.5;
border: solid 1px black;
margin-bottom: 30px;
}
.red-bg {
background-color: red;
font-weight: bold;
}
<p class="editable" contenteditable></p>
<p class="red-bg test">
This is some text
</p>
About the drag and drop functionality, you have to use event.dataTransfer.getData() in the drop event listener, the rest is the same.
References
How to generate a DOM element from a HTML string
How to add CSS classes at runtime using Javascript
How to generate a random string (unique ID) in Javascript
Drag&drop data transfer
You could accomplish what you want, but would require an additional set of considerations.
First you add an "Add Source Assets" button, which the user would supply the source page URL... Then you would fetch the source HTML and match the pasted content, with the source content, then interrogate the source elements for all related attributes, referenced classes etc.
You could even process the source URL's markup to extract it's css and images as references... Essentially onboarding the resources based on a whitelist of acceptable media and styles.
Depending on your needs and DOM Kung Fu, you could extract all "consumed" css and import those styles... It really depends how far you want to go.

Word counter ignoring words after carriage return

I have this:
var editor = document.getElementById('editor');
var words = document.querySelector('.words');
function wordsCount() {
var arr = editor.textContent.trim().replace(/\s+/g, ' ').split(' ');
words.textContent = !arr[0] ? 0 : arr.length;
}
editor.addEventListener('input', wordsCount);
wordsCount();
body {
width: max-content;
font-family: 'Helvetica';
}
section {
outline: 2px solid #BFDEFF;
}
aside {
background-color: silver;
}
<section id="editor" contenteditable="true">Default text</section>
<aside class="words"></aside>
The problem is that the script ignores words after a carriage return. So, if I type,
Example
Example
Example
it only counts one word instead of three. Any help?
There are no line breaks to begin with. Let's see what happens when you enter those three lines:
Example
Example
Example
The editor element's HTML now looks as follows:
<section id="editor" contenteditable="true">Example<div>Example</div><div>Example</div></section>
Instead of line breaks, new lines are wrapped by <div> elements.
Now, accessing editor.textContent returns the concatenation of the textContent property value of all child nodes. Thus, it returns ExampleExampleExample.
To fix your problem, you could replace editor.textContent with editor.innerText or simply use a <textarea> instead.
const editor = document.getElementById('editor');
const output = document.getElementById('counter');
editor.addEventListener('input', event => {
output.textContent = (editor.value.match(/\S+/g) || []).length;
});
<textarea id='editor'></textarea>
<output id='counter'></output>
Try this instead,
function wordsCount() {
var arr = editor.innerText.trim().match(/\S+/g); //use innerText instead
words.textContent = !arr[0] ? 0 : arr.length;
}

Categories