upfront-editable
Version:
Friendly contenteditable API
215 lines (172 loc) • 6.78 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _from = require('babel-runtime/core-js/array/from');
var _from2 = _interopRequireDefault(_from);
exports.updateConfig = updateConfig;
exports.paste = paste;
exports.injectPasteholder = injectPasteholder;
exports.parseContent = parseContent;
exports.filterHtmlElements = filterHtmlElements;
exports.conditionalNodeWrap = conditionalNodeWrap;
exports.filterAttributes = filterAttributes;
exports.transformNodeName = transformNodeName;
exports.hasRequiredAttributes = hasRequiredAttributes;
exports.shouldKeepNode = shouldKeepNode;
exports.cleanWhitespace = cleanWhitespace;
var _jquery = require('jquery');
var _jquery2 = _interopRequireDefault(_jquery);
var _config = require('./config');
var config = _interopRequireWildcard(_config);
var _string = require('./util/string');
var string = _interopRequireWildcard(_string);
var _nodeType = require('./node-type');
var nodeType = _interopRequireWildcard(_nodeType);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var allowedElements = void 0,
requiredAttributes = void 0,
transformElements = void 0,
blockLevelElements = void 0;
var splitIntoBlocks = void 0,
blacklistedElements = void 0;
var whitespaceOnly = /^\s*$/;
var blockPlaceholder = '<!-- BLOCK -->';
var keepInternalRelativeLinks = void 0;
updateConfig(config);
function updateConfig(config) {
var rules = config.pastedHtmlRules;
allowedElements = rules.allowedElements || {};
requiredAttributes = rules.requiredAttributes || {};
transformElements = rules.transformElements || {};
blacklistedElements = rules.blacklistedElements || [];
keepInternalRelativeLinks = rules.keepInternalRelativeLinks || false;
blockLevelElements = {};
rules.blockLevelElements.forEach(function (name) {
blockLevelElements[name] = true;
});
splitIntoBlocks = {};
rules.splitIntoBlocks.forEach(function (name) {
splitIntoBlocks[name] = true;
});
}
function paste(element, cursor, callback) {
var document = element.ownerDocument;
element.setAttribute(config.pastingAttribute, true);
if (cursor.isSelection) cursor = cursor.deleteContent();
// Create a placeholder and set the focus to the pasteholder
// to redirect the browser pasting into the pasteholder.
cursor.save();
var pasteHolder = injectPasteholder(document);
pasteHolder.focus();
// Use a timeout to give the browser some time to paste the content.
// After that grab the pasted content, filter it and restore the focus.
setTimeout(function () {
var blocks = parseContent(pasteHolder);
(0, _jquery2.default)(pasteHolder).remove();
element.removeAttribute(config.pastingAttribute);
cursor.restore();
callback(blocks, cursor);
}, 0);
}
function injectPasteholder(document) {
var pasteHolder = (0, _jquery2.default)('<div>').attr('contenteditable', true).css({
position: 'fixed',
right: '5px',
top: '50%',
width: '1px',
height: '1px',
overflow: 'hidden',
outline: 'none'
});
(0, _jquery2.default)(document.body).append(pasteHolder);
return pasteHolder.get(0);
}
/**
* - Parse pasted content
* - Split it up into blocks
* - clean and normalize every block
* - optionally strip the host location an anchorTag-href
* www.livindocs.io/internalLink -> /internalLink
*
* @param {DOM node} A container where the pasted content is located.
* @returns {Array of Strings} An array of cleaned innerHTML like strings.
*/
function parseContent(element) {
// Filter pasted content
return filterHtmlElements(element)
// Handle Blocks
.split(blockPlaceholder).map(function (entry) {
return string.trim(cleanWhitespace(entry));
}).filter(function (entry) {
return !whitespaceOnly.test(entry);
});
}
function filterHtmlElements(elem) {
return (0, _from2.default)(elem.childNodes).reduce(function (content, child) {
if (blacklistedElements.indexOf(child.nodeName.toLowerCase()) !== -1) {
return '';
}
// Keep internal relative links relative (on paste).
if (keepInternalRelativeLinks && child.nodeName === 'A' && child.href) {
var stripInternalHost = child.getAttribute('href').replace(window.location.origin, '');
child.setAttribute('href', stripInternalHost);
}
if (child.nodeType === nodeType.elementNode) {
var childContent = filterHtmlElements(child);
return content + conditionalNodeWrap(child, childContent);
}
// Escape HTML characters <, > and &
if (child.nodeType === nodeType.textNode) return content + string.escapeHtml(child.nodeValue);
return content;
}, '');
}
function conditionalNodeWrap(child, content) {
var nodeName = child.nodeName.toLowerCase();
nodeName = transformNodeName(nodeName);
if (shouldKeepNode(nodeName, child)) {
var attributes = filterAttributes(nodeName, child);
if (nodeName === 'br') return '<' + (nodeName + attributes) + '>';
if (!whitespaceOnly.test(content)) {
return '<' + (nodeName + attributes) + '>' + content + '</' + nodeName + '>';
}
return content;
}
if (splitIntoBlocks[nodeName]) {
return blockPlaceholder + content + blockPlaceholder;
}
// prevent missing whitespace between text when block-level
// elements are removed.
if (blockLevelElements[nodeName]) return content + ' ';
return content;
}
// returns string of concatenated attributes e.g. 'target="_blank" rel="nofollow" href="/test.com"'
function filterAttributes(nodeName, node) {
return (0, _from2.default)(node.attributes).reduce(function (attributes, _ref) {
var name = _ref.name,
value = _ref.value;
if (allowedElements[nodeName][name] && value) {
return attributes + ' ' + name + '="' + value + '"';
}
return attributes;
}, '');
}
function transformNodeName(nodeName) {
return transformElements[nodeName] || nodeName;
}
function hasRequiredAttributes(nodeName, node) {
var requiredAttrs = requiredAttributes[nodeName];
if (!requiredAttrs) return true;
return !requiredAttrs.some(function (name) {
return !node.getAttribute(name);
});
}
function shouldKeepNode(nodeName, node) {
return allowedElements[nodeName] && hasRequiredAttributes(nodeName, node);
}
function cleanWhitespace(str) {
return str.replace(/\n/g, ' ').replace(/ {2,}/g, ' ').replace(/(.)\u00A0/g, function (match, group) {
return group + (/[\u0020]/.test(group) ? '\xA0' : ' ');
});
}