249 lines
8.3 KiB
JavaScript
249 lines
8.3 KiB
JavaScript
import DiffMatchPatch from 'diff-match-patch';
|
|
import cledit from '../libs/cledit';
|
|
import utils from './utils';
|
|
import diffUtils from './diffUtils';
|
|
import store from '../store';
|
|
import EditorClassApplier from '../components/common/EditorClassApplier';
|
|
import PreviewClassApplier from '../components/common/PreviewClassApplier';
|
|
|
|
let clEditor;
|
|
// let discussionIds = {};
|
|
let discussionMarkers = {};
|
|
let markerKeys;
|
|
let markerIdxMap;
|
|
let previousPatchableText;
|
|
let currentPatchableText;
|
|
let isChangePatch;
|
|
let contentId;
|
|
let editorClassAppliers = {};
|
|
let previewClassAppliers = {};
|
|
|
|
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
|
const getMarker = (offsetName) => {
|
|
const markerKey = `${discussionId}:${offsetName}`;
|
|
let marker = discussionMarkers[markerKey];
|
|
if (!marker) {
|
|
marker = new cledit.Marker(discussion[offsetName], offsetName === 'end');
|
|
marker.discussionId = discussionId;
|
|
marker.offsetName = offsetName;
|
|
clEditor.addMarker(marker);
|
|
discussionMarkers[markerKey] = marker;
|
|
}
|
|
onMarker(marker);
|
|
};
|
|
getMarker('start');
|
|
getMarker('end');
|
|
}
|
|
|
|
function syncDiscussionMarkers(content, writeOffsets) {
|
|
const discussions = {
|
|
...content.discussions,
|
|
};
|
|
const newDiscussion = store.getters['discussion/newDiscussion'];
|
|
if (newDiscussion) {
|
|
discussions[store.state.discussion.newDiscussionId] = {
|
|
...newDiscussion,
|
|
};
|
|
}
|
|
Object.entries(discussionMarkers).forEach(([markerKey, marker]) => {
|
|
// Remove marker if discussion was removed
|
|
const discussion = discussions[marker.discussionId];
|
|
if (!discussion) {
|
|
clEditor.removeMarker(marker);
|
|
delete discussionMarkers[markerKey];
|
|
}
|
|
});
|
|
|
|
Object.entries(discussions).forEach(([discussionId, discussion]) => {
|
|
getDiscussionMarkers(discussion, discussionId, writeOffsets
|
|
? (marker) => {
|
|
discussion[marker.offsetName] = marker.offset;
|
|
}
|
|
: (marker) => {
|
|
marker.offset = discussion[marker.offsetName];
|
|
});
|
|
});
|
|
|
|
if (writeOffsets && newDiscussion) {
|
|
store.commit('discussion/patchNewDiscussion',
|
|
discussions[store.state.discussion.newDiscussionId]);
|
|
}
|
|
}
|
|
|
|
function removeDiscussionMarkers() {
|
|
Object.entries(discussionMarkers).forEach(([, marker]) => {
|
|
clEditor.removeMarker(marker);
|
|
});
|
|
discussionMarkers = {};
|
|
markerKeys = [];
|
|
markerIdxMap = Object.create(null);
|
|
}
|
|
|
|
const diffMatchPatch = new DiffMatchPatch();
|
|
|
|
function makePatches() {
|
|
const diffs = diffMatchPatch.diff_main(previousPatchableText, currentPatchableText);
|
|
return diffMatchPatch.patch_make(previousPatchableText, diffs);
|
|
}
|
|
|
|
function applyPatches(patches) {
|
|
const newPatchableText = diffMatchPatch.patch_apply(patches, currentPatchableText)[0];
|
|
let result = newPatchableText;
|
|
if (markerKeys.length) {
|
|
// Strip text markers
|
|
result = result.replace(new RegExp(`[\ue000-${String.fromCharCode((0xe000 + markerKeys.length) - 1)}]`, 'g'), '');
|
|
}
|
|
// Expect a `contentChanged` event
|
|
if (result !== clEditor.getContent()) {
|
|
previousPatchableText = currentPatchableText;
|
|
currentPatchableText = newPatchableText;
|
|
isChangePatch = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function reversePatches(patches) {
|
|
const result = diffMatchPatch.patch_deepCopy(patches).reverse();
|
|
result.forEach((patch) => {
|
|
patch.diffs.forEach((diff) => {
|
|
diff[0] = -diff[0];
|
|
});
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export default {
|
|
createClEditor(editorElt) {
|
|
this.clEditor = cledit(editorElt, editorElt.parentNode);
|
|
clEditor = this.clEditor;
|
|
clEditor.on('contentChanged', (text) => {
|
|
const oldContent = store.getters['content/current'];
|
|
const newContent = {
|
|
...utils.deepCopy(oldContent),
|
|
text: utils.sanitizeText(text),
|
|
};
|
|
syncDiscussionMarkers(newContent, true);
|
|
if (!isChangePatch) {
|
|
previousPatchableText = currentPatchableText;
|
|
currentPatchableText = diffUtils.makePatchableText(newContent, markerKeys, markerIdxMap);
|
|
} else {
|
|
// Take a chance to restore discussion offsets on undo/redo
|
|
newContent.text = currentPatchableText;
|
|
diffUtils.restoreDiscussionOffsets(newContent, markerKeys);
|
|
syncDiscussionMarkers(newContent, false);
|
|
}
|
|
store.dispatch('content/patchCurrent', newContent);
|
|
isChangePatch = false;
|
|
});
|
|
clEditor.on('focus', () => store.commit('discussion/setNewCommentFocus', false));
|
|
},
|
|
initClEditorInternal(opts) {
|
|
const content = store.getters['content/current'];
|
|
if (content) {
|
|
removeDiscussionMarkers(); // Markers will be recreated on contentChanged
|
|
const contentState = store.getters['contentState/current'];
|
|
const options = Object.assign({
|
|
selectionStart: contentState.selectionStart,
|
|
selectionEnd: contentState.selectionEnd,
|
|
patchHandler: {
|
|
makePatches,
|
|
applyPatches,
|
|
reversePatches,
|
|
},
|
|
}, opts);
|
|
|
|
if (contentId !== content.id) {
|
|
contentId = content.id;
|
|
currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
|
previousPatchableText = currentPatchableText;
|
|
syncDiscussionMarkers(content, false);
|
|
options.content = content.text;
|
|
}
|
|
|
|
clEditor.init(options);
|
|
}
|
|
},
|
|
applyContent() {
|
|
if (clEditor) {
|
|
const content = store.getters['content/current'];
|
|
if (clEditor.setContent(content.text, true).range) {
|
|
// Marker will be recreated on contentChange
|
|
removeDiscussionMarkers();
|
|
} else {
|
|
syncDiscussionMarkers(content, false);
|
|
}
|
|
}
|
|
},
|
|
getTrimmedSelection() {
|
|
const selectionMgr = clEditor.selectionMgr;
|
|
let start = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
|
let end = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
|
const text = clEditor.getContent();
|
|
while ((text[start] || '').match(/\s/)) {
|
|
start += 1;
|
|
}
|
|
while ((text[end - 1] || '').match(/\s/)) {
|
|
end -= 1;
|
|
}
|
|
return start < end && { start, end };
|
|
},
|
|
initHighlighters() {
|
|
store.watch(
|
|
() => store.getters['discussion/newDiscussion'],
|
|
() => syncDiscussionMarkers(store.getters['content/current'], false),
|
|
);
|
|
|
|
store.watch(
|
|
() => store.getters['discussion/currentFileDiscussions'],
|
|
(discussions) => {
|
|
const classGetter = (type, discussionId) => () => {
|
|
const classes = [`discussion-${type}-highlighting--${discussionId}`, `discussion-${type}-highlighting`];
|
|
if (store.state.discussion.currentDiscussionId === discussionId) {
|
|
classes.push(`discussion-${type}-highlighting--selected`);
|
|
}
|
|
return classes;
|
|
};
|
|
const offsetGetter = discussionId => () => {
|
|
const startMarker = discussionMarkers[`${discussionId}:start`];
|
|
const endMarker = discussionMarkers[`${discussionId}:end`];
|
|
return startMarker && endMarker && {
|
|
start: startMarker.offset,
|
|
end: endMarker.offset,
|
|
};
|
|
};
|
|
|
|
// Editor class appliers
|
|
const oldEditorClassAppliers = editorClassAppliers;
|
|
editorClassAppliers = {};
|
|
Object.keys(discussions).forEach((discussionId) => {
|
|
const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
|
|
classGetter('editor', discussionId), offsetGetter(discussionId), { discussionId });
|
|
editorClassAppliers[discussionId] = classApplier;
|
|
});
|
|
// Clean unused class appliers
|
|
Object.entries(oldEditorClassAppliers).forEach(([discussionId, classApplier]) => {
|
|
if (!editorClassAppliers[discussionId]) {
|
|
classApplier.stop();
|
|
}
|
|
});
|
|
|
|
// Preview class appliers
|
|
const oldPreviewClassAppliers = previewClassAppliers;
|
|
previewClassAppliers = {};
|
|
Object.keys(discussions).forEach((discussionId) => {
|
|
const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
|
|
classGetter('preview', discussionId), offsetGetter(discussionId), { discussionId });
|
|
previewClassAppliers[discussionId] = classApplier;
|
|
});
|
|
// Clean unused class appliers
|
|
Object.entries(oldPreviewClassAppliers).forEach(([discussionId, classApplier]) => {
|
|
if (!previewClassAppliers[discussionId]) {
|
|
classApplier.stop();
|
|
}
|
|
});
|
|
},
|
|
);
|
|
},
|
|
};
|
|
|