258 lines
8.7 KiB
JavaScript
258 lines
8.7 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 discussionMarkers = {};
|
|
let markerKeys;
|
|
let markerIdxMap;
|
|
let previousPatchableText;
|
|
let currentPatchableText;
|
|
let isChangePatch;
|
|
let contentId;
|
|
let editorClassAppliers = {};
|
|
let previewClassAppliers = {};
|
|
|
|
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
|
function 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.keys(discussionMarkers).forEach((markerKey) => {
|
|
const marker = discussionMarkers[markerKey];
|
|
// Remove marker if discussion was removed
|
|
const discussion = discussions[marker.discussionId];
|
|
if (!discussion) {
|
|
clEditor.removeMarker(marker);
|
|
delete discussionMarkers[markerKey];
|
|
}
|
|
});
|
|
|
|
Object.keys(discussions).forEach((discussionId) => {
|
|
const discussion = discussions[discussionId];
|
|
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.keys(discussionMarkers).forEach((markerKey) => {
|
|
clEditor.removeMarker(discussionMarkers[markerKey]);
|
|
});
|
|
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;
|
|
removeDiscussionMarkers();
|
|
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
|
|
diffUtils.restoreDiscussionOffsets(newContent, markerKeys);
|
|
syncDiscussionMarkers(newContent, false);
|
|
}
|
|
store.dispatch('content/patchCurrent', newContent);
|
|
isChangePatch = false;
|
|
});
|
|
},
|
|
initClEditorInternal(opts) {
|
|
const content = store.getters['content/current'];
|
|
if (content) {
|
|
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) => {
|
|
// Editor class appliers
|
|
const oldEditorClassAppliers = editorClassAppliers;
|
|
editorClassAppliers = {};
|
|
Object.keys(discussions).forEach((discussionId) => {
|
|
const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
|
|
() => {
|
|
const classes = [`discussion-editor-highlighting--${discussionId}`, 'discussion-editor-highlighting'];
|
|
if (store.state.discussion.currentDiscussionId === discussionId) {
|
|
classes.push('discussion-editor-highlighting--selected');
|
|
}
|
|
return classes;
|
|
},
|
|
() => {
|
|
const startMarker = discussionMarkers[`${discussionId}:start`];
|
|
const endMarker = discussionMarkers[`${discussionId}:end`];
|
|
return startMarker && endMarker && {
|
|
start: startMarker.offset,
|
|
end: endMarker.offset,
|
|
};
|
|
}, { discussionId });
|
|
editorClassAppliers[discussionId] = classApplier;
|
|
});
|
|
Object.keys(oldEditorClassAppliers).forEach((discussionId) => {
|
|
if (!editorClassAppliers[discussionId]) {
|
|
oldEditorClassAppliers[discussionId].stop();
|
|
}
|
|
});
|
|
|
|
// Preview class appliers
|
|
const oldPreviewClassAppliers = previewClassAppliers;
|
|
previewClassAppliers = {};
|
|
Object.keys(discussions).forEach((discussionId) => {
|
|
const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
|
|
() => {
|
|
const classes = [`discussion-preview-highlighting--${discussionId}`, 'discussion-preview-highlighting'];
|
|
if (store.state.discussion.currentDiscussionId === discussionId) {
|
|
classes.push('discussion-preview-highlighting--selected');
|
|
}
|
|
return classes;
|
|
},
|
|
() => {
|
|
const startMarker = discussionMarkers[`${discussionId}:start`];
|
|
const endMarker = discussionMarkers[`${discussionId}:end`];
|
|
return startMarker && endMarker && {
|
|
start: this.getPreviewOffset(startMarker.offset),
|
|
end: this.getPreviewOffset(endMarker.offset),
|
|
};
|
|
}, { discussionId });
|
|
previewClassAppliers[discussionId] = classApplier;
|
|
});
|
|
Object.keys(oldPreviewClassAppliers).forEach((discussionId) => {
|
|
if (!previewClassAppliers[discussionId]) {
|
|
oldPreviewClassAppliers[discussionId].stop();
|
|
}
|
|
});
|
|
},
|
|
);
|
|
},
|
|
};
|
|
|