Skip to content

Commit 907062f

Browse files
committed
Using debounce solution. Trying to make a decent package
2 parents 93ab68e + 2cec974 commit 907062f

File tree

7 files changed

+242
-216
lines changed

7 files changed

+242
-216
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
demo/
2+
3+
!demo/main.js
4+
!demo/index.html

demo/diff-area.js

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
var React = require('react');
2+
var Immutable = require('immutable');
3+
var Draft = require('draft-js');
4+
var debounce = require('lodash.debounce');
5+
6+
var diff_word_mode = require('./lib/diff-word-mode');
7+
8+
var DWM = new diff_word_mode();
9+
10+
// diff_match_patch codes
11+
var DIFF = {
12+
DELETE: -1,
13+
INSERT: 1,
14+
EQUAL: 0
15+
};
16+
17+
var DEBOUNCE_WAIT = 300; // ms
18+
var DEBOUNCE_OPTS = {
19+
trailing: true // We want to update after the delay only
20+
};
21+
22+
var DiffArea = React.createClass({
23+
24+
propTypes: {
25+
left: React.PropTypes.string,
26+
right: React.PropTypes.string
27+
},
28+
29+
getInitialState: function () {
30+
// Make a debounced diff update
31+
this.debouncedUpdateDiffs = debounce(this.updateDiffs, DEBOUNCE_WAIT, DEBOUNCE_OPTS);
32+
var left = this.props.left;
33+
var right = this.props.right;
34+
35+
var state = {};
36+
37+
// Create editors state
38+
state.leftState = editorStateFromText(left);
39+
state.rightState = editorStateFromText(right);
40+
41+
// Compute diff on whole texts
42+
var diffs = computeDiff(left, right);
43+
var mappingLeft = mapDiffsToBlocks(diffs, DIFF.REMOVED, state.leftState.getCurrentContent().getBlockMap());
44+
var mappingRight = mapDiffsToBlocks(diffs, DIFF.INSERTED, state.rightState.getCurrentContent().getBlockMap());
45+
// Update the decorators
46+
state.leftState = Draft.EditorState.set(state.leftState, {
47+
decorator: createDiffsDecorator(mappingLeft, DIFF.REMOVED)
48+
});
49+
state.rightState = Draft.EditorState.set(state.rightState, {
50+
decorator: createDiffsDecorator(mappingRight, DIFF.INSERTED)
51+
});
52+
53+
return state;
54+
},
55+
56+
onRightChanged: function (rightState) {
57+
// Text changed ?
58+
var contentChanged = this.state.rightState.getCurrentContent()
59+
!== rightState.getCurrentContent();
60+
// Update diffs
61+
if (contentChanged) {
62+
this.debouncedUpdateDiffs();
63+
}
64+
// Update the EditorState
65+
this.setState({
66+
rightState: rightState
67+
});
68+
},
69+
70+
onLeftChanged: function (leftState) {
71+
var contentChanged = this.state.leftState.getCurrentContent()
72+
!== leftState.getCurrentContent();
73+
// Update diffs
74+
if (contentChanged) {
75+
this.debouncedUpdateDiffs();
76+
}
77+
// Update the EditorState
78+
this.setState({
79+
leftState: leftState
80+
});
81+
},
82+
83+
// We use a debounced version of it
84+
updateDiffs: function () {
85+
var newState = {};
86+
var left = this.state.leftState.getCurrentContent().getPlainText();
87+
var right = this.state.rightState.getCurrentContent().getPlainText();
88+
89+
var diffs = computeDiff(left, right);
90+
91+
var mappingLeft = mapDiffsToBlocks(diffs, DIFF.REMOVED, this.state.leftState.getCurrentContent().getBlockMap());
92+
var mappingRight = mapDiffsToBlocks(diffs, DIFF.INSERTED, this.state.rightState.getCurrentContent().getBlockMap());
93+
94+
// Update the decorators
95+
newState.leftState = Draft.EditorState.set(this.state.leftState, {
96+
decorator: createDiffsDecorator(mappingLeft, DIFF.REMOVED)
97+
});
98+
newState.rightState = Draft.EditorState.set(this.state.rightState, {
99+
decorator: createDiffsDecorator(mappingRight, DIFF.INSERTED)
100+
});
101+
this.setState(newState);
102+
},
103+
104+
render: function () {
105+
return <div className='diffarea'>
106+
<div className='left'>
107+
<Draft.Editor
108+
editorState={this.state.leftState}
109+
onChange={this.onLeftChanged}
110+
/>
111+
</div>
112+
<div className='right'>
113+
<Draft.Editor
114+
editorState={this.state.rightState}
115+
onChange={this.onRightChanged}
116+
/>
117+
</div>
118+
</div>;
119+
}
120+
});
121+
122+
function editorStateFromText(text) {
123+
var content = Draft.ContentState.createFromText(text);
124+
return Draft.EditorState.createWithContent(content);
125+
}
126+
127+
function computeDiff(txt1, txt2) {
128+
var diffs = DWM.diff_wordMode(txt1, txt2);
129+
return diffs;
130+
}
131+
132+
/**
133+
* Returns the lists of highlighted ranges for each block of a blockMap
134+
* @returns {Immutable.Map} blockKey -> { text, ranges: Array<{start, end}}>
135+
*/
136+
function mapDiffsToBlocks(diffs, type, blockMap) {
137+
var charIndex = 0;
138+
var absoluteRanges = [];
139+
140+
diffs.forEach(function (diff) {
141+
var diffType = diff[0];
142+
var diffText = diff[1];
143+
if (diffType === DIFF.EQUAL) {
144+
// No highlight. Move to next difference
145+
charIndex += diffText.length;
146+
} else if (diffType === type) {
147+
// Highlight, and move to next difference
148+
absoluteRanges.push({
149+
start: charIndex,
150+
end: charIndex + diffText.length
151+
});
152+
charIndex += diffText.length;
153+
} else {
154+
// The diff text should not be in the contentBlock, so skip.
155+
return;
156+
}
157+
});
158+
159+
// `end` excluded
160+
function findRangesBetween(ranges, start, end) {
161+
var res = [];
162+
ranges.forEach(function (range) {
163+
if (range.start < end && range.end > start) {
164+
var intersectionStart = Math.max(range.start, start);
165+
var intersectionEnd = Math.min(range.end, end);
166+
// Push relative range
167+
res.push({
168+
start: intersectionStart - start,
169+
end: intersectionEnd - start
170+
});
171+
}
172+
});
173+
return res;
174+
}
175+
176+
var blockStartIndex = 0;
177+
return blockMap.map(function (block) {
178+
var ranges = findRangesBetween(absoluteRanges, blockStartIndex, blockStartIndex + block.getLength());
179+
blockStartIndex += block.getLength() + 1; // Account for \n
180+
return {
181+
text: block.getText(),
182+
ranges: ranges
183+
};
184+
});
185+
}
186+
187+
// Decorators
188+
189+
var InsertedSpan = function (props) {
190+
return <span {...props} className="inserted">{props.children}</span>;
191+
};
192+
var RemovedSpan = function (props) {
193+
return <span {...props} className="removed">{props.children}</span>;
194+
};
195+
196+
/**
197+
* @param type The type of diff to highlight
198+
*/
199+
function createDiffsDecorator(mappedRanges, type) {
200+
return new Draft.CompositeDecorator([{
201+
strategy: findDiff.bind(undefined, mappedRanges, type),
202+
component: type === DIFF.INSERTED ? InsertedSpan : RemovedSpan
203+
}]);
204+
}
205+
206+
/**
207+
* Applies the decorator callback to all differences in the content block.
208+
* This needs to be cheap, because decorators are called often.
209+
*/
210+
function findDiff(mappedRanges, type, contentBlock, callback) {
211+
var mapping = mappedRanges.get(contentBlock.getKey());
212+
if (mapping && mapping.text === contentBlock.getText()) {
213+
mapping.ranges.forEach(function (range) {
214+
callback(range.start, range.end);
215+
});
216+
}
217+
}
218+
219+
module.exports = DiffArea;

demo/diff.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ body {
22
font-family: monospace;
33
}
44

5-
.left {
6-
color: #666;
7-
}
8-
95
.left, .right {
106
width: 44%;
117
margin: 20px 3%;

demo/index.html

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,12 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.7.0/Draft.min.css">
4+
<link rel="stylesheet" href="draft.css">
55
<link rel="stylesheet" href="diff.css">
66
</head>
77

88
<body>
99
<div id="content"></div>
10-
11-
<p id="left-initial" hidden="true">Accusantium libero illum tempore provident doloribus voluptate. Blanditiis et recusandae unde non necessitatibus. Nulla qui libero optio doloremque dolorum accusamus.
12-
13-
Sapiente est, un mot par ci, et corporis. Porro magni sit similique quo. Molestias rerum quo ipsum ut. Aut sint id non mollitia.
14-
15-
Voluptas hic quia dolores quo inventore. Quasi minima sit ut culpa consequuntur et. Inventore perferendis nihil aut ipsa odio. Possimus ratione temporibus et aut. Dicta incidunt magni voluptatem.
16-
17-
Molestiae optio et quam at labore voluptatem animi. Sed libero odio repellendus iure omnis eos. Alias sapiente placeat est provident repellendus. Earum minima vero nesciunt voluptatem ipsa. Sint vitae et hic eveniet sunt. Doloremque ea esse consequatur.</p>
18-
19-
<p id="right-initial" hidden="true">Accusantium libero illum tempore provident doloribus voluptate. Blanditiis et recusandae unde non necessitatibus. Nulla qui libero optio doloremque dolorum accusamus.
20-
21-
Sapiente est molestias et corporis. Porro magni sit similique quo. Molestias rerum quo ipsum ut. Aut sint id non mollitia.
22-
23-
Voluptas hic quia dolores quo inventore. Quasi minima sit ut culpa consequuntur et. Inventore perferendis nihil aut ipsa odio. Possimus ratione temporibus et aut. Dicta incidunt magni voluptatem.
24-
25-
Id est unde eum. Et voluptatem ut fugiat consequatur temporibus harum. Quasi quae ut accusantium. Et architecto rerum optio qui maxime molestiae libero.
26-
27-
Molestiae optio et quam at labore voluptatem animi. Sed libero odio repellendus iure omnis eos. Alias sapiente placeat est provident repellendus. Earum minima vero nesciunt voluptatem ipsa. Sint vitae et hic eveniet sunt. Doloremque ea esse consequatur.</p>
28-
2910
<script type="text/javascript" src="bundle.js"></script>
3011
</body>
3112
</html>

demo.js renamed to demo/main.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@ var React = require('react');
22
var ReactDOM = require('react-dom');
33

44
var DiffArea = require('./diff-area');
5-
var data = require('./test/data');
5+
var data = require('../test/data');
66

77
// ---- main
88

9-
var left = document.getElementById('left-initial').innerText;
10-
var right = document.getElementById('right-initial').innerText;
11-
129
left = data.text1;
1310
right = data.text2;
1411

0 commit comments

Comments
 (0)