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