draftjs-filters
Advanced tools
Comparing version 2.2.4 to 2.3.0
@@ -7,2 +7,8 @@ # Changelog | ||
# [2.3.0](https://github.com/thibaudcolas/draftjs-filters/compare/v2.2.4...v2.3.0) (2020-01-03) | ||
### Features | ||
- **api:** add `blockTextRules` parameter to options. Fix [#65](https://github.com/thibaudcolas/draftjs-filters/issues/65) ([#127](https://github.com/thibaudcolas/draftjs-filters/issues/127)) ([2850d72](https://github.com/thibaudcolas/draftjs-filters/commit/2850d7219abe8f17411c93376c62448accd1516d)) | ||
## [2.2.4](https://github.com/thibaudcolas/draftjs-filters/compare/v2.2.3...v2.2.4) (2019-10-11) | ||
@@ -9,0 +15,0 @@ |
@@ -14,7 +14,5 @@ // @flow | ||
var ORDERED_LIST_ITEM = "ordered-list-item"; | ||
var IMAGE = "IMAGE"; | ||
// @flow | ||
/** | ||
@@ -26,5 +24,7 @@ * Creates atomic blocks where they would be required for a block-level entity | ||
*/ | ||
var preserveAtomicBlocks = function preserveAtomicBlocks(content /*: ContentState*/) { | ||
var preserveAtomicBlocks = function preserveAtomicBlocks(content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var perservedBlocks = blockMap.filter(function (block) { | ||
@@ -34,3 +34,2 @@ var text = block.getText(); | ||
var shouldPreserve = entityKey && ["📷", " ", "📷 "].includes(text); | ||
return shouldPreserve; | ||
@@ -49,3 +48,2 @@ }).map(function (block) { | ||
}; | ||
/** | ||
@@ -55,6 +53,8 @@ * Resets atomic blocks to have a single-space char and no styles. | ||
*/ | ||
var resetAtomicBlocks = function resetAtomicBlocks(content /*: ContentState*/) { | ||
var resetAtomicBlocks = function resetAtomicBlocks(content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap; | ||
var normalisedBlocks = blocks.filter(function (block) { | ||
@@ -66,10 +66,7 @@ return block.getType() === ATOMIC && (block.getText() !== " " || block.getInlineStyleAt(0).size !== 0); | ||
var newChar = char; | ||
char.getStyle().forEach(function (type) { | ||
newChar = draftJs.CharacterMetadata.removeStyle(newChar, type); | ||
}); | ||
return newChar; | ||
}); | ||
return block.merge({ | ||
@@ -89,7 +86,11 @@ text: " ", | ||
}; | ||
/** | ||
* Removes atomic blocks for which the entity isn't whitelisted. | ||
*/ | ||
var removeInvalidAtomicBlocks = function removeInvalidAtomicBlocks(whitelist /*: $ReadOnlyArray<{ type: string }>*/, content /*: ContentState*/) { | ||
var removeInvalidAtomicBlocks = function removeInvalidAtomicBlocks(whitelist | ||
/*: $ReadOnlyArray<{ type: string }>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
@@ -103,9 +104,8 @@ | ||
var entityKey = block.getEntityAt(0); | ||
var isValid = void 0; | ||
var isValid; | ||
if (entityKey) { | ||
var _type = content.getEntity(entityKey).getType(); | ||
var type = content.getEntity(entityKey).getType(); | ||
isValid = whitelist.some(function (t) { | ||
return t.type === _type; | ||
return t.type === type; | ||
}); | ||
@@ -131,3 +131,2 @@ } else { | ||
// @flow | ||
/** | ||
@@ -137,3 +136,6 @@ * Removes blocks that have a non-zero depth, and aren't list items. | ||
*/ | ||
var removeInvalidDepthBlocks = function removeInvalidDepthBlocks(content /*: ContentState*/) { | ||
var removeInvalidDepthBlocks = function removeInvalidDepthBlocks(content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
@@ -143,3 +145,2 @@ | ||
var isListBlock = [UNORDERED_LIST_ITEM, ORDERED_LIST_ITEM].includes(block.getType()); | ||
return isListBlock || block.getDepth() === 0; | ||
@@ -158,3 +159,2 @@ }; | ||
}; | ||
/** | ||
@@ -167,9 +167,13 @@ * Changes block type and depth based on the block's text. – some word processors | ||
*/ | ||
var preserveBlockByText = function preserveBlockByText(rules /*: $ReadOnlyArray<{ | ||
test: string, | ||
type: string, | ||
depth: number, | ||
}>*/, content /*: ContentState*/) { | ||
var preserveBlockByText = function preserveBlockByText(rules | ||
/*: $ReadOnlyArray<{ | ||
test: string, | ||
type: string, | ||
depth: number, | ||
}>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap.filter(function (block) { | ||
@@ -180,4 +184,3 @@ return block.getType() === "unstyled"; | ||
var newBlock = block; | ||
var match = void 0; | ||
var match; | ||
var matchingRule = rules.find(function (rule) { | ||
@@ -190,11 +193,9 @@ match = new RegExp(rule.test).exec(text); | ||
var _text = block.getText(); | ||
var entity = block.getEntityAt(0); | ||
// Special case – do not convert the block if there is an entity at the start, and the matching text is the full block’s text. | ||
var entity = block.getEntityAt(0); // Special case – do not convert the block if there is an entity at the start, and the matching text is the full block’s text. | ||
// This can happen in Word for equations, which are injected as images with text "📷 ". | ||
if (entity && match[0] === _text) { | ||
return newBlock; | ||
} | ||
// Unicode gotcha: | ||
} // Unicode gotcha: | ||
// At the moment, Draft.js stores one CharacterMetadata in the character list | ||
@@ -206,8 +207,10 @@ // for each "character" in an astral symbol. "📷" has a length of 2, is stored with two CharacterMetadata instances. | ||
// See https://mathiasbynens.be/notes/javascript-unicode. | ||
var sliceOffset = match[0].length; | ||
// Maintain persistence in the list while removing chars from the start. | ||
var sliceOffset = match[0].length; // Maintain persistence in the list while removing chars from the start. | ||
// https://github.com/facebook/draft-js/blob/788595984da7c1e00d1071ea82b063ff87140be4/src/model/transaction/removeRangeFromContentState.js#L333 | ||
var chars = block.getCharacterList(); | ||
var startOffset = 0; | ||
while (startOffset < sliceOffset) { | ||
@@ -228,3 +231,2 @@ chars = chars.shift(); | ||
}); | ||
return blocks.size === 0 ? content : content.merge({ | ||
@@ -234,9 +236,12 @@ blockMap: blockMap.merge(blocks) | ||
}; | ||
/** | ||
* Resets the depth of all the content to at most max. | ||
*/ | ||
var limitBlockDepth = function limitBlockDepth(max /*: number*/, content /*: ContentState*/) { | ||
var limitBlockDepth = function limitBlockDepth(max | ||
/*: number*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var changedBlocks = blockMap.filter(function (block) { | ||
@@ -247,3 +252,2 @@ return block.getDepth() > max; | ||
}); | ||
return changedBlocks.size === 0 ? content : content.merge({ | ||
@@ -253,3 +257,2 @@ blockMap: blockMap.merge(changedBlocks) | ||
}; | ||
/** | ||
@@ -259,5 +262,9 @@ * Converts all block types not present in the whitelist to unstyled. | ||
*/ | ||
var filterBlockTypes = function filterBlockTypes(whitelist /*: $ReadOnlyArray<string>*/, content /*: ContentState*/) { | ||
var filterBlockTypes = function filterBlockTypes(whitelist | ||
/*: $ReadOnlyArray<string>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var changedBlocks = blockMap.filter(function (block) { | ||
@@ -271,3 +278,2 @@ return !whitelist.includes(block.getType()); | ||
}); | ||
return changedBlocks.size === 0 ? content : content.merge({ | ||
@@ -279,15 +285,16 @@ blockMap: blockMap.merge(changedBlocks) | ||
// @flow | ||
/** | ||
* Removes all styles not present in the whitelist. | ||
*/ | ||
var filterInlineStyles = function filterInlineStyles(whitelist /*: $ReadOnlyArray<string>*/, content /*: ContentState*/) { | ||
var filterInlineStyles = function filterInlineStyles(whitelist | ||
/*: $ReadOnlyArray<string>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap.map(function (block) { | ||
var altered = false; | ||
var chars = block.getCharacterList().map(function (char) { | ||
var newChar = char; | ||
char.getStyle().filter(function (type) { | ||
@@ -299,9 +306,6 @@ return !whitelist.includes(type); | ||
}); | ||
return newChar; | ||
}); | ||
return altered ? block.set("characterList", chars) : block; | ||
}); | ||
return content.merge({ | ||
@@ -313,3 +317,2 @@ blockMap: blockMap.merge(blocks) | ||
// @flow | ||
/** | ||
@@ -320,9 +323,10 @@ * Clones entities in the entityMap, so each range points to its own entity instance. | ||
*/ | ||
var cloneEntities = function cloneEntities(content /*: ContentState*/) { | ||
var cloneEntities = function cloneEntities(content | ||
/*: ContentState*/ | ||
) { | ||
var newContent = content; | ||
var blockMap = newContent.getBlockMap(); | ||
var encounteredEntities = []; // Marks ranges that need cloning, because their entity has been encountered previously. | ||
var encounteredEntities = []; | ||
// Marks ranges that need cloning, because their entity has been encountered previously. | ||
var shouldCloneEntity = function shouldCloneEntity(firstChar) { | ||
@@ -340,18 +344,15 @@ var key = firstChar.getEntity(); | ||
return false; | ||
}; | ||
}; // We're going to update blocks that contain ranges pointing at the same entity as other ranges. | ||
// We're going to update blocks that contain ranges pointing at the same entity as other ranges. | ||
var blocks = blockMap.map(function (block) { | ||
var newChars = block.getCharacterList(); | ||
var altered = false; | ||
var altered = false; // Updates ranges for which the entity needs to be cloned. | ||
// Updates ranges for which the entity needs to be cloned. | ||
var updateRangeWithClone = function updateRangeWithClone(start, end) { | ||
var key = newChars.get(start).getEntity(); | ||
var entity = newContent.getEntity(key); | ||
newContent = newContent.createEntity(entity.getType(), entity.getMutability(), entity.getData()); | ||
var newKey = newContent.getLastCreatedEntityKey(); | ||
var newKey = newContent.getLastCreatedEntityKey(); // Update all of the chars in the range with the new entity. | ||
// Update all of the chars in the range with the new entity. | ||
newChars = newChars.map(function (char, i) { | ||
@@ -364,3 +365,2 @@ if (start <= i && i <= end) { | ||
}); | ||
altered = true; | ||
@@ -370,6 +370,4 @@ }; | ||
block.findEntityRanges(shouldCloneEntity, updateRangeWithClone); | ||
return altered ? block.set("characterList", newChars) : block; | ||
}); | ||
return newContent.merge({ | ||
@@ -379,3 +377,2 @@ blockMap: blockMap.merge(blocks) | ||
}; | ||
/*:: import type { BlockNode } from "draft-js/lib/BlockNode.js.flow" */ | ||
@@ -388,9 +385,13 @@ | ||
*/ | ||
var filterEntityRanges = function filterEntityRanges(filterFn /*: ( | ||
content: ContentState, | ||
entityKey: string, | ||
block: BlockNode, | ||
) => boolean*/, content /*: ContentState*/) { | ||
var filterEntityRanges = function filterEntityRanges(filterFn | ||
/*: ( | ||
content: ContentState, | ||
entityKey: string, | ||
block: BlockNode, | ||
) => boolean*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
/* | ||
@@ -404,5 +405,5 @@ * Removes entities from the character list if the entity isn't enabled. | ||
*/ | ||
var blocks = blockMap.map(function (block) { | ||
var altered = false; | ||
var chars = block.getCharacterList().map(function (char) { | ||
@@ -422,6 +423,4 @@ var entityKey = char.getEntity(); | ||
}); | ||
return altered ? block.set("characterList", chars) : block; | ||
}); | ||
return content.merge({ | ||
@@ -431,7 +430,11 @@ blockMap: blockMap.merge(blocks) | ||
}; | ||
/** | ||
* Keeps all entity types (images, links, documents, embeds) that are enabled. | ||
*/ | ||
var shouldKeepEntityType = function shouldKeepEntityType(whitelist /*: $ReadOnlyArray<{ type: string }>*/, type /*: string*/) { | ||
var shouldKeepEntityType = function shouldKeepEntityType(whitelist | ||
/*: $ReadOnlyArray<{ type: string }>*/ | ||
, type | ||
/*: string*/ | ||
) { | ||
return whitelist.some(function (e) { | ||
@@ -441,3 +444,2 @@ return e.type === type; | ||
}; | ||
/** | ||
@@ -447,21 +449,31 @@ * Removes invalid images – they should only be in atomic blocks. | ||
*/ | ||
var shouldRemoveImageEntity = function shouldRemoveImageEntity(entityType /*: string*/, blockType /*: string*/) { | ||
var shouldRemoveImageEntity = function shouldRemoveImageEntity(entityType | ||
/*: string*/ | ||
, blockType | ||
/*: string*/ | ||
) { | ||
return entityType === IMAGE && blockType !== ATOMIC; | ||
}; | ||
/** | ||
* Filters entities based on the data they contain. | ||
*/ | ||
var shouldKeepEntityByAttribute = function shouldKeepEntityByAttribute(entityTypes /*: $ReadOnlyArray<{ | ||
type: string, | ||
whitelist?: { | ||
[attribute: string]: string | boolean, | ||
}, | ||
}>*/, entityType /*: string*/, data /*: {}*/) { | ||
var shouldKeepEntityByAttribute = function shouldKeepEntityByAttribute(entityTypes | ||
/*: $ReadOnlyArray<{ | ||
type: string, | ||
whitelist?: { | ||
[attribute: string]: string | boolean, | ||
}, | ||
}>*/ | ||
, entityType | ||
/*: string*/ | ||
, data | ||
/*: {}*/ | ||
) { | ||
var config = entityTypes.find(function (t) { | ||
return t.type === entityType; | ||
}); | ||
// If no whitelist is defined, the filter keeps the entity. | ||
}); // If no whitelist is defined, the filter keeps the entity. | ||
var whitelist = config && config.whitelist ? config.whitelist : {}; | ||
var isValid = Object.keys(whitelist).every(function (attr) { | ||
@@ -472,3 +484,2 @@ var check = whitelist[attr]; | ||
var hasData = data.hasOwnProperty(attr); | ||
return check ? hasData : !hasData; | ||
@@ -479,6 +490,4 @@ } | ||
}); | ||
return isValid; | ||
}; | ||
/** | ||
@@ -489,12 +498,17 @@ * Filters data on an entity to only retain what is whitelisted. | ||
*/ | ||
var filterEntityData = function filterEntityData(entityTypes /*: $ReadOnlyArray<{ | ||
type: string, | ||
attributes?: $ReadOnlyArray<string>, | ||
}>*/, content /*: ContentState*/) { | ||
var filterEntityData = function filterEntityData(entityTypes | ||
/*: $ReadOnlyArray<{ | ||
type: string, | ||
attributes?: $ReadOnlyArray<string>, | ||
}>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var newContent = content; | ||
var entities = {}; | ||
newContent.getBlockMap().forEach(function (block) { | ||
block.findEntityRanges(function (char) { | ||
var entityKey = char.getEntity(); | ||
if (entityKey) { | ||
@@ -506,3 +520,2 @@ var entity = newContent.getEntity(entityKey); | ||
}); | ||
Object.keys(entities).forEach(function (key) { | ||
@@ -514,5 +527,4 @@ var entity = entities[key]; | ||
}); | ||
var whitelist = config ? config.attributes : null; | ||
var whitelist = config ? config.attributes : null; // If no whitelist is defined, keep all of the data. | ||
// If no whitelist is defined, keep all of the data. | ||
if (!whitelist) { | ||
@@ -530,6 +542,4 @@ return data; | ||
}, {}); | ||
newContent = newContent.replaceEntityData(key, newData); | ||
}); | ||
return newContent; | ||
@@ -539,23 +549,23 @@ }; | ||
// @flow | ||
/** | ||
* Replaces the given characters by their equivalent length of spaces, in all blocks. | ||
*/ | ||
var replaceTextBySpaces = function replaceTextBySpaces(characters /*: $ReadOnlyArray<string>*/, content /*: ContentState*/) { | ||
var replaceTextBySpaces = function replaceTextBySpaces(characters | ||
/*: $ReadOnlyArray<string>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap.map(function (block) { | ||
var text = block.getText(); | ||
// Only replaces the character(s) with as many spaces as their length, | ||
var text = block.getText(); // Only replaces the character(s) with as many spaces as their length, | ||
// so that style and entity ranges are left undisturbed. | ||
// If we want to completely remove the character, we also need to filter | ||
// the corresponding CharacterMetadata entities. | ||
var newText = characters.reduce(function (txt, char) { | ||
return txt.replace(new RegExp(char, "g"), " ".repeat(char.length)); | ||
}, text); | ||
return text !== newText ? block.set("text", newText) : block; | ||
}); | ||
return content.merge({ | ||
@@ -567,3 +577,2 @@ blockMap: blockMap.merge(blocks) | ||
// @flow | ||
/** | ||
@@ -574,9 +583,16 @@ * Applies the new content to the editor state, optionally moving the selection | ||
*/ | ||
var applyContentWithSelection = function applyContentWithSelection(editorState /*: EditorState*/, content /*: ContentState*/, nextContent /*: ContentState*/) { | ||
var applyContentWithSelection = function applyContentWithSelection(editorState | ||
/*: EditorState*/ | ||
, content | ||
/*: ContentState*/ | ||
, nextContent | ||
/*: ContentState*/ | ||
) { | ||
// If the content is the same before/after, return the state unaltered. | ||
if (nextContent === content) { | ||
return editorState; | ||
} | ||
} // If the block map is empty, insert a new unstyled block and put the selection on it. | ||
// If the block map is empty, insert a new unstyled block and put the selection on it. | ||
if (nextContent.getBlockMap().size === 0) { | ||
@@ -593,7 +609,7 @@ return draftJs.EditorState.moveFocusToEnd(draftJs.EditorState.set(editorState, { | ||
var anchorKey = selection.getAnchorKey(); | ||
var anchorBlock = nextContent.getBlockForKey(anchorKey); | ||
var anchorBlock = nextContent.getBlockForKey(anchorKey); // We only support moving collapsed selections, which is the only behavior of selections after paste. | ||
// And if the anchor block is valid, no need to move the selection. | ||
// We only support moving collapsed selections, which is the only behavior of selections after paste. | ||
// And if the anchor block is valid, no need to move the selection. | ||
var shouldKeepSelection = !selection.isCollapsed() || !!anchorBlock; | ||
if (shouldKeepSelection) { | ||
@@ -603,11 +619,9 @@ return nextState; | ||
var nextKeys = nextContent.getBlockMap().keySeq(); | ||
var nextKeys = nextContent.getBlockMap().keySeq(); // Find the first key whose successor is different in the old content (because a block was removed). | ||
// Starting from the end so the selection is preserved towards the last preserved block in the filtered region. | ||
// Find the first key whose successor is different in the old content (because a block was removed). | ||
// Starting from the end so the selection is preserved towards the last preserved block in the filtered region. | ||
var nextAnchorKey = nextKeys.reverse().find(function (k) { | ||
return content.getKeyAfter(k) !== nextContent.getKeyAfter(k); | ||
}); | ||
}); // If the selection was already misplaced before paste, we do not move it. | ||
// If the selection was already misplaced before paste, we do not move it. | ||
if (nextAnchorKey) { | ||
@@ -622,3 +636,2 @@ var nextSelectedBlock = nextContent.getBlockForKey(nextAnchorKey); | ||
}); | ||
return draftJs.EditorState.acceptSelection(nextState, nextSelection); | ||
@@ -632,2 +645,3 @@ } | ||
/*:: import type { EditorState as EditorStateType } from "draft-js"*/ | ||
/*:: type FilterOptions = { | ||
@@ -656,6 +670,16 @@ // Whitelist of allowed block types. unstyled and atomic are always included. | ||
whitespacedCharacters: Array<string>, | ||
// Optional: Rules used to automatically convert blocks from one type to another | ||
// based on the block’s text. Also supports setting the block depth. | ||
// Defaults to the filters’ built-in block prefix rules. | ||
blockTextRules?: $ReadOnlyArray<{ | ||
// A regex as a string, to match against block text, e.g. "^(◦|o |o\t)". | ||
test: string, | ||
// The type to convert the block to if the test regex matches. | ||
type: string, | ||
// The depth to set (e.g. for list items with different prefixes per depth). | ||
depth: number, | ||
}>, | ||
}*/ | ||
var PREFIX_RULES = [{ | ||
var BLOCK_PREFIX_RULES = [{ | ||
// https://regexper.com/#%5E(%C2%B7%20%7C%E2%80%A2%5Ct%7C%E2%80%A2%7C%F0%9F%93%B7%20%7C%5Ct%7C%20%5Ct) | ||
@@ -665,7 +689,13 @@ test: "^(· |•\t|•|📷 |\t| \t)", | ||
depth: 0 | ||
}, | ||
// https://regexper.com/#%5E(%E2%97%A6%7Co%20%7Co%5Ct) | ||
{ test: "^(◦|o |o\t)", type: "unordered-list-item", depth: 1 }, | ||
// https://regexper.com/#%5E(%C2%A7%20%7C%EF%82%A7%5Ct%7C%E2%97%BE) | ||
{ test: "^(§ |\t|◾)", type: "unordered-list-item", depth: 2 }, { | ||
}, // https://regexper.com/#%5E(%E2%97%A6%7Co%20%7Co%5Ct) | ||
{ | ||
test: "^(◦|o |o\t)", | ||
type: "unordered-list-item", | ||
depth: 1 | ||
}, // https://regexper.com/#%5E(%C2%A7%20%7C%EF%82%A7%5Ct%7C%E2%97%BE) | ||
{ | ||
test: "^(§ |\t|◾)", | ||
type: "unordered-list-item", | ||
depth: 2 | ||
}, { | ||
// https://regexper.com/#%5E1%7B0%2C1%7D%5Cd%5C.%5B%20%5Ct%5D | ||
@@ -690,3 +720,2 @@ test: "^1{0,1}\\d\\.[ \t]", | ||
}]; | ||
/** | ||
@@ -697,3 +726,8 @@ * Applies whitelist and blacklist operations to the editor content, | ||
*/ | ||
var filterEditorState = function filterEditorState(options /*: FilterOptions*/, editorState /*: EditorStateType*/) { | ||
var filterEditorState = function filterEditorState(options | ||
/*: FilterOptions*/ | ||
, editorState | ||
/*: EditorStateType*/ | ||
) { | ||
var blocks = options.blocks, | ||
@@ -703,3 +737,5 @@ styles = options.styles, | ||
maxNesting = options.maxNesting, | ||
whitespacedCharacters = options.whitespacedCharacters; | ||
whitespacedCharacters = options.whitespacedCharacters, | ||
_options$blockTextRul = options.blockTextRules, | ||
blockTextRules = _options$blockTextRul === void 0 ? BLOCK_PREFIX_RULES : _options$blockTextRul; | ||
@@ -711,51 +747,40 @@ var shouldKeepEntityRange = function shouldKeepEntityRange(content, entityKey, block) { | ||
var blockType = block.getType(); | ||
return shouldKeepEntityType(entities, entityType) && shouldKeepEntityByAttribute(entities, entityType, entityData) && !shouldRemoveImageEntity(entityType, blockType); | ||
}; | ||
}; // Order matters. Some filters may need the information filtered out by others. | ||
// Order matters. Some filters may need the information filtered out by others. | ||
var filters = [ | ||
// 1. clean up blocks. | ||
removeInvalidDepthBlocks, preserveBlockByText.bind(null, PREFIX_RULES), limitBlockDepth.bind(null, maxNesting), | ||
// 2. reset styles and blocks. | ||
filterInlineStyles.bind(null, styles), | ||
// Add block types that are always enabled in Draft.js. | ||
filterBlockTypes.bind(null, blocks.concat([UNSTYLED, ATOMIC])), | ||
// 4. Process atomic blocks before processing entities. | ||
preserveAtomicBlocks, resetAtomicBlocks, | ||
// 5. Remove entity ranges (and linked entities) | ||
filterEntityRanges.bind(null, shouldKeepEntityRange), | ||
// 6. Remove/filter entity-related matters. | ||
removeInvalidAtomicBlocks.bind(null, entities), filterEntityData.bind(null, entities), | ||
// 7. Clone entities for which it is necessary. | ||
cloneEntities, | ||
// 8. Finally, do text operations. | ||
var filters = [// 1. clean up blocks. | ||
removeInvalidDepthBlocks, preserveBlockByText.bind(null, blockTextRules), limitBlockDepth.bind(null, maxNesting), // 2. reset styles and blocks. | ||
filterInlineStyles.bind(null, styles), // Add block types that are always enabled in Draft.js. | ||
filterBlockTypes.bind(null, blocks.concat([UNSTYLED, ATOMIC])), // 4. Process atomic blocks before processing entities. | ||
preserveAtomicBlocks, resetAtomicBlocks, // 5. Remove entity ranges (and linked entities) | ||
filterEntityRanges.bind(null, shouldKeepEntityRange), // 6. Remove/filter entity-related matters. | ||
removeInvalidAtomicBlocks.bind(null, entities), filterEntityData.bind(null, entities), // 7. Clone entities for which it is necessary. | ||
cloneEntities, // 8. Finally, do text operations. | ||
replaceTextBySpaces.bind(null, whitespacedCharacters)]; | ||
var content = editorState.getCurrentContent(); | ||
var nextContent = filters.reduce(function (c, filter /*: (ContentState) => ContentState*/) { | ||
var nextContent = filters.reduce(function (c, filter | ||
/*: (ContentState) => ContentState*/ | ||
) { | ||
return filter(c); | ||
}, content); | ||
return applyContentWithSelection(editorState, content, nextContent); | ||
}; | ||
// @flow | ||
exports.applyContentWithSelection = applyContentWithSelection; | ||
exports.cloneEntities = cloneEntities; | ||
exports.filterBlockTypes = filterBlockTypes; | ||
exports.filterEditorState = filterEditorState; | ||
exports.filterEntityData = filterEntityData; | ||
exports.filterEntityRanges = filterEntityRanges; | ||
exports.filterInlineStyles = filterInlineStyles; | ||
exports.limitBlockDepth = limitBlockDepth; | ||
exports.preserveAtomicBlocks = preserveAtomicBlocks; | ||
exports.resetAtomicBlocks = resetAtomicBlocks; | ||
exports.preserveBlockByText = preserveBlockByText; | ||
exports.removeInvalidAtomicBlocks = removeInvalidAtomicBlocks; | ||
exports.removeInvalidDepthBlocks = removeInvalidDepthBlocks; | ||
exports.limitBlockDepth = limitBlockDepth; | ||
exports.preserveBlockByText = preserveBlockByText; | ||
exports.filterBlockTypes = filterBlockTypes; | ||
exports.filterInlineStyles = filterInlineStyles; | ||
exports.cloneEntities = cloneEntities; | ||
exports.filterEntityRanges = filterEntityRanges; | ||
exports.replaceTextBySpaces = replaceTextBySpaces; | ||
exports.resetAtomicBlocks = resetAtomicBlocks; | ||
exports.shouldKeepEntityByAttribute = shouldKeepEntityByAttribute; | ||
exports.shouldKeepEntityType = shouldKeepEntityType; | ||
exports.shouldRemoveImageEntity = shouldRemoveImageEntity; | ||
exports.shouldKeepEntityByAttribute = shouldKeepEntityByAttribute; | ||
exports.filterEntityData = filterEntityData; | ||
exports.replaceTextBySpaces = replaceTextBySpaces; | ||
exports.applyContentWithSelection = applyContentWithSelection; | ||
exports.filterEditorState = filterEditorState; |
// @flow | ||
import { CharacterMetadata, ContentState, EditorState } from 'draft-js'; | ||
import { CharacterMetadata, EditorState, ContentState } from 'draft-js'; | ||
@@ -9,7 +9,5 @@ // @flow | ||
var ORDERED_LIST_ITEM = "ordered-list-item"; | ||
var IMAGE = "IMAGE"; | ||
// @flow | ||
/** | ||
@@ -21,5 +19,7 @@ * Creates atomic blocks where they would be required for a block-level entity | ||
*/ | ||
var preserveAtomicBlocks = function preserveAtomicBlocks(content /*: ContentState*/) { | ||
var preserveAtomicBlocks = function preserveAtomicBlocks(content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var perservedBlocks = blockMap.filter(function (block) { | ||
@@ -29,3 +29,2 @@ var text = block.getText(); | ||
var shouldPreserve = entityKey && ["📷", " ", "📷 "].includes(text); | ||
return shouldPreserve; | ||
@@ -44,3 +43,2 @@ }).map(function (block) { | ||
}; | ||
/** | ||
@@ -50,6 +48,8 @@ * Resets atomic blocks to have a single-space char and no styles. | ||
*/ | ||
var resetAtomicBlocks = function resetAtomicBlocks(content /*: ContentState*/) { | ||
var resetAtomicBlocks = function resetAtomicBlocks(content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap; | ||
var normalisedBlocks = blocks.filter(function (block) { | ||
@@ -61,10 +61,7 @@ return block.getType() === ATOMIC && (block.getText() !== " " || block.getInlineStyleAt(0).size !== 0); | ||
var newChar = char; | ||
char.getStyle().forEach(function (type) { | ||
newChar = CharacterMetadata.removeStyle(newChar, type); | ||
}); | ||
return newChar; | ||
}); | ||
return block.merge({ | ||
@@ -84,7 +81,11 @@ text: " ", | ||
}; | ||
/** | ||
* Removes atomic blocks for which the entity isn't whitelisted. | ||
*/ | ||
var removeInvalidAtomicBlocks = function removeInvalidAtomicBlocks(whitelist /*: $ReadOnlyArray<{ type: string }>*/, content /*: ContentState*/) { | ||
var removeInvalidAtomicBlocks = function removeInvalidAtomicBlocks(whitelist | ||
/*: $ReadOnlyArray<{ type: string }>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
@@ -98,9 +99,8 @@ | ||
var entityKey = block.getEntityAt(0); | ||
var isValid = void 0; | ||
var isValid; | ||
if (entityKey) { | ||
var _type = content.getEntity(entityKey).getType(); | ||
var type = content.getEntity(entityKey).getType(); | ||
isValid = whitelist.some(function (t) { | ||
return t.type === _type; | ||
return t.type === type; | ||
}); | ||
@@ -126,3 +126,2 @@ } else { | ||
// @flow | ||
/** | ||
@@ -132,3 +131,6 @@ * Removes blocks that have a non-zero depth, and aren't list items. | ||
*/ | ||
var removeInvalidDepthBlocks = function removeInvalidDepthBlocks(content /*: ContentState*/) { | ||
var removeInvalidDepthBlocks = function removeInvalidDepthBlocks(content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
@@ -138,3 +140,2 @@ | ||
var isListBlock = [UNORDERED_LIST_ITEM, ORDERED_LIST_ITEM].includes(block.getType()); | ||
return isListBlock || block.getDepth() === 0; | ||
@@ -153,3 +154,2 @@ }; | ||
}; | ||
/** | ||
@@ -162,9 +162,13 @@ * Changes block type and depth based on the block's text. – some word processors | ||
*/ | ||
var preserveBlockByText = function preserveBlockByText(rules /*: $ReadOnlyArray<{ | ||
test: string, | ||
type: string, | ||
depth: number, | ||
}>*/, content /*: ContentState*/) { | ||
var preserveBlockByText = function preserveBlockByText(rules | ||
/*: $ReadOnlyArray<{ | ||
test: string, | ||
type: string, | ||
depth: number, | ||
}>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap.filter(function (block) { | ||
@@ -175,4 +179,3 @@ return block.getType() === "unstyled"; | ||
var newBlock = block; | ||
var match = void 0; | ||
var match; | ||
var matchingRule = rules.find(function (rule) { | ||
@@ -185,11 +188,9 @@ match = new RegExp(rule.test).exec(text); | ||
var _text = block.getText(); | ||
var entity = block.getEntityAt(0); | ||
// Special case – do not convert the block if there is an entity at the start, and the matching text is the full block’s text. | ||
var entity = block.getEntityAt(0); // Special case – do not convert the block if there is an entity at the start, and the matching text is the full block’s text. | ||
// This can happen in Word for equations, which are injected as images with text "📷 ". | ||
if (entity && match[0] === _text) { | ||
return newBlock; | ||
} | ||
// Unicode gotcha: | ||
} // Unicode gotcha: | ||
// At the moment, Draft.js stores one CharacterMetadata in the character list | ||
@@ -201,8 +202,10 @@ // for each "character" in an astral symbol. "📷" has a length of 2, is stored with two CharacterMetadata instances. | ||
// See https://mathiasbynens.be/notes/javascript-unicode. | ||
var sliceOffset = match[0].length; | ||
// Maintain persistence in the list while removing chars from the start. | ||
var sliceOffset = match[0].length; // Maintain persistence in the list while removing chars from the start. | ||
// https://github.com/facebook/draft-js/blob/788595984da7c1e00d1071ea82b063ff87140be4/src/model/transaction/removeRangeFromContentState.js#L333 | ||
var chars = block.getCharacterList(); | ||
var startOffset = 0; | ||
while (startOffset < sliceOffset) { | ||
@@ -223,3 +226,2 @@ chars = chars.shift(); | ||
}); | ||
return blocks.size === 0 ? content : content.merge({ | ||
@@ -229,9 +231,12 @@ blockMap: blockMap.merge(blocks) | ||
}; | ||
/** | ||
* Resets the depth of all the content to at most max. | ||
*/ | ||
var limitBlockDepth = function limitBlockDepth(max /*: number*/, content /*: ContentState*/) { | ||
var limitBlockDepth = function limitBlockDepth(max | ||
/*: number*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var changedBlocks = blockMap.filter(function (block) { | ||
@@ -242,3 +247,2 @@ return block.getDepth() > max; | ||
}); | ||
return changedBlocks.size === 0 ? content : content.merge({ | ||
@@ -248,3 +252,2 @@ blockMap: blockMap.merge(changedBlocks) | ||
}; | ||
/** | ||
@@ -254,5 +257,9 @@ * Converts all block types not present in the whitelist to unstyled. | ||
*/ | ||
var filterBlockTypes = function filterBlockTypes(whitelist /*: $ReadOnlyArray<string>*/, content /*: ContentState*/) { | ||
var filterBlockTypes = function filterBlockTypes(whitelist | ||
/*: $ReadOnlyArray<string>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var changedBlocks = blockMap.filter(function (block) { | ||
@@ -266,3 +273,2 @@ return !whitelist.includes(block.getType()); | ||
}); | ||
return changedBlocks.size === 0 ? content : content.merge({ | ||
@@ -274,15 +280,16 @@ blockMap: blockMap.merge(changedBlocks) | ||
// @flow | ||
/** | ||
* Removes all styles not present in the whitelist. | ||
*/ | ||
var filterInlineStyles = function filterInlineStyles(whitelist /*: $ReadOnlyArray<string>*/, content /*: ContentState*/) { | ||
var filterInlineStyles = function filterInlineStyles(whitelist | ||
/*: $ReadOnlyArray<string>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap.map(function (block) { | ||
var altered = false; | ||
var chars = block.getCharacterList().map(function (char) { | ||
var newChar = char; | ||
char.getStyle().filter(function (type) { | ||
@@ -294,9 +301,6 @@ return !whitelist.includes(type); | ||
}); | ||
return newChar; | ||
}); | ||
return altered ? block.set("characterList", chars) : block; | ||
}); | ||
return content.merge({ | ||
@@ -308,3 +312,2 @@ blockMap: blockMap.merge(blocks) | ||
// @flow | ||
/** | ||
@@ -315,9 +318,10 @@ * Clones entities in the entityMap, so each range points to its own entity instance. | ||
*/ | ||
var cloneEntities = function cloneEntities(content /*: ContentState*/) { | ||
var cloneEntities = function cloneEntities(content | ||
/*: ContentState*/ | ||
) { | ||
var newContent = content; | ||
var blockMap = newContent.getBlockMap(); | ||
var encounteredEntities = []; // Marks ranges that need cloning, because their entity has been encountered previously. | ||
var encounteredEntities = []; | ||
// Marks ranges that need cloning, because their entity has been encountered previously. | ||
var shouldCloneEntity = function shouldCloneEntity(firstChar) { | ||
@@ -335,18 +339,15 @@ var key = firstChar.getEntity(); | ||
return false; | ||
}; | ||
}; // We're going to update blocks that contain ranges pointing at the same entity as other ranges. | ||
// We're going to update blocks that contain ranges pointing at the same entity as other ranges. | ||
var blocks = blockMap.map(function (block) { | ||
var newChars = block.getCharacterList(); | ||
var altered = false; | ||
var altered = false; // Updates ranges for which the entity needs to be cloned. | ||
// Updates ranges for which the entity needs to be cloned. | ||
var updateRangeWithClone = function updateRangeWithClone(start, end) { | ||
var key = newChars.get(start).getEntity(); | ||
var entity = newContent.getEntity(key); | ||
newContent = newContent.createEntity(entity.getType(), entity.getMutability(), entity.getData()); | ||
var newKey = newContent.getLastCreatedEntityKey(); | ||
var newKey = newContent.getLastCreatedEntityKey(); // Update all of the chars in the range with the new entity. | ||
// Update all of the chars in the range with the new entity. | ||
newChars = newChars.map(function (char, i) { | ||
@@ -359,3 +360,2 @@ if (start <= i && i <= end) { | ||
}); | ||
altered = true; | ||
@@ -365,6 +365,4 @@ }; | ||
block.findEntityRanges(shouldCloneEntity, updateRangeWithClone); | ||
return altered ? block.set("characterList", newChars) : block; | ||
}); | ||
return newContent.merge({ | ||
@@ -374,3 +372,2 @@ blockMap: blockMap.merge(blocks) | ||
}; | ||
/*:: import type { BlockNode } from "draft-js/lib/BlockNode.js.flow" */ | ||
@@ -383,9 +380,13 @@ | ||
*/ | ||
var filterEntityRanges = function filterEntityRanges(filterFn /*: ( | ||
content: ContentState, | ||
entityKey: string, | ||
block: BlockNode, | ||
) => boolean*/, content /*: ContentState*/) { | ||
var filterEntityRanges = function filterEntityRanges(filterFn | ||
/*: ( | ||
content: ContentState, | ||
entityKey: string, | ||
block: BlockNode, | ||
) => boolean*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
/* | ||
@@ -399,5 +400,5 @@ * Removes entities from the character list if the entity isn't enabled. | ||
*/ | ||
var blocks = blockMap.map(function (block) { | ||
var altered = false; | ||
var chars = block.getCharacterList().map(function (char) { | ||
@@ -417,6 +418,4 @@ var entityKey = char.getEntity(); | ||
}); | ||
return altered ? block.set("characterList", chars) : block; | ||
}); | ||
return content.merge({ | ||
@@ -426,7 +425,11 @@ blockMap: blockMap.merge(blocks) | ||
}; | ||
/** | ||
* Keeps all entity types (images, links, documents, embeds) that are enabled. | ||
*/ | ||
var shouldKeepEntityType = function shouldKeepEntityType(whitelist /*: $ReadOnlyArray<{ type: string }>*/, type /*: string*/) { | ||
var shouldKeepEntityType = function shouldKeepEntityType(whitelist | ||
/*: $ReadOnlyArray<{ type: string }>*/ | ||
, type | ||
/*: string*/ | ||
) { | ||
return whitelist.some(function (e) { | ||
@@ -436,3 +439,2 @@ return e.type === type; | ||
}; | ||
/** | ||
@@ -442,21 +444,31 @@ * Removes invalid images – they should only be in atomic blocks. | ||
*/ | ||
var shouldRemoveImageEntity = function shouldRemoveImageEntity(entityType /*: string*/, blockType /*: string*/) { | ||
var shouldRemoveImageEntity = function shouldRemoveImageEntity(entityType | ||
/*: string*/ | ||
, blockType | ||
/*: string*/ | ||
) { | ||
return entityType === IMAGE && blockType !== ATOMIC; | ||
}; | ||
/** | ||
* Filters entities based on the data they contain. | ||
*/ | ||
var shouldKeepEntityByAttribute = function shouldKeepEntityByAttribute(entityTypes /*: $ReadOnlyArray<{ | ||
type: string, | ||
whitelist?: { | ||
[attribute: string]: string | boolean, | ||
}, | ||
}>*/, entityType /*: string*/, data /*: {}*/) { | ||
var shouldKeepEntityByAttribute = function shouldKeepEntityByAttribute(entityTypes | ||
/*: $ReadOnlyArray<{ | ||
type: string, | ||
whitelist?: { | ||
[attribute: string]: string | boolean, | ||
}, | ||
}>*/ | ||
, entityType | ||
/*: string*/ | ||
, data | ||
/*: {}*/ | ||
) { | ||
var config = entityTypes.find(function (t) { | ||
return t.type === entityType; | ||
}); | ||
// If no whitelist is defined, the filter keeps the entity. | ||
}); // If no whitelist is defined, the filter keeps the entity. | ||
var whitelist = config && config.whitelist ? config.whitelist : {}; | ||
var isValid = Object.keys(whitelist).every(function (attr) { | ||
@@ -467,3 +479,2 @@ var check = whitelist[attr]; | ||
var hasData = data.hasOwnProperty(attr); | ||
return check ? hasData : !hasData; | ||
@@ -474,6 +485,4 @@ } | ||
}); | ||
return isValid; | ||
}; | ||
/** | ||
@@ -484,12 +493,17 @@ * Filters data on an entity to only retain what is whitelisted. | ||
*/ | ||
var filterEntityData = function filterEntityData(entityTypes /*: $ReadOnlyArray<{ | ||
type: string, | ||
attributes?: $ReadOnlyArray<string>, | ||
}>*/, content /*: ContentState*/) { | ||
var filterEntityData = function filterEntityData(entityTypes | ||
/*: $ReadOnlyArray<{ | ||
type: string, | ||
attributes?: $ReadOnlyArray<string>, | ||
}>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var newContent = content; | ||
var entities = {}; | ||
newContent.getBlockMap().forEach(function (block) { | ||
block.findEntityRanges(function (char) { | ||
var entityKey = char.getEntity(); | ||
if (entityKey) { | ||
@@ -501,3 +515,2 @@ var entity = newContent.getEntity(entityKey); | ||
}); | ||
Object.keys(entities).forEach(function (key) { | ||
@@ -509,5 +522,4 @@ var entity = entities[key]; | ||
}); | ||
var whitelist = config ? config.attributes : null; | ||
var whitelist = config ? config.attributes : null; // If no whitelist is defined, keep all of the data. | ||
// If no whitelist is defined, keep all of the data. | ||
if (!whitelist) { | ||
@@ -525,6 +537,4 @@ return data; | ||
}, {}); | ||
newContent = newContent.replaceEntityData(key, newData); | ||
}); | ||
return newContent; | ||
@@ -534,23 +544,23 @@ }; | ||
// @flow | ||
/** | ||
* Replaces the given characters by their equivalent length of spaces, in all blocks. | ||
*/ | ||
var replaceTextBySpaces = function replaceTextBySpaces(characters /*: $ReadOnlyArray<string>*/, content /*: ContentState*/) { | ||
var replaceTextBySpaces = function replaceTextBySpaces(characters | ||
/*: $ReadOnlyArray<string>*/ | ||
, content | ||
/*: ContentState*/ | ||
) { | ||
var blockMap = content.getBlockMap(); | ||
var blocks = blockMap.map(function (block) { | ||
var text = block.getText(); | ||
// Only replaces the character(s) with as many spaces as their length, | ||
var text = block.getText(); // Only replaces the character(s) with as many spaces as their length, | ||
// so that style and entity ranges are left undisturbed. | ||
// If we want to completely remove the character, we also need to filter | ||
// the corresponding CharacterMetadata entities. | ||
var newText = characters.reduce(function (txt, char) { | ||
return txt.replace(new RegExp(char, "g"), " ".repeat(char.length)); | ||
}, text); | ||
return text !== newText ? block.set("text", newText) : block; | ||
}); | ||
return content.merge({ | ||
@@ -562,3 +572,2 @@ blockMap: blockMap.merge(blocks) | ||
// @flow | ||
/** | ||
@@ -569,9 +578,16 @@ * Applies the new content to the editor state, optionally moving the selection | ||
*/ | ||
var applyContentWithSelection = function applyContentWithSelection(editorState /*: EditorState*/, content /*: ContentState*/, nextContent /*: ContentState*/) { | ||
var applyContentWithSelection = function applyContentWithSelection(editorState | ||
/*: EditorState*/ | ||
, content | ||
/*: ContentState*/ | ||
, nextContent | ||
/*: ContentState*/ | ||
) { | ||
// If the content is the same before/after, return the state unaltered. | ||
if (nextContent === content) { | ||
return editorState; | ||
} | ||
} // If the block map is empty, insert a new unstyled block and put the selection on it. | ||
// If the block map is empty, insert a new unstyled block and put the selection on it. | ||
if (nextContent.getBlockMap().size === 0) { | ||
@@ -588,7 +604,7 @@ return EditorState.moveFocusToEnd(EditorState.set(editorState, { | ||
var anchorKey = selection.getAnchorKey(); | ||
var anchorBlock = nextContent.getBlockForKey(anchorKey); | ||
var anchorBlock = nextContent.getBlockForKey(anchorKey); // We only support moving collapsed selections, which is the only behavior of selections after paste. | ||
// And if the anchor block is valid, no need to move the selection. | ||
// We only support moving collapsed selections, which is the only behavior of selections after paste. | ||
// And if the anchor block is valid, no need to move the selection. | ||
var shouldKeepSelection = !selection.isCollapsed() || !!anchorBlock; | ||
if (shouldKeepSelection) { | ||
@@ -598,11 +614,9 @@ return nextState; | ||
var nextKeys = nextContent.getBlockMap().keySeq(); | ||
var nextKeys = nextContent.getBlockMap().keySeq(); // Find the first key whose successor is different in the old content (because a block was removed). | ||
// Starting from the end so the selection is preserved towards the last preserved block in the filtered region. | ||
// Find the first key whose successor is different in the old content (because a block was removed). | ||
// Starting from the end so the selection is preserved towards the last preserved block in the filtered region. | ||
var nextAnchorKey = nextKeys.reverse().find(function (k) { | ||
return content.getKeyAfter(k) !== nextContent.getKeyAfter(k); | ||
}); | ||
}); // If the selection was already misplaced before paste, we do not move it. | ||
// If the selection was already misplaced before paste, we do not move it. | ||
if (nextAnchorKey) { | ||
@@ -617,3 +631,2 @@ var nextSelectedBlock = nextContent.getBlockForKey(nextAnchorKey); | ||
}); | ||
return EditorState.acceptSelection(nextState, nextSelection); | ||
@@ -627,2 +640,3 @@ } | ||
/*:: import type { EditorState as EditorStateType } from "draft-js"*/ | ||
/*:: type FilterOptions = { | ||
@@ -651,6 +665,16 @@ // Whitelist of allowed block types. unstyled and atomic are always included. | ||
whitespacedCharacters: Array<string>, | ||
// Optional: Rules used to automatically convert blocks from one type to another | ||
// based on the block’s text. Also supports setting the block depth. | ||
// Defaults to the filters’ built-in block prefix rules. | ||
blockTextRules?: $ReadOnlyArray<{ | ||
// A regex as a string, to match against block text, e.g. "^(◦|o |o\t)". | ||
test: string, | ||
// The type to convert the block to if the test regex matches. | ||
type: string, | ||
// The depth to set (e.g. for list items with different prefixes per depth). | ||
depth: number, | ||
}>, | ||
}*/ | ||
var PREFIX_RULES = [{ | ||
var BLOCK_PREFIX_RULES = [{ | ||
// https://regexper.com/#%5E(%C2%B7%20%7C%E2%80%A2%5Ct%7C%E2%80%A2%7C%F0%9F%93%B7%20%7C%5Ct%7C%20%5Ct) | ||
@@ -660,7 +684,13 @@ test: "^(· |•\t|•|📷 |\t| \t)", | ||
depth: 0 | ||
}, | ||
// https://regexper.com/#%5E(%E2%97%A6%7Co%20%7Co%5Ct) | ||
{ test: "^(◦|o |o\t)", type: "unordered-list-item", depth: 1 }, | ||
// https://regexper.com/#%5E(%C2%A7%20%7C%EF%82%A7%5Ct%7C%E2%97%BE) | ||
{ test: "^(§ |\t|◾)", type: "unordered-list-item", depth: 2 }, { | ||
}, // https://regexper.com/#%5E(%E2%97%A6%7Co%20%7Co%5Ct) | ||
{ | ||
test: "^(◦|o |o\t)", | ||
type: "unordered-list-item", | ||
depth: 1 | ||
}, // https://regexper.com/#%5E(%C2%A7%20%7C%EF%82%A7%5Ct%7C%E2%97%BE) | ||
{ | ||
test: "^(§ |\t|◾)", | ||
type: "unordered-list-item", | ||
depth: 2 | ||
}, { | ||
// https://regexper.com/#%5E1%7B0%2C1%7D%5Cd%5C.%5B%20%5Ct%5D | ||
@@ -685,3 +715,2 @@ test: "^1{0,1}\\d\\.[ \t]", | ||
}]; | ||
/** | ||
@@ -692,3 +721,8 @@ * Applies whitelist and blacklist operations to the editor content, | ||
*/ | ||
var filterEditorState = function filterEditorState(options /*: FilterOptions*/, editorState /*: EditorStateType*/) { | ||
var filterEditorState = function filterEditorState(options | ||
/*: FilterOptions*/ | ||
, editorState | ||
/*: EditorStateType*/ | ||
) { | ||
var blocks = options.blocks, | ||
@@ -698,3 +732,5 @@ styles = options.styles, | ||
maxNesting = options.maxNesting, | ||
whitespacedCharacters = options.whitespacedCharacters; | ||
whitespacedCharacters = options.whitespacedCharacters, | ||
_options$blockTextRul = options.blockTextRules, | ||
blockTextRules = _options$blockTextRul === void 0 ? BLOCK_PREFIX_RULES : _options$blockTextRul; | ||
@@ -706,35 +742,24 @@ var shouldKeepEntityRange = function shouldKeepEntityRange(content, entityKey, block) { | ||
var blockType = block.getType(); | ||
return shouldKeepEntityType(entities, entityType) && shouldKeepEntityByAttribute(entities, entityType, entityData) && !shouldRemoveImageEntity(entityType, blockType); | ||
}; | ||
}; // Order matters. Some filters may need the information filtered out by others. | ||
// Order matters. Some filters may need the information filtered out by others. | ||
var filters = [ | ||
// 1. clean up blocks. | ||
removeInvalidDepthBlocks, preserveBlockByText.bind(null, PREFIX_RULES), limitBlockDepth.bind(null, maxNesting), | ||
// 2. reset styles and blocks. | ||
filterInlineStyles.bind(null, styles), | ||
// Add block types that are always enabled in Draft.js. | ||
filterBlockTypes.bind(null, blocks.concat([UNSTYLED, ATOMIC])), | ||
// 4. Process atomic blocks before processing entities. | ||
preserveAtomicBlocks, resetAtomicBlocks, | ||
// 5. Remove entity ranges (and linked entities) | ||
filterEntityRanges.bind(null, shouldKeepEntityRange), | ||
// 6. Remove/filter entity-related matters. | ||
removeInvalidAtomicBlocks.bind(null, entities), filterEntityData.bind(null, entities), | ||
// 7. Clone entities for which it is necessary. | ||
cloneEntities, | ||
// 8. Finally, do text operations. | ||
var filters = [// 1. clean up blocks. | ||
removeInvalidDepthBlocks, preserveBlockByText.bind(null, blockTextRules), limitBlockDepth.bind(null, maxNesting), // 2. reset styles and blocks. | ||
filterInlineStyles.bind(null, styles), // Add block types that are always enabled in Draft.js. | ||
filterBlockTypes.bind(null, blocks.concat([UNSTYLED, ATOMIC])), // 4. Process atomic blocks before processing entities. | ||
preserveAtomicBlocks, resetAtomicBlocks, // 5. Remove entity ranges (and linked entities) | ||
filterEntityRanges.bind(null, shouldKeepEntityRange), // 6. Remove/filter entity-related matters. | ||
removeInvalidAtomicBlocks.bind(null, entities), filterEntityData.bind(null, entities), // 7. Clone entities for which it is necessary. | ||
cloneEntities, // 8. Finally, do text operations. | ||
replaceTextBySpaces.bind(null, whitespacedCharacters)]; | ||
var content = editorState.getCurrentContent(); | ||
var nextContent = filters.reduce(function (c, filter /*: (ContentState) => ContentState*/) { | ||
var nextContent = filters.reduce(function (c, filter | ||
/*: (ContentState) => ContentState*/ | ||
) { | ||
return filter(c); | ||
}, content); | ||
return applyContentWithSelection(editorState, content, nextContent); | ||
}; | ||
// @flow | ||
export { preserveAtomicBlocks, resetAtomicBlocks, removeInvalidAtomicBlocks, removeInvalidDepthBlocks, limitBlockDepth, preserveBlockByText, filterBlockTypes, filterInlineStyles, cloneEntities, filterEntityRanges, shouldKeepEntityType, shouldRemoveImageEntity, shouldKeepEntityByAttribute, filterEntityData, replaceTextBySpaces, applyContentWithSelection, filterEditorState }; | ||
export { applyContentWithSelection, cloneEntities, filterBlockTypes, filterEditorState, filterEntityData, filterEntityRanges, filterInlineStyles, limitBlockDepth, preserveAtomicBlocks, preserveBlockByText, removeInvalidAtomicBlocks, removeInvalidDepthBlocks, replaceTextBySpaces, resetAtomicBlocks, shouldKeepEntityByAttribute, shouldKeepEntityType, shouldRemoveImageEntity }; |
{ | ||
"name": "draftjs-filters", | ||
"version": "2.2.4", | ||
"version": "2.3.0", | ||
"description": "Filter Draft.js content to preserve only the formatting you allow", | ||
@@ -30,36 +30,31 @@ "author": "Thibaud Colas", | ||
"browserslist": "> 1%, IE 11", | ||
"babel": { | ||
"presets": [ | ||
"env" | ||
] | ||
}, | ||
"devDependencies": { | ||
"@babel/plugin-transform-flow-comments": "7.7.4", | ||
"@commitlint/cli": "8.2.0", | ||
"@commitlint/config-conventional": "8.2.0", | ||
"@commitlint/travis-cli": "8.2.0", | ||
"@semantic-release/changelog": "3.0.4", | ||
"@semantic-release/exec": "3.3.7", | ||
"@semantic-release/git": "7.0.16", | ||
"babel-plugin-transform-flow-comments": "6.22.0", | ||
"core-js": "^2.5.7", | ||
"coveralls": "3.0.6", | ||
"danger": "9.2.1", | ||
"documentation": "8.1.2", | ||
"@semantic-release/changelog": "3.0.6", | ||
"@semantic-release/exec": "3.3.8", | ||
"@semantic-release/git": "7.0.18", | ||
"coveralls": "3.0.9", | ||
"danger": "9.2.9", | ||
"documentation": "12.1.4", | ||
"draft-js": "0.10.5", | ||
"enzyme": "3.10.0", | ||
"enzyme-adapter-react-16": "1.14.0", | ||
"enzyme-to-json": "3.4.2", | ||
"flow-bin": "0.91.0", | ||
"draft-js-11": "npm:draft-js@0.11.3", | ||
"enzyme": "3.11.0", | ||
"enzyme-adapter-react-16": "1.15.2", | ||
"enzyme-to-json": "3.4.3", | ||
"flow-bin": "0.114.0", | ||
"immutable": "~3.7.6", | ||
"normalize.css": "^7.0.0", | ||
"prettier": "1.18.2", | ||
"react": "16.10.2", | ||
"react-dom": "16.10.2", | ||
"react-scripts": "1.1.5", | ||
"react-test-renderer": "16.10.2", | ||
"rollup": "0.66.2", | ||
"rollup-plugin-babel": "3.0.7", | ||
"semantic-release": "15.13.24", | ||
"snapshot-diff": "0.4.0", | ||
"source-map-explorer": "2.1.0" | ||
"normalize.css": "7.0.0", | ||
"prettier": "1.19.1", | ||
"react": "16.12.0", | ||
"react-dom": "16.12.0", | ||
"react-scripts": "3.3.0", | ||
"react-test-renderer": "16.12.0", | ||
"rollup": "1.27.14", | ||
"rollup-plugin-babel": "4.3.3", | ||
"semantic-release": "15.14.0", | ||
"snapshot-diff": "0.6.1", | ||
"source-map-explorer": "2.1.2" | ||
}, | ||
@@ -72,3 +67,3 @@ "peerDependencies": { | ||
"build": "CI=true react-scripts build && source-map-explorer --html build/static/js/main.* > build/source-map-explorer.html && rollup -c", | ||
"test": "react-scripts test --env=jsdom --coverage", | ||
"test": "CI=true react-scripts test --env=jsdom --coverage", | ||
"test:watch": "react-scripts test --env=jsdom", | ||
@@ -75,0 +70,0 @@ "report:coverage": "open coverage/lcov-report/index.html", |
@@ -63,3 +63,3 @@ # [Draft.js filters](https://thibaudcolas.github.io/draftjs-filters/) [<img src="https://raw.githubusercontent.com/thibaudcolas/draftail.org/master/.github/draftail-logo.svg?sanitize=true" width="90" height="90" align="right">](https://www.draftail.org/) | ||
Here are the available options: | ||
Here are all the available options: | ||
@@ -89,2 +89,13 @@ ```jsx | ||
whitespacedCharacters: Array<string>, | ||
// Optional: Rules used to automatically convert blocks from one type to another | ||
// based on the block’s text. Also supports setting the block depth. | ||
// Defaults to the filters’ built-in block prefix rules. | ||
blockTextRules?: $ReadOnlyArray<{ | ||
// A regex as a string, to match against block text, e.g. "^(◦|o |o\t)". | ||
test: string, | ||
// The type to convert the block to if the test regex matches. | ||
type: string, | ||
// The depth to set (e.g. for list items with different prefixes per depth). | ||
depth: number, | ||
}>, | ||
``` | ||
@@ -91,0 +102,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1266
337
76826