Skip to content

Instantly share code, notes, and snippets.

@kensnyder
Created October 7, 2015 03:56
Show Gist options
  • Save kensnyder/9c9bb4a8f7bd05c183e6 to your computer and use it in GitHub Desktop.
Save kensnyder/9c9bb4a8f7bd05c183e6 to your computer and use it in GitHub Desktop.
/**
* Given a DOM node, clean children so the node is suitable for pasting in a rich text area
* @param {HTMLElement} root The DOM node to clean
* @param {Object} [options] Cleaning options
* @param {object} [options.classMap] A map of tagName to CSS class for assigning classes
* @returns {undefined}
*/
function cleanElementChildren(root, options) {
// options may or may not be given
options = options || {};
//
// processing steps
//
// 1. unwrap paragraphs
eachMatch(root, 'p', unwrapParagraph);
// 2. unrap all unwanted elements
walkElementNodes(root, normalizeElement);
// 3. remove element ids
walkElementNodes(root, cleanAttributes);
// 4. clean up whitespace a bit
walkTextNodes(root, normalizeWhitespace);
// 5. wrap any non-empty root text nodes with a p
eachChildNode(root, processRootNode);
// 6. remove any double <br> that get stuck at as the last child of <p>
eachMatch(root, 'br:last-child', removeBrIfLast);
eachMatch(root, 'br:last-child', removeBrIfLast);
// 7. set element classes as required
walkElementNodes(root, setCssClass);
//
// processing functions
//
// run a callback on all descendent nodes
function walkNodes(node, callback, whatToShow) {
// breadth-first walker
var child, list = [];
var walker = node.ownerDocument.createTreeWalker(node, whatToShow, null);
while ( (child = walker.nextNode()) ) {
list.push(child);
}
list.forEach(callback);
}
// run a callback on every child node
function eachChildNode(node, callback) {
[].slice.call(node.childNodes, 0).forEach(callback);
}
// handle root nodes
function processRootNode(node) {
var p;
if (node.nodeType === 3 && node.textContent.trim() !== '') {
// wrap root text nodes in a p
p = node.ownerDocument.createElement('p');
p.textContent = node.textContent;
node.parentNode.insertBefore(p, node);
removeNode(node);
}
else if (node.tagName && node.tagName.toLowerCase() == 'br') {
// no brs at the root level
removeNode(node);
}
}
// remove <br> if not followed by any text nodes
function removeBrIfLast(br) {
if (!br.nextSibling) {
removeNode(br);
}
}
// run a callback on all descendent nodes matching the given selector
function eachMatch(node, selector, callback) {
[].slice.call(node.querySelectorAll(selector), 0).forEach(callback);
}
// run a callback on all descendent text nodes
function walkTextNodes(node, callback) {
walkNodes(node, callback, NodeFilter.SHOW_TEXT);
}
// run a callback on all descendent element nodes
function walkElementNodes(node, callback) {
walkNodes(node, callback, NodeFilter.SHOW_ELEMENT);
}
// unwrap a <p> or change to a span with trailing <br><br>
function unwrapParagraph(p) {
if (hasNextSiblingElement(p)) {
var span = changeTagName(p, 'span');
span.appendChild(p.ownerDocument.createElement('br'));
span.appendChild(p.ownerDocument.createElement('br'));
}
else {
unwrap(p);
}
}
// get the nextSibling that is an element (not a text node)
function getNextSiblingElement(node) {
var current = node;
while ( (current = current.nextSibling) ) {
if (current.nodeType === 1) {
return current;
}
}
return false;
}
// true if there are any nextSibling element nodes
function hasNextSiblingElement(node) {
return !!getNextSiblingElement(node);
}
// 1. Remove form elements and other non-presentational elements
// 2. Replace <input> and <textarea> elements with text nodes
// 3. Unwrap any non-root-level elements (for disallowed tags)
// 4. Change root-level elements to p (for disallowed tags)
// 5. Leave alone unknown tags and allowed tags
function normalizeElement(node) {
// see list of tags here: http://www.quackit.com/html_5/tags/
var tag = node.tagName.toLowerCase();
if (
// strip form elements
( tag == 'input' && node.type.match(/^(radio|checkbox|hidden|file|image|password)$/) ) ||
// strip elements with no useful content
( tag.match(/^(audio|canvas|dialog|embed|frame|frameset|hr|link|map|meta|noframes|noscript|object|script|style|video)$/) ) ||
// strip empty <a> elements (e.g. anchor targets)
( tag.match(/^(a)$/) && node.textContent.trim() === '' )
) {
removeNode(node);
}
else if (tag.match(/^(select)$/)) {
// replace with a span containing value of form element
replaceWithText(node, node.hasAttribute('multiple') ? '' : node.options[node.selectedIndex].text || '');
}
else if (tag.match(/^(input|textarea)$/)) {
// replace with a text node containing value of form element
replaceWithText(node, node.value || node.getAttribute('placeholder') || '');
}
else if (
// linline elements
tag.match(/^(abbr|address|button|code|font|kbd|label|output|span|u)$/) ||
// block elements
tag.match(/^(article|aside|audio|blockquote|capture|center|div|fieldset|figcaption|figure|footer|form|header|hgroup|legend|nav|p|pre|section)$/)
) {
if (node.parentNode.parentNode && node.parentNode.parentNode.tagName.toLowerCase() != 'body') {
unwrap(node);
}
else {
// base level
changeTagName(node, 'p');
}
}
// leave alone unknown tags and the following
// b|bdi|bdo|br|cite|em|dd|del|dfn|div|dl|h1|h2|h3|h4|h5|h6|i|ins|li|ol|strong|table|tbody|tfoot|thead|tr|ul
}
// Remove a node from its parent
function removeNode(node) {
node.parentNode.removeChild(node);
}
// process attributes
function cleanAttributes(node) {
// remove all name and id attributes
node.removeAttribute('id');
node.removeAttribute('name');
// allow only percent widths
var width = node.getAttribute('width');
if (width && !width.match(/%\s*$/)) {
node.removeAttribute('width');
}
}
// Set the CSS class to the looked-up value
function setCssClass(node) {
var newClass = options.classMap ? (options.classMap[node.tagName.toLowerCase()] || '') : '';
node.className = newClass;
if (newClass === '') {
node.removeAttribute('class');
}
}
// Replace multiple whitespace characters in a node's textContent with a single space
function normalizeWhitespace(node) {
node.textContent = node.textContent.replace(/\s+/g, ' ');
}
// Change the tag name of a node
function changeTagName(node, tagName) {
var newNode = node.ownerDocument.createElement(tagName);
while (node.childNodes.length) {
// running insertBefore immediately updates node.childNodes
// so we can't use for or forEach
newNode.appendChild(node.childNodes[0]);
}
node.parentNode.replaceChild(newNode, node);
return newNode;
}
// Replace an element node with a text node with the given textContent
function replaceWithText(node, textContent) {
var text = node.ownerDocument.createTextNode(textContent);
node.parentNode.replaceChild(text, node);
}
// Move all children of this node to its parent then remove this node
function unwrap(node) {
var parent = node.parentNode;
while (node.childNodes.length) {
// running insertBefore immediately updates node.childNodes
// so we can't use for or forEach
parent.insertBefore(node.childNodes[0], node);
}
parent.removeChild(node);
}
}
tinymceOptions = {
...
paste_retain_style_properties: '',
paste_postprocess: pastePostProcess
};
function pastePostProcess(plugin, args) {
cleanElementChildren(args.node);
}
tinymceOptions = {
...
paste_retain_style_properties: '',
paste_postprocess: pastePostProcess
};
function pastePostProcess(plugin, args) {
var classMap = {
h1: 'heading-font',
h2: 'heading-font',
h3: 'heading-font',
h4: 'heading-font',
h5: 'heading-font',
h6: 'heading-font',
p: 'body-font p',
a: 'a',
th: 'body-font',
td: 'body-font',
li: 'body-font',
dd: 'body-font',
dl: 'body-font',
del:'body-font',
ins:'body-font'
};
cleanElementChildren(args.node, {
classMap: classMap
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment