7

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> 
MikeMichaels
  • 454
  • 1
  • 6
  • 25
  • 2
    When we copy from VSCode to Google Docs, it works exactly as intended. Can help for research. – doğukan Jul 25 '20 at 17:37
  • 2
    "_I am looking to address all possibilities_" There's no generic way to address all possibilities. You can't get the external CSS used on the source via the clipboard, the information is simply not stored. – Teemu Jul 28 '20 at 14:15
  • 1
    @dgknca is correct - try copying the 3rd paragraph in OP's runnable snippet to Google Docs - so perhaps the data is stored after all. On a side note, I remember working on a WYSIWYG (I think we forked trumbowyg) in a previous project that IIRC does this functionality - will have to verify though – 95faf8e76605e973 Jul 31 '20 at 12:11
  • Thank you all for the help. After researching and experimenting a lot, I came to terms with being impossible to achieve what I would like to, as things are, right now. I will award the bounty to the answer that came closer to it. – MikeMichaels Aug 01 '20 at 08:32
  • @95faf8e76605e973 I tired copying 3rd paragraph in gogole DOC on MAC OSX with Firefox and Chrome, it does not work. – ikiK Aug 01 '20 at 15:02

3 Answers3

2

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>
ikiK
  • 6,328
  • 4
  • 20
  • 40
  • It is an interesting approach. But it only fixes half the problem - pasting text from my own page. It still does not work when the text origin is a different website with external css. – MikeMichaels Aug 01 '20 at 08:19
  • @MikeMichaels I tried to research more but couldn't find anything. I got an idea that if you could make user to give source and load it in iframe, to run upeer script in iframe, but there is blocking of cross-origin, couldn't find a way to bypass that. Not sure if what ask for is even possible. – ikiK Aug 01 '20 at 15:06
1

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

  1. How to generate a DOM element from a HTML string
  2. How to add CSS classes at runtime using Javascript
  3. How to generate a random string (unique ID) in Javascript
  4. Drag&drop data transfer
Mihail Feraru
  • 1,419
  • 9
  • 17
0

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.

rexfordkelly
  • 1,623
  • 10
  • 15