Comparing version 0.2.2 to 0.3.0
@@ -7,2 +7,15 @@ # Changelog | ||
## 0.3.0 (March 22, 2016) | ||
### Fixed | ||
* Properly extract custom inline styles for `convertToRaw` | ||
* Fix internal paste behavior to better handle copied custom blocks | ||
### Added | ||
* Export `getVisibleSelectionRect` | ||
* Export `convertFromHTML` | ||
* Export `DraftEditorBlock` | ||
## 0.2.2 (March 9, 2016) | ||
@@ -9,0 +22,0 @@ |
@@ -20,2 +20,3 @@ /** | ||
var DraftEditor = require('./DraftEditor.react'); | ||
var DraftEditorBlock = require('./DraftEditorBlock.react'); | ||
var DraftModifier = require('./DraftModifier'); | ||
@@ -30,8 +31,11 @@ var DraftEntity = require('./DraftEntity'); | ||
var convertFromDraftStateToRaw = require('./convertFromDraftStateToRaw'); | ||
var convertFromHTMLToContentBlocks = require('./convertFromHTMLToContentBlocks'); | ||
var convertFromRawToDraftState = require('./convertFromRawToDraftState'); | ||
var generateRandomKey = require('./generateRandomKey'); | ||
var getDefaultKeyBinding = require('./getDefaultKeyBinding'); | ||
var getVisibleSelectionRect = require('./getVisibleSelectionRect'); | ||
var DraftPublic = { | ||
Editor: DraftEditor, | ||
EditorBlock: DraftEditorBlock, | ||
EditorState: EditorState, | ||
@@ -53,8 +57,10 @@ | ||
convertFromHTML: convertFromHTMLToContentBlocks, | ||
convertFromRaw: convertFromRawToDraftState, | ||
convertToRaw: convertFromDraftStateToRaw, | ||
genKey: generateRandomKey, | ||
getDefaultKeyBinding: getDefaultKeyBinding | ||
getDefaultKeyBinding: getDefaultKeyBinding, | ||
getVisibleSelectionRect: getVisibleSelectionRect | ||
}; | ||
module.exports = DraftPublic; |
@@ -41,2 +41,3 @@ /** | ||
var emptyFunction = require('fbjs/lib/emptyFunction'); | ||
var generateRandomKey = require('./generateRandomKey'); | ||
var getDefaultKeyBinding = require('./getDefaultKeyBinding'); | ||
@@ -85,2 +86,4 @@ var nullthrows = require('fbjs/lib/nullthrows'); | ||
function DraftEditor(props) { | ||
var _this = this; | ||
_classCallCheck(this, DraftEditor); | ||
@@ -95,2 +98,3 @@ | ||
this._dragCount = 0; | ||
this._editorKey = generateRandomKey(); | ||
@@ -128,2 +132,5 @@ this._onBeforeInput = this._buildHandler('onBeforeInput'); | ||
this.getClipboard = this._getClipboard.bind(this); | ||
this.getEditorKey = function () { | ||
return _this._editorKey; | ||
}; | ||
this.update = this._update.bind(this); | ||
@@ -146,8 +153,8 @@ this.onDragEnter = this._onDragEnter.bind(this); | ||
value: function _buildHandler(eventName) { | ||
var _this = this; | ||
var _this2 = this; | ||
return function (e) { | ||
if (!_this.props.readOnly) { | ||
var method = _this._handler && _this._handler[eventName]; | ||
method && method.call(_this, e); | ||
if (!_this2.props.readOnly) { | ||
var method = _this2._handler && _this2._handler[eventName]; | ||
method && method.call(_this2, e); | ||
} | ||
@@ -187,3 +194,4 @@ }; | ||
outline: 'none', | ||
whiteSpace: 'pre-wrap' | ||
whiteSpace: 'pre-wrap', | ||
wordWrap: 'break-word' | ||
}; | ||
@@ -245,2 +253,3 @@ | ||
customStyleMap: _extends({}, DefaultDraftInlineStyle, this.props.customStyleMap), | ||
editorKey: this._editorKey, | ||
editorState: this.props.editorState | ||
@@ -363,6 +372,6 @@ }) | ||
value: function _restoreEditorDOM(scrollPosition) { | ||
var _this2 = this; | ||
var _this3 = this; | ||
this.setState({ containerKey: this.state.containerKey + 1 }, function () { | ||
_this2._focus(scrollPosition); | ||
_this3._focus(scrollPosition); | ||
}); | ||
@@ -369,0 +378,0 @@ } |
@@ -174,2 +174,3 @@ /** | ||
'data-block': true, | ||
'data-editor': this.props.editorKey, | ||
'data-offset-key': offsetKey, | ||
@@ -176,0 +177,0 @@ key: key |
@@ -18,377 +18,14 @@ /** | ||
var ContentBlock = require('./ContentBlock'); | ||
var DraftEntity = require('./DraftEntity'); | ||
var Immutable = require('immutable'); | ||
var URI = require('fbjs/lib/URI'); | ||
var convertFromHTMLtoContentBlocks = require('./convertFromHTMLToContentBlocks'); | ||
var generateRandomKey = require('./generateRandomKey'); | ||
var getSafeBodyFromHTML = require('./getSafeBodyFromHTML'); | ||
var invariant = require('fbjs/lib/invariant'); | ||
var nullthrows = require('fbjs/lib/nullthrows'); | ||
var sanitizeDraftText = require('./sanitizeDraftText'); | ||
var List = Immutable.List; | ||
var OrderedSet = Immutable.OrderedSet; | ||
var Repeat = Immutable.Repeat; | ||
var NBSP = ' '; | ||
var SPACE = ' '; | ||
// Corresponds to max indent in campfire editor | ||
var MAX_DEPTH = 4; | ||
// used for replacing characters in HTML | ||
var REGEX_CR = new RegExp('\r', 'g'); | ||
var REGEX_LF = new RegExp('\n', 'g'); | ||
var REGEX_NBSP = new RegExp(NBSP, 'g'); | ||
// Block tag flow is different because LIs do not have | ||
// a deterministic style ;_; | ||
var blockTags = ['p', 'h1', 'h2', 'h3', 'li', 'blockquote', 'pre']; | ||
var inlineTags = { | ||
b: 'BOLD', | ||
code: 'CODE', | ||
del: 'STRIKETHROUGH', | ||
em: 'ITALIC', | ||
i: 'ITALIC', | ||
s: 'STRIKETHROUGH', | ||
strike: 'STRIKETHROUGH', | ||
strong: 'BOLD', | ||
u: 'UNDERLINE' | ||
}; | ||
var lastBlock; | ||
function getEmptyChunk() { | ||
return { | ||
text: '', | ||
inlines: [], | ||
entities: [], | ||
blocks: [] | ||
}; | ||
} | ||
function getWhitespaceChunk(inEntity) { | ||
var entities = new Array(1); | ||
if (inEntity) { | ||
entities[0] = inEntity; | ||
} | ||
return { | ||
text: SPACE, | ||
inlines: [OrderedSet()], | ||
entities: entities, | ||
blocks: [] | ||
}; | ||
} | ||
function getSoftNewlineChunk() { | ||
return { | ||
text: '\n', | ||
inlines: [OrderedSet()], | ||
entities: new Array(1), | ||
blocks: [] | ||
}; | ||
} | ||
function getBlockDividerChunk(block, depth) { | ||
return { | ||
text: '\r', | ||
inlines: [OrderedSet()], | ||
entities: new Array(1), | ||
blocks: [{ | ||
type: block, | ||
depth: Math.max(0, Math.min(MAX_DEPTH, depth)) | ||
}] | ||
}; | ||
} | ||
function getBlockTypeForTag(tag, lastList) { | ||
switch (tag) { | ||
case 'h1': | ||
return 'header-one'; | ||
case 'h2': | ||
return 'header-two'; | ||
case 'h3': | ||
return 'header-three'; | ||
case 'h4': | ||
return 'header-four'; | ||
case 'h5': | ||
return 'header-five'; | ||
case 'h6': | ||
return 'header-six'; | ||
case 'li': | ||
if (lastList === 'ol') { | ||
return 'ordered-list-item'; | ||
} | ||
return 'unordered-list-item'; | ||
case 'blockquote': | ||
return 'blockquote'; | ||
case 'pre': | ||
return 'code-block'; | ||
default: | ||
return 'unstyled'; | ||
} | ||
} | ||
function processInlineTag(tag, node, currentStyle) { | ||
var styleToCheck = inlineTags[tag]; | ||
if (styleToCheck) { | ||
currentStyle = currentStyle.add(styleToCheck).toOrderedSet(); | ||
} else if (node instanceof HTMLElement) { | ||
(function () { | ||
var htmlElement = node; | ||
currentStyle = currentStyle.withMutations(function (style) { | ||
if (htmlElement.style.fontWeight === 'bold') { | ||
style.add('BOLD'); | ||
} | ||
if (htmlElement.style.fontStyle === 'italic') { | ||
style.add('ITALIC'); | ||
} | ||
if (htmlElement.style.textDecoration === 'underline') { | ||
style.add('UNDERLINE'); | ||
} | ||
if (htmlElement.style.textDecoration === 'line-through') { | ||
style.add('STRIKETHROUGH'); | ||
} | ||
}).toOrderedSet(); | ||
})(); | ||
} | ||
return currentStyle; | ||
} | ||
function joinChunks(A, B) { | ||
// Sometimes two blocks will touch in the DOM and we need to strip the | ||
// extra delimiter to preserve niceness. | ||
var lastInB = B.text.slice(0, 1); | ||
if (A.text.slice(-1) === '\r' && lastInB === '\r') { | ||
A.text = A.text.slice(0, -1); | ||
A.inlines.pop(); | ||
A.entities.pop(); | ||
A.blocks.pop(); | ||
} | ||
// Kill whitespace after blocks | ||
if (A.text.slice(-1) === '\r') { | ||
if (B.text === SPACE || B.text === '\n') { | ||
return A; | ||
} else if (lastInB === SPACE || lastInB === '\n') { | ||
B.text = B.text.slice(1); | ||
B.inlines.shift(); | ||
B.entities.shift(); | ||
} | ||
} | ||
return { | ||
text: A.text + B.text, | ||
inlines: A.inlines.concat(B.inlines), | ||
entities: A.entities.concat(B.entities), | ||
blocks: A.blocks.concat(B.blocks) | ||
}; | ||
} | ||
/** | ||
* Check to see if we have anything like <p> <blockquote> <h1>... to create | ||
* block tags from. If we do, we can use those and ignore <div> tags. If we | ||
* don't, we can treat <div> tags as meaningful (unstyled) blocks. | ||
*/ | ||
function containsSemanticBlockMarkup(html) { | ||
return blockTags.some(function (tag) { | ||
return html.indexOf('<' + tag) !== -1; | ||
}); | ||
} | ||
function hasValidLinkText(link) { | ||
!(link instanceof HTMLAnchorElement) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Link must be an HTMLAnchorElement.') : invariant(false) : undefined; | ||
var protocol = link.protocol; | ||
return protocol === 'http:' || protocol === 'https:'; | ||
} | ||
function genFragment(node, inlineStyle, lastList, inBlock, blockTags, depth, inEntity) { | ||
var nodeName = node.nodeName.toLowerCase(); | ||
var newBlock = false; | ||
var nextBlockType = 'unstyled'; | ||
var lastLastBlock = lastBlock; | ||
// Base Case | ||
if (nodeName === '#text') { | ||
var text = node.textContent; | ||
if (text.trim() === '' && inBlock !== 'pre') { | ||
return getWhitespaceChunk(inEntity); | ||
} | ||
if (inBlock !== 'pre') { | ||
// Can't use empty string because MSWord | ||
text = text.replace(REGEX_LF, SPACE); | ||
} | ||
// save the last block so we can use it later | ||
lastBlock = nodeName; | ||
return { | ||
text: text, | ||
inlines: Array(text.length).fill(inlineStyle), | ||
entities: Array(text.length).fill(inEntity), | ||
blocks: [] | ||
}; | ||
} | ||
// save the last block so we can use it later | ||
lastBlock = nodeName; | ||
// BR tags | ||
if (nodeName === 'br') { | ||
if (lastLastBlock === 'br' && (!inBlock || getBlockTypeForTag(inBlock, lastList) === 'unstyled')) { | ||
return getBlockDividerChunk('unstyled', depth); | ||
} | ||
return getSoftNewlineChunk(); | ||
} | ||
var chunk = getEmptyChunk(); | ||
var newChunk = null; | ||
// Inline tags | ||
inlineStyle = processInlineTag(nodeName, node, inlineStyle); | ||
// Handle lists | ||
if (nodeName === 'ul' || nodeName === 'ol') { | ||
if (lastList) { | ||
depth += 1; | ||
} | ||
lastList = nodeName; | ||
} | ||
// Block Tags | ||
if (!inBlock && blockTags.indexOf(nodeName) !== -1) { | ||
chunk = getBlockDividerChunk(getBlockTypeForTag(nodeName, lastList), depth); | ||
inBlock = nodeName; | ||
newBlock = true; | ||
} else if (lastList && inBlock === 'li' && nodeName === 'li') { | ||
chunk = getBlockDividerChunk(getBlockTypeForTag(nodeName, lastList), depth); | ||
inBlock = nodeName; | ||
newBlock = true; | ||
nextBlockType = lastList === 'ul' ? 'unordered-list-item' : 'ordered-list-item'; | ||
} | ||
// Recurse through children | ||
var child = node.firstChild; | ||
if (child != null) { | ||
nodeName = child.nodeName.toLowerCase(); | ||
} | ||
var entityId = null; | ||
var href = null; | ||
while (child) { | ||
if (nodeName === 'a' && child.href && hasValidLinkText(child)) { | ||
href = new URI(child.href).toString(); | ||
entityId = DraftEntity.create('LINK', 'MUTABLE', { url: href }); | ||
} else { | ||
entityId = undefined; | ||
} | ||
newChunk = genFragment(child, inlineStyle, lastList, inBlock, blockTags, depth, entityId || inEntity); | ||
chunk = joinChunks(chunk, newChunk); | ||
var sibling = child.nextSibling; | ||
// Put in a newline to break up blocks inside blocks | ||
if (sibling && blockTags.indexOf(nodeName) >= 0 && inBlock) { | ||
chunk = joinChunks(chunk, getSoftNewlineChunk()); | ||
} | ||
if (sibling) { | ||
nodeName = sibling.nodeName.toLowerCase(); | ||
} | ||
child = sibling; | ||
} | ||
if (newBlock) { | ||
chunk = joinChunks(chunk, getBlockDividerChunk(nextBlockType, depth)); | ||
} | ||
return chunk; | ||
} | ||
function getChunkForHTML(html) { | ||
html = html.trim().replace(REGEX_CR, '').replace(REGEX_NBSP, SPACE); | ||
var safeBody = getSafeBodyFromHTML(html); | ||
if (!safeBody) { | ||
return null; | ||
} | ||
lastBlock = null; | ||
// Sometimes we aren't dealing with content that contains nice semantic | ||
// tags. In this case, use divs to separate everything out into paragraphs | ||
// and hope for the best. | ||
var workingBlocks = containsSemanticBlockMarkup(html) ? blockTags : ['div']; | ||
// Start with -1 block depth to offset the fact that we are passing in a fake | ||
// UL block to start with. | ||
var chunk = genFragment(safeBody, OrderedSet(), 'ul', null, workingBlocks, -1); | ||
// join with previous block to prevent weirdness on paste | ||
if (chunk.text.indexOf('\r') === 0) { | ||
chunk = { | ||
text: chunk.text.slice(1), | ||
inlines: chunk.inlines.slice(1), | ||
entities: chunk.entities.slice(1), | ||
blocks: chunk.blocks | ||
}; | ||
} | ||
// Kill block delimiter at the end | ||
if (chunk.text.slice(-1) === '\r') { | ||
chunk.text = chunk.text.slice(0, -1); | ||
chunk.inlines = chunk.inlines.slice(0, -1); | ||
chunk.entities = chunk.entities.slice(0, -1); | ||
chunk.blocks.pop(); | ||
} | ||
// If we saw no block tags, put an unstyled one in | ||
if (chunk.blocks.length === 0) { | ||
chunk.blocks.push({ type: 'unstyled', depth: 0 }); | ||
} | ||
// Sometimes we start with text that isn't in a block, which is then | ||
// followed by blocks. Need to fix up the blocks to add in | ||
// an unstyled block for this content | ||
if (chunk.text.split('\r').length === chunk.blocks.length + 1) { | ||
chunk.blocks.unshift({ type: 'unstyled', depth: 0 }); | ||
} | ||
return chunk; | ||
} | ||
var DraftPasteProcessor = { | ||
processHTML: function processHTML(html) { | ||
var chunk = getChunkForHTML(html); | ||
if (chunk == null) { | ||
return null; | ||
} | ||
var start = 0; | ||
return chunk.text.split('\r').map(function (textBlock, ii) { | ||
// Make absolutely certain that our text is acceptable. | ||
textBlock = sanitizeDraftText(textBlock); | ||
var end = start + textBlock.length; | ||
var inlines = nullthrows(chunk).inlines.slice(start, end); | ||
var entities = nullthrows(chunk).entities.slice(start, end); | ||
var characterList = List(inlines.map(function (style, ii) { | ||
var data = { style: style, entity: null }; | ||
if (entities[ii]) { | ||
data.entity = entities[ii]; | ||
} | ||
return CharacterMetadata.create(data); | ||
})); | ||
start = end + 1; | ||
return new ContentBlock({ | ||
key: generateRandomKey(), | ||
type: nullthrows(chunk).blocks[ii].type, | ||
depth: nullthrows(chunk).blocks[ii].depth, | ||
text: textBlock, | ||
characterList: characterList | ||
}); | ||
}); | ||
return convertFromHTMLtoContentBlocks(html); | ||
}, | ||
@@ -395,0 +32,0 @@ |
@@ -73,3 +73,3 @@ /** | ||
var textBlocks = null; | ||
var textBlocks = []; | ||
var text = data.getText(); | ||
@@ -89,16 +89,14 @@ this.props.onPasteRawText && this.props.onPasteRawText(text); | ||
// paste will preserve the newlines correctly. | ||
if (data.isRichText() && this.getClipboard()) { | ||
textBlocks = nullthrows(textBlocks); | ||
var textBlocksWithoutNewlines = textBlocks.filter(filterOutNewlines); | ||
var currentClipboard = this.getClipboard(); | ||
var clipboardWithoutNewlines = currentClipboard.toSeq().map(function (clipBlock) { | ||
return clipBlock.getText(); | ||
}).filter(filterOutNewlines).toArray(); | ||
var clipboardMatch = textBlocksWithoutNewlines.every(function (line, ii) { | ||
return line === clipboardWithoutNewlines[ii]; | ||
}); | ||
if (clipboardMatch) { | ||
this.update(insertFragment(this.props.editorState, currentClipboard)); | ||
var html = data.getHTML(); | ||
var internalClipboard = this.getClipboard(); | ||
if (data.isRichText() && internalClipboard) { | ||
if ( | ||
// If the editorKey is present in the pasted HTML, it should be safe to | ||
// assume this is an internal paste. | ||
html.indexOf(this.getEditorKey()) !== -1 || | ||
// The copy may have been made within a single block, in which case the | ||
// editor key won't be part of the paste. In this case, just check | ||
// whether the pasted text matches the internal clipboard. | ||
textBlocks.length === 1 && internalClipboard.size === 1 && internalClipboard.first().getText() === text) { | ||
this.update(insertFragment(this.props.editorState, internalClipboard)); | ||
return; | ||
@@ -109,6 +107,4 @@ } | ||
// If there is html paste data, try to parse that. | ||
var htmlData = data.getHTML(); | ||
if (htmlData) { | ||
var htmlFragment = DraftPasteProcessor.processHTML(htmlData); | ||
if (html) { | ||
var htmlFragment = DraftPasteProcessor.processHTML(html); | ||
if (htmlFragment) { | ||
@@ -115,0 +111,0 @@ var htmlMap = BlockMapBuilder.createFromArray(htmlFragment); |
@@ -62,10 +62,9 @@ /** | ||
}).toList(); | ||
var styles = Object.keys(DefaultDraftInlineStyle); | ||
var ranges = styles.map(function (style) { | ||
var ranges = styleList.flatten().toSet().map(function (style) { | ||
return getEncodedInlinesForType(block, styleList, style); | ||
}); | ||
return Array.prototype.concat.apply(EMPTY_ARRAY, ranges); | ||
return Array.prototype.concat.apply(EMPTY_ARRAY, ranges.toJS()); | ||
} | ||
module.exports = encodeInlineStyleRanges; |
{ | ||
"name": "draft-js", | ||
"description": "A React framework for building text editors.", | ||
"version": "0.2.2", | ||
"version": "0.3.0", | ||
"keywords": [ | ||
@@ -23,2 +23,3 @@ "draftjs", | ||
"scripts": { | ||
"prepublish": "npm run build", | ||
"pretest": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json", | ||
@@ -31,3 +32,3 @@ "build": "gulp", | ||
"dependencies": { | ||
"fbjs": "^0.8.0-alpha.1", | ||
"fbjs": "^0.8.0-alpha.2", | ||
"immutable": "^3.7.4" | ||
@@ -51,5 +52,7 @@ }, | ||
"gulp-browserify-thin": "^0.1.5", | ||
"gulp-clean-css": "^2.0.3", | ||
"gulp-concat-css": "^2.2.0", | ||
"gulp-derequire": "^2.1.0", | ||
"gulp-flatten": "^0.2.0", | ||
"gulp-header": "^1.7.1", | ||
"gulp-uglify": "^1.2.0", | ||
@@ -56,0 +59,0 @@ "gulp-util": "^3.0.6", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
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
138
880648
26
19098
Updatedfbjs@^0.8.0-alpha.2