Created
October 7, 2015 03:56
-
-
Save kensnyder/9c9bb4a8f7bd05c183e6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
tinymceOptions = { | |
... | |
paste_retain_style_properties: '', | |
paste_postprocess: pastePostProcess | |
}; | |
function pastePostProcess(plugin, args) { | |
cleanElementChildren(args.node); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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