UNPKG

upfront-editable

Version:
430 lines (337 loc) 14.4 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.tidyHtml = tidyHtml; exports.normalizeTags = normalizeTags; exports.normalizeWhitespace = normalizeWhitespace; exports.cleanInternals = cleanInternals; exports.extractContent = extractContent; exports.getInnerHtmlOfFragment = getInnerHtmlOfFragment; exports.createFragmentFromString = createFragmentFromString; exports.adoptElement = adoptElement; exports.cloneRangeContents = cloneRangeContents; exports.unwrapInternalNodes = unwrapInternalNodes; exports.getTags = getTags; exports.getTagsByName = getTagsByName; exports.getTagsByNameAndAttributes = getTagsByNameAndAttributes; exports.areSameAttributes = areSameAttributes; exports.getInnerTags = getInnerTags; exports.getTagNames = getTagNames; exports.isAffectedBy = isAffectedBy; exports.selectNodeContents = selectNodeContents; exports.isExactSelection = isExactSelection; exports.expandTo = expandTo; exports.toggleTag = toggleTag; exports.isWrappable = isWrappable; exports.forceWrap = forceWrap; exports.wrap = wrap; exports.unwrap = unwrap; exports.removeFormattingElem = removeFormattingElem; exports.removeFormatting = removeFormatting; exports.nuke = nuke; exports.nukeElem = nukeElem; exports.insertCharacter = insertCharacter; exports.surround = surround; exports.deleteCharacter = deleteCharacter; exports.containsString = containsString; exports.nukeTag = nukeTag; var _jquery = _interopRequireDefault(require("jquery")); var _rangy = _interopRequireDefault(require("rangy")); var nodeType = _interopRequireWildcard(require("./node-type")); var rangeSaveRestore = _interopRequireWildcard(require("./range-save-restore")); var parser = _interopRequireWildcard(require("./parser")); var string = _interopRequireWildcard(require("./util/string")); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function restoreRange(host, range, func) { range = rangeSaveRestore.save(range); func(); return rangeSaveRestore.restore(host, range); } var zeroWidthSpace = /\u200B/g; var zeroWidthNonBreakingSpace = /\uFEFF/g; var whitespaceExceptSpace = /[^\S ]/g; // Clean up the Html. function tidyHtml(element) { // if (element.normalize) element.normalize() normalizeTags(element); } // Remove empty tags and merge consecutive tags (they must have the same // attributes). // // @method normalizeTags // @param {HTMLElement} element The element to process. function normalizeTags(element) { var fragment = document.createDocumentFragment(); Array.prototype.forEach.call(element.childNodes, function (node) { // skip empty tags, so they'll get removed if (node.nodeName !== 'BR' && !node.textContent) return; if (node.nodeType === nodeType.elementNode && node.nodeName !== 'BR') { var sibling = node; while ((sibling = sibling.nextSibling) !== null) { if (!parser.isSameNode(sibling, node)) break; Array.from(sibling.childNodes).forEach(function (siblingChild) { node.appendChild(siblingChild.cloneNode(true)); }); sibling.remove(); } normalizeTags(node); } fragment.appendChild(node.cloneNode(true)); }); while (element.firstChild) { element.removeChild(element.firstChild); } element.appendChild(fragment); } function normalizeWhitespace(text) { return text.replace(whitespaceExceptSpace, ' '); } // Clean the element from character, tags, etc... added by the plugin logic. // // @method cleanInternals // @param {HTMLElement} element The element to process. function cleanInternals(element) { // Uses extract content for simplicity. A custom method // that does not clone the element could be faster if needed. element.innerHTML = extractContent(element, true); } // Extracts the content from a host element. // Does not touch or change the host. Just returns // the content and removes elements marked for removal by editable. // // @param {DOM node or document fragment} Element where to clean out the innerHTML. // If you pass a document fragment it will be empty after this call. // @param {Boolean} Flag whether to keep ui elements like spellchecking highlights. // @returns {String} The cleaned innerHTML of the passed element or document fragment. function extractContent(element, keepUiElements) { var innerHtml = (element.nodeType === nodeType.documentFragmentNode ? getInnerHtmlOfFragment(element) : element.innerHTML).replace(zeroWidthNonBreakingSpace, '') // Used for forcing inline elements to have a height .replace(zeroWidthSpace, '<br>'); // Used for cross-browser newlines var clone = document.createElement('div'); clone.innerHTML = innerHtml; unwrapInternalNodes(clone, keepUiElements); return clone.innerHTML; } function getInnerHtmlOfFragment(documentFragment) { var div = document.createElement('div'); div.appendChild(documentFragment); return div.innerHTML; } // Create a document fragment from an html string // @param {String} e.g. 'some html <span>text</span>.' function createFragmentFromString(htmlString) { var fragment = document.createDocumentFragment(); (0, _jquery["default"])('<div>').html(htmlString).contents().each(function (i, el) { fragment.appendChild(el); }); return fragment; } function adoptElement(node, doc) { return node.ownerDocument !== doc ? doc.adoptNode(node) : node; } // This is a slight variation of the cloneContents method of a rangyRange. // It will return a fragment with the cloned contents of the range // without the commonAncestorElement. // // @param {rangyRange} // @return {DocumentFragment} function cloneRangeContents(range) { var rangeFragment = range.cloneContents(); var parent = rangeFragment.childNodes[0]; var fragment = document.createDocumentFragment(); while (parent.childNodes.length) { fragment.appendChild(parent.childNodes[0]); } return fragment; } // Remove elements that were inserted for internal or user interface purposes // // @param {DOM node} // @param {Boolean} whether to keep ui elements like spellchecking highlights // Currently: // - Saved ranges function unwrapInternalNodes(sibling, keepUiElements) { while (sibling) { var nextSibling = sibling.nextSibling; if (sibling.nodeType !== nodeType.elementNode) { sibling = nextSibling; continue; } var attr = sibling.getAttribute('data-editable'); if (sibling.firstChild) unwrapInternalNodes(sibling.firstChild, keepUiElements); if (attr === 'remove' || attr === 'ui-remove' && !keepUiElements) { (0, _jquery["default"])(sibling).remove(); } if (attr === 'unwrap' || attr === 'ui-unwrap' && !keepUiElements) { unwrap(sibling); } sibling = nextSibling; } } // Get all tags that start or end inside the range function getTags(host, range, filterFunc) { var tags = getInnerTags(range, filterFunc); // get all tags that surround the range var node = range.commonAncestorContainer; while (node !== host) { if (!filterFunc || filterFunc(node)) tags.push(node); node = node.parentNode; } return tags; } function getTagsByName(host, range, tagName) { return getTags(host, range, function (node) { return node.nodeName.toUpperCase() === tagName.toUpperCase(); }); } function getTagsByNameAndAttributes(host, range, elem) { return getTags(host, range, function (node) { return node.nodeName.toUpperCase() === elem.nodeName.toUpperCase() && areSameAttributes(node.attributes, elem.attributes); }); } function areSameAttributes(attrs1, attrs2) { if (attrs1.length !== attrs2.length) return false; for (var i = 0; i < attrs1.length; i++) { var attr = attrs2[attrs1[i].name]; if (!(attr && attr.value === attrs1[i].value)) return false; } return true; } // Get all tags that start or end inside the range function getInnerTags(range, filterFunc) { return range.getNodes([nodeType.elementNode], filterFunc); } // Transform an array of elements into an array // of tagnames in uppercase // // @return example: ['STRONG', 'B'] function getTagNames() { var elements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; return elements.map(function (element) { return element.nodeName; }); } function isAffectedBy(host, range, tagName) { return getTags(host, range).some(function (elem) { return elem.nodeName === tagName.toUpperCase(); }); } // select a whole element function selectNodeContents(element) { var range = _rangy["default"].createRange(); range.selectNodeContents(element); return range; } // Check if the range selects all of the elements contents, // not less or more. // // @param visible: Only compare visible text. That way it does not // matter if the user selects an additional whitespace or not. function isExactSelection(range, elem, visible) { var elemRange = _rangy["default"].createRange(); elemRange.selectNodeContents(elem); if (!range.intersectsRange(elemRange)) return false; var rangeText = range.toString(); var elemText = (0, _jquery["default"])(elem).text(); if (visible) { rangeText = string.trim(rangeText); elemText = string.trim(elemText); } return rangeText !== '' && rangeText === elemText; } function expandTo(host, range, elem) { range.selectNodeContents(elem); return range; } function toggleTag(host, range, elem) { var elems = getTagsByNameAndAttributes(host, range, elem); if (elems.length === 1 && isExactSelection(range, elems[0], 'visible')) { return removeFormattingElem(host, range, elem); } return forceWrap(host, range, elem); } function isWrappable(range) { return range.canSurroundContents(); } function forceWrap(host, range, elem) { var restoredRange = restoreRange(host, range, function () { nukeElem(host, range, elem); }); // remove all tags if the range is not wrappable if (!isWrappable(restoredRange)) { restoredRange = restoreRange(host, restoredRange, function () { nuke(host, restoredRange); }); } wrap(restoredRange, elem); return restoredRange; } function wrap(range, elem) { var el = string.isString(elem) ? (0, _jquery["default"])(elem).get(0) : elem; if (!isWrappable(range)) { console.log('content.wrap(): can not surround range'); return; } range.surroundContents(el); } function unwrap(elem) { var $elem = (0, _jquery["default"])(elem); var contents = $elem.contents(); contents.length ? contents.unwrap() : $elem.remove(); } function removeFormattingElem(host, range, elem) { return restoreRange(host, range, function () { nukeElem(host, range, elem); }); } function removeFormatting(host, range, tagName) { return restoreRange(host, range, function () { nuke(host, range, tagName); }); } // Unwrap all tags this range is affected by. // Can also affect content outside of the range. function nuke(host, range, tagName) { getTags(host, range).forEach(function (elem) { if (elem.nodeName.toUpperCase() !== 'BR' && (!tagName || elem.nodeName.toUpperCase() === tagName.toUpperCase())) { unwrap(elem); } }); } // Unwrap all tags this range is affected by. // Can also affect content outside of the range. function nukeElem(host, range, node) { getTags(host, range).forEach(function (elem) { if (elem.nodeName.toUpperCase() !== 'BR' && (!node || elem.nodeName.toUpperCase() === node.nodeName.toUpperCase() && areSameAttributes(elem.attributes, node.attributes))) { unwrap(elem); } }); } // Insert a single character (or string) before or after // the range. function insertCharacter(range, character, atStart) { var insertEl = document.createTextNode(character); var boundaryRange = range.cloneRange(); boundaryRange.collapse(atStart); boundaryRange.insertNode(insertEl); range[atStart ? 'setStartBefore' : 'setEndAfter'](insertEl); range.normalizeBoundaries(); } // Surround the range with characters like start and end quotes. // // @method surround function surround(host, range, startCharacter, endCharacter) { insertCharacter(range, endCharacter || startCharacter, false); insertCharacter(range, startCharacter, true); return range; } // Removes a character from the text within a range. // // @method deleteCharacter function deleteCharacter(host, range, character) { if (!containsString(range, character)) return range; range.splitBoundaries(); var restoredRange = restoreRange(host, range, function () { var charRegexp = string.regexp(character); range.getNodes([nodeType.textNode], function (node) { return node.nodeValue.search(charRegexp) >= 0; }).forEach(function (node) { node.nodeValue = node.nodeValue.replace(charRegexp, ''); }); }); restoredRange.normalizeBoundaries(); return restoredRange; } function containsString(range, str) { return range.toString().indexOf(str) >= 0; } // Unwrap all tags this range is affected by. // Can also affect content outside of the range. function nukeTag(host, range, tagName) { getTags(host, range).forEach(function (elem) { if (elem.nodeName.toUpperCase() === tagName.toUpperCase()) unwrap(elem); }); }