Stackedit/src/services/editorSvcDiscussions.js
2017-12-10 23:49:20 +00:00

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();
}
});
},
);
},
};