Stackedit/public/res/editor.js

1132 lines
33 KiB
JavaScript
Raw Normal View History

2014-03-16 02:13:42 +00:00
/* jshint -W084, -W099 */
2014-04-06 00:59:32 +00:00
// Credit to http://dabblet.com/
2014-03-16 02:13:42 +00:00
define([
2014-04-26 00:53:06 +00:00
'jquery',
'underscore',
'utils',
'settings',
'eventMgr',
'prism-core',
'diff_match_patch_uncompressed',
'jsondiffpatch',
'crel',
'rangy',
'MutationObservers',
'libs/prism-markdown'
], function($, _, utils, settings, eventMgr, Prism, diff_match_patch, jsondiffpatch, crel, rangy) {
var editor = {};
var scrollTop = 0;
var inputElt;
var $inputElt;
var contentElt;
var $contentElt;
var marginElt;
var $marginElt;
var previewElt;
var pagedownEditor;
2014-08-21 18:47:54 +00:00
var trailingLfNode;
2014-04-26 00:53:06 +00:00
var refreshPreviewLater = (function() {
var elapsedTime = 0;
var timeoutId;
2014-04-26 00:53:06 +00:00
var refreshPreview = function() {
var startTime = Date.now();
pagedownEditor.refreshPreview();
elapsedTime = Date.now() - startTime;
};
if(settings.lazyRendering === true) {
return _.debounce(refreshPreview, 500);
}
return function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(refreshPreview, elapsedTime < 2000 ? elapsedTime : 2000);
2014-04-26 00:53:06 +00:00
};
})();
eventMgr.addListener('onPagedownConfigure', function(pagedownEditorParam) {
pagedownEditor = pagedownEditorParam;
});
2014-05-04 18:13:55 +00:00
var isComposing = 0;
2014-04-26 00:53:06 +00:00
eventMgr.addListener('onSectionsCreated', function(newSectionList) {
2014-05-04 17:30:14 +00:00
if(!isComposing) {
updateSectionList(newSectionList);
highlightSections();
}
2014-04-26 00:53:06 +00:00
if(fileChanged === true) {
// Refresh preview synchronously
pagedownEditor.refreshPreview();
}
else {
refreshPreviewLater();
}
});
var fileChanged = true;
var fileDesc;
eventMgr.addListener('onFileSelected', function(selectedFileDesc) {
fileChanged = true;
fileDesc = selectedFileDesc;
});
// Used to detect editor changes
function Watcher() {
this.isWatching = false;
var contentObserver;
this.startWatching = function() {
this.isWatching = true;
contentObserver = contentObserver || new MutationObserver(checkContentChange);
contentObserver.observe(contentElt, {
childList: true,
subtree: true,
characterData: true
});
};
this.stopWatching = function() {
contentObserver.disconnect();
this.isWatching = false;
};
this.noWatch = function(cb) {
if(this.isWatching === true) {
this.stopWatching();
cb();
this.startWatching();
}
else {
cb();
}
};
}
var watcher = new Watcher();
editor.watcher = watcher;
var diffMatchPatch = new diff_match_patch();
var jsonDiffPatch = jsondiffpatch.create({
objectHash: function(obj) {
return JSON.stringify(obj);
},
arrays: {
detectMove: false
},
textDiff: {
minLength: 9999999
}
});
function SelectionMgr() {
var self = this;
2014-08-10 23:44:54 +00:00
var lastSelectionStart = 0, lastSelectionEnd = 0;
2014-04-26 00:53:06 +00:00
this.selectionStart = 0;
this.selectionEnd = 0;
this.cursorY = 0;
2014-07-13 21:45:55 +00:00
this.adjustTop = 0;
this.adjustBottom = 0;
this.findOffsets = function(offsetList) {
var result = [];
if(!offsetList.length) {
return result;
}
var offset = offsetList.shift();
2014-04-26 00:53:06 +00:00
var walker = document.createTreeWalker(contentElt, 4, null, false);
var text = '';
2014-07-13 21:45:55 +00:00
var walkerOffset = 0;
2014-04-26 00:53:06 +00:00
while(walker.nextNode()) {
text = walker.currentNode.nodeValue || '';
2014-07-13 21:45:55 +00:00
var newWalkerOffset = walkerOffset + text.length;
while(newWalkerOffset > offset) {
result.push({
2014-04-26 00:53:06 +00:00
container: walker.currentNode,
2014-07-13 21:45:55 +00:00
offsetInContainer: offset - walkerOffset,
2014-04-26 00:53:06 +00:00
offset: offset
2014-07-13 21:45:55 +00:00
});
if(!offsetList.length) {
return result;
}
offset = offsetList.shift();
2014-04-26 00:53:06 +00:00
}
2014-07-13 21:45:55 +00:00
walkerOffset = newWalkerOffset;
2014-04-26 00:53:06 +00:00
}
2014-07-13 21:45:55 +00:00
do {
result.push({
container: walker.currentNode,
2014-08-21 18:47:54 +00:00
offsetInContainer: text.length,
2014-07-13 21:45:55 +00:00
offset: offset
});
offset = offsetList.shift();
}
while(offset);
return result;
2014-04-26 00:53:06 +00:00
};
this.createRange = function(start, end) {
start = start < 0 ? 0 : start;
end = end < 0 ? 0 : end;
var range = document.createRange();
2014-07-24 12:20:56 +00:00
var offsetList = [], startIndex, endIndex;
2014-07-13 21:45:55 +00:00
if(_.isNumber(start)) {
offsetList.push(start);
2014-07-24 12:20:56 +00:00
startIndex = offsetList.length - 1;
2014-07-13 21:45:55 +00:00
}
if(_.isNumber(end)) {
offsetList.push(end);
2014-07-24 12:20:56 +00:00
endIndex = offsetList.length - 1;
2014-07-13 21:45:55 +00:00
}
offsetList = this.findOffsets(offsetList);
2014-07-24 12:20:56 +00:00
var startOffset = _.isObject(start) ? start : offsetList[startIndex];
2014-07-13 21:45:55 +00:00
range.setStart(startOffset.container, startOffset.offsetInContainer);
var endOffset = startOffset;
2014-04-26 00:53:06 +00:00
if(end && end != start) {
2014-07-24 12:20:56 +00:00
endOffset = _.isObject(end) ? end : offsetList[endIndex];
2014-04-26 00:53:06 +00:00
}
2014-07-13 21:45:55 +00:00
range.setEnd(endOffset.container, endOffset.offsetInContainer);
2014-04-26 00:53:06 +00:00
return range;
};
2014-05-21 22:54:50 +00:00
var adjustScroll;
var debouncedUpdateCursorCoordinates = utils.debounce(function() {
2014-04-26 00:53:06 +00:00
$inputElt.toggleClass('has-selection', this.selectionStart !== this.selectionEnd);
var coordinates = this.getCoordinates(this.selectionEnd, this.selectionEndContainer, this.selectionEndOffset);
if(this.cursorY !== coordinates.y) {
this.cursorY = coordinates.y;
eventMgr.onCursorCoordinates(coordinates.x, coordinates.y);
2014-07-30 23:43:14 +00:00
}
if(adjustScroll) {
var adjustTop, adjustBottom;
adjustTop = adjustBottom = inputElt.offsetHeight / 2 * settings.cursorFocusRatio;
adjustTop = this.adjustTop || adjustTop;
adjustBottom = this.adjustBottom || adjustTop;
if(adjustTop && adjustBottom) {
var cursorMinY = inputElt.scrollTop + adjustTop;
var cursorMaxY = inputElt.scrollTop + inputElt.offsetHeight - adjustBottom;
if(selectionMgr.cursorY < cursorMinY) {
inputElt.scrollTop += selectionMgr.cursorY - cursorMinY;
}
else if(selectionMgr.cursorY > cursorMaxY) {
inputElt.scrollTop += selectionMgr.cursorY - cursorMaxY;
2014-05-21 22:54:50 +00:00
}
2014-04-26 00:53:06 +00:00
}
}
2014-05-21 22:54:50 +00:00
adjustScroll = false;
}, this);
this.updateCursorCoordinates = function(adjustScrollParam) {
adjustScroll = adjustScroll || adjustScrollParam;
debouncedUpdateCursorCoordinates();
};
2014-05-23 18:19:07 +00:00
this.updateSelectionRange = function() {
2014-05-21 22:54:50 +00:00
var min = Math.min(this.selectionStart, this.selectionEnd);
var max = Math.max(this.selectionStart, this.selectionEnd);
2014-06-09 11:58:18 +00:00
var range = this.createRange(min, max);
var selection = rangy.getSelection();
2014-05-21 22:54:50 +00:00
selection.removeAllRanges();
selection.addRange(range, this.selectionStart > this.selectionEnd);
2014-05-21 22:54:50 +00:00
};
2014-08-10 23:44:54 +00:00
var saveLastSelection = _.debounce(function() {
lastSelectionStart = self.selectionStart;
lastSelectionEnd = self.selectionEnd;
}, 50);
2014-05-21 22:54:50 +00:00
this.setSelectionStartEnd = function(start, end) {
2014-04-26 00:53:06 +00:00
if(start === undefined) {
start = this.selectionStart;
}
if(start < 0) {
start = 0;
}
if(end === undefined) {
end = this.selectionEnd;
}
if(end < 0) {
end = 0;
}
this.selectionStart = start;
this.selectionEnd = end;
2014-05-21 22:54:50 +00:00
fileDesc.editorStart = start;
fileDesc.editorEnd = end;
2014-08-10 23:44:54 +00:00
saveLastSelection();
2014-04-26 00:53:06 +00:00
};
this.saveSelectionState = (function() {
2014-05-21 22:54:50 +00:00
function save() {
2014-04-26 00:53:06 +00:00
if(fileChanged === false) {
var selectionStart = self.selectionStart;
var selectionEnd = self.selectionEnd;
var range;
var selection = rangy.getSelection();
2014-04-26 00:53:06 +00:00
if(selection.rangeCount > 0) {
var selectionRange = selection.getRangeAt(0);
var element = selectionRange.startContainer;
if((contentElt.compareDocumentPosition(element) & 0x10)) {
range = selectionRange;
var container = element;
var offset = range.startOffset;
do {
while(element = element.previousSibling) {
if(element.textContent) {
offset += element.textContent.length;
}
}
element = container = container.parentNode;
} while(element && element != inputElt);
if(selection.isBackwards()) {
2014-04-26 00:53:06 +00:00
selectionStart = offset + (range + '').length;
selectionEnd = offset;
}
else {
selectionStart = offset;
selectionEnd = offset + (range + '').length;
}
2014-08-21 18:47:54 +00:00
if(selectionStart === selectionEnd && selectionStart > textContent.length) {
// In Firefox cursor can be after the trailingLfNode
2014-08-21 23:14:58 +00:00
selection.nativeSelection.modify && selection.nativeSelection.modify("move", "backward", "character");
2014-08-21 18:47:54 +00:00
selectionStart = --selectionEnd;
}
2014-04-26 00:53:06 +00:00
}
}
2014-05-21 22:54:50 +00:00
self.setSelectionStartEnd(selectionStart, selectionEnd);
2014-04-26 00:53:06 +00:00
}
undoMgr.saveSelectionState();
}
2014-06-09 11:58:18 +00:00
2014-05-21 22:54:50 +00:00
var nextTickAdjustScroll = false;
var debouncedSave = utils.debounce(function() {
save();
2014-08-08 18:51:22 +00:00
if(lastSelectionStart == self.selectionStart && lastSelectionEnd == self.selectionEnd) {
nextTickAdjustScroll = false;
}
2014-05-21 22:54:50 +00:00
self.updateCursorCoordinates(nextTickAdjustScroll);
nextTickAdjustScroll = false;
});
2014-04-26 00:53:06 +00:00
return function(debounced, adjustScroll) {
if(debounced) {
2014-05-21 22:54:50 +00:00
nextTickAdjustScroll = nextTickAdjustScroll || adjustScroll;
return debouncedSave();
2014-04-26 00:53:06 +00:00
}
else {
2014-05-21 22:54:50 +00:00
save();
2014-04-26 00:53:06 +00:00
}
};
})();
2014-07-13 21:45:55 +00:00
this.getSelectedText = function() {
var min = Math.min(this.selectionStart, this.selectionEnd);
var max = Math.max(this.selectionStart, this.selectionEnd);
return textContent.substring(min, max);
};
this.getCoordinates = function(inputOffset, container, offsetInContainer) {
2014-04-26 00:53:06 +00:00
if(!container) {
2014-07-13 21:45:55 +00:00
var offset = this.findOffsets([inputOffset])[0];
2014-04-26 00:53:06 +00:00
container = offset.container;
2014-07-13 21:45:55 +00:00
offsetInContainer = offset.offsetInContainer;
2014-04-26 00:53:06 +00:00
}
var x = 0;
var y = 0;
if(container.textContent == '\n') {
y = container.parentNode.offsetTop + container.parentNode.offsetHeight / 2;
}
else {
var selectedChar = textContent[inputOffset];
var startOffset = {
container: container,
2014-07-13 21:45:55 +00:00
offsetInContainer: offsetInContainer,
offset: inputOffset
2014-04-26 00:53:06 +00:00
};
var endOffset = {
container: container,
2014-07-13 21:45:55 +00:00
offsetInContainer: offsetInContainer,
offset: inputOffset
2014-04-26 00:53:06 +00:00
};
if(inputOffset > 0 && (selectedChar === undefined || selectedChar == '\n')) {
if(startOffset.offset === 0) {
2014-07-13 21:45:55 +00:00
// Need to calculate offset-1
2014-04-26 00:53:06 +00:00
startOffset = inputOffset - 1;
}
else {
2014-07-13 21:45:55 +00:00
startOffset.offsetInContainer -= 1;
2014-04-26 00:53:06 +00:00
}
}
else {
if(endOffset.offset === container.textContent.length) {
2014-07-13 21:45:55 +00:00
// Need to calculate offset+1
2014-04-26 00:53:06 +00:00
endOffset = inputOffset + 1;
}
else {
2014-07-13 21:45:55 +00:00
endOffset.offsetInContainer += 1;
2014-04-26 00:53:06 +00:00
}
}
var selectionRange = this.createRange(startOffset, endOffset);
var selectionRect = selectionRange.getBoundingClientRect();
y = selectionRect.top + selectionRect.height / 2 - inputElt.getBoundingClientRect().top + inputElt.scrollTop;
selectionRange.detach();
}
return {
x: x,
y: y
};
};
this.getClosestWordOffset = function(offset) {
var offsetStart = 0;
var offsetEnd = 0;
var nextOffset = 0;
textContent.split(/\s/).some(function(word) {
if(word) {
offsetStart = nextOffset;
offsetEnd = nextOffset + word.length;
if(offsetEnd > offset) {
return true;
}
}
nextOffset += word.length + 1;
});
return {
start: offsetStart,
end: offsetEnd
};
};
}
var selectionMgr = new SelectionMgr();
editor.selectionMgr = selectionMgr;
2014-05-21 22:54:50 +00:00
$(document).on('selectionchange', '.editor-content', _.bind(selectionMgr.saveSelectionState, selectionMgr, true, false));
2014-04-26 00:53:06 +00:00
function adjustCursorPosition() {
if(inputElt === undefined) {
return;
}
selectionMgr.saveSelectionState(true, true);
}
editor.adjustCursorPosition = adjustCursorPosition;
var textContent;
function setValue(value) {
var startOffset = diffMatchPatch.diff_commonPrefix(textContent, value);
if(startOffset === textContent.length) {
startOffset--;
}
var endOffset = Math.min(
diffMatchPatch.diff_commonSuffix(textContent, value),
textContent.length - startOffset,
value.length - startOffset
);
var replacement = value.substring(startOffset, value.length - endOffset);
var range = selectionMgr.createRange(startOffset, textContent.length - endOffset);
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
2014-06-09 11:58:18 +00:00
range.detach();
2014-07-13 21:45:55 +00:00
return {
start: startOffset,
end: value.length - endOffset
};
2014-04-26 00:53:06 +00:00
}
editor.setValue = setValue;
2014-07-13 21:45:55 +00:00
function replace(selectionStart, selectionEnd, replacement) {
undoMgr.currentMode = undoMgr.currentMode || 'replace';
var range = selectionMgr.createRange(selectionStart, selectionEnd);
if('' + range == replacement) {
return;
}
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
range.detach();
var endOffset = selectionStart + replacement.length;
selectionMgr.setSelectionStartEnd(endOffset, endOffset);
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates(true);
}
editor.replace = replace;
function replaceAll(search, replacement) {
undoMgr.currentMode = undoMgr.currentMode || 'replace';
var value = textContent.replace(search, replacement);
if(value != textContent) {
var offset = editor.setValue(value);
selectionMgr.setSelectionStartEnd(offset.end, offset.end);
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates(true);
}
}
editor.replaceAll = replaceAll;
2014-04-26 00:53:06 +00:00
function replacePreviousText(text, replacement) {
var offset = selectionMgr.selectionStart;
if(offset !== selectionMgr.selectionEnd) {
return false;
}
var range = selectionMgr.createRange(offset - text.length, offset);
if('' + range != text) {
return false;
}
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
2014-06-09 11:58:18 +00:00
range.detach();
2014-04-26 00:53:06 +00:00
offset = offset - text.length + replacement.length;
selectionMgr.setSelectionStartEnd(offset, offset);
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
2014-07-13 21:45:55 +00:00
selectionMgr.updateCursorCoordinates(true);
2014-04-26 00:53:06 +00:00
return true;
}
editor.replacePreviousText = replacePreviousText;
function setValueNoWatch(value) {
setValue(value);
textContent = value;
}
editor.setValueNoWatch = setValueNoWatch;
function getValue() {
return textContent;
}
editor.getValue = getValue;
function focus() {
$contentElt.focus();
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
2014-04-26 00:53:06 +00:00
inputElt.scrollTop = scrollTop;
}
editor.focus = focus;
function UndoMgr() {
var undoStack = [];
var redoStack = [];
var lastTime;
var lastMode;
var currentState;
var selectionStartBefore;
var selectionEndBefore;
this.setCommandMode = function() {
this.currentMode = 'command';
};
this.setMode = function() {
}; // For compatibility with PageDown
this.onButtonStateChange = function() {
}; // To be overridden by PageDown
this.saveState = utils.debounce(function() {
redoStack = [];
var currentTime = Date.now();
2014-07-13 21:45:55 +00:00
if(this.currentMode == 'comment' ||
this.currentMode == 'replace' ||
lastMode == 'newlines' ||
this.currentMode != lastMode ||
currentTime - lastTime > 1000) {
2014-04-26 00:53:06 +00:00
undoStack.push(currentState);
// Limit the size of the stack
2014-05-21 22:54:50 +00:00
while(undoStack.length > 100) {
2014-04-26 00:53:06 +00:00
undoStack.shift();
}
}
else {
2014-05-21 22:54:50 +00:00
// Restore selectionBefore that has potentially been modified by saveSelectionState
2014-04-26 00:53:06 +00:00
selectionStartBefore = currentState.selectionStartBefore;
selectionEndBefore = currentState.selectionEndBefore;
}
currentState = {
selectionStartBefore: selectionStartBefore,
selectionEndBefore: selectionEndBefore,
selectionStartAfter: selectionMgr.selectionStart,
selectionEndAfter: selectionMgr.selectionEnd,
content: textContent,
discussionListJSON: fileDesc.discussionListJSON
};
lastTime = currentTime;
lastMode = this.currentMode;
this.currentMode = undefined;
this.onButtonStateChange();
}, this);
this.saveSelectionState = _.debounce(function() {
2014-07-13 21:45:55 +00:00
// Should happen just after saveState
2014-04-26 00:53:06 +00:00
if(this.currentMode === undefined) {
selectionStartBefore = selectionMgr.selectionStart;
selectionEndBefore = selectionMgr.selectionEnd;
}
2014-07-13 21:45:55 +00:00
}, 50);
2014-04-26 00:53:06 +00:00
this.canUndo = function() {
return undoStack.length;
};
this.canRedo = function() {
return redoStack.length;
};
function restoreState(state, selectionStart, selectionEnd) {
// Update editor
watcher.noWatch(function() {
if(textContent != state.content) {
setValueNoWatch(state.content);
fileDesc.content = state.content;
eventMgr.onContentChanged(fileDesc, state.content);
}
selectionMgr.setSelectionStartEnd(selectionStart, selectionEnd);
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
2014-07-13 21:45:55 +00:00
selectionMgr.updateCursorCoordinates(true);
2014-04-26 00:53:06 +00:00
var discussionListJSON = fileDesc.discussionListJSON;
if(discussionListJSON != state.discussionListJSON) {
var oldDiscussionList = fileDesc.discussionList;
fileDesc.discussionListJSON = state.discussionListJSON;
var newDiscussionList = fileDesc.discussionList;
var diff = jsonDiffPatch.diff(oldDiscussionList, newDiscussionList);
var commentsChanged = false;
_.each(diff, function(discussionDiff, discussionIndex) {
if(!_.isArray(discussionDiff)) {
commentsChanged = true;
}
else if(discussionDiff.length === 1) {
eventMgr.onDiscussionCreated(fileDesc, newDiscussionList[discussionIndex]);
}
else {
eventMgr.onDiscussionRemoved(fileDesc, oldDiscussionList[discussionIndex]);
}
});
commentsChanged && eventMgr.onCommentsChanged(fileDesc);
}
});
selectionStartBefore = selectionStart;
selectionEndBefore = selectionEnd;
currentState = state;
this.currentMode = undefined;
lastMode = undefined;
this.onButtonStateChange();
adjustCursorPosition();
}
this.undo = function() {
var state = undoStack.pop();
if(!state) {
return;
}
redoStack.push(currentState);
restoreState.call(this, state, currentState.selectionStartBefore, currentState.selectionEndBefore);
};
this.redo = function() {
var state = redoStack.pop();
if(!state) {
return;
}
undoStack.push(currentState);
restoreState.call(this, state, state.selectionStartAfter, state.selectionEndAfter);
};
this.init = function() {
var content = fileDesc.content;
undoStack = [];
redoStack = [];
lastTime = 0;
currentState = {
selectionStartAfter: fileDesc.selectionStart,
selectionEndAfter: fileDesc.selectionEnd,
content: content,
discussionListJSON: fileDesc.discussionListJSON
};
this.currentMode = undefined;
lastMode = undefined;
contentElt.textContent = content;
// Force this since the content could be the same
checkContentChange();
};
}
var undoMgr = new UndoMgr();
editor.undoMgr = undoMgr;
function onComment() {
if(watcher.isWatching === true) {
undoMgr.currentMode = undoMgr.currentMode || 'comment';
undoMgr.saveState();
}
}
eventMgr.addListener('onDiscussionCreated', onComment);
eventMgr.addListener('onDiscussionRemoved', onComment);
eventMgr.addListener('onCommentsChanged', onComment);
2014-07-24 12:20:56 +00:00
var triggerSpellCheck = _.debounce(function() {
2014-08-21 23:14:58 +00:00
var selection = window.getSelection();
2014-08-23 23:36:12 +00:00
if(!selectionMgr.hasFocus || isComposing || selectionMgr.selectionStart !== selectionMgr.selectionEnd || !selection.modify) {
2014-07-24 12:20:56 +00:00
return;
}
// Hack for Chrome to trigger the spell checker
if(selectionMgr.selectionStart) {
selection.modify("move", "backward", "character");
selection.modify("move", "forward", "character");
}
else {
selection.modify("move", "forward", "character");
selection.modify("move", "backward", "character");
}
}, 10);
2014-04-26 00:53:06 +00:00
function checkContentChange() {
var newTextContent = inputElt.textContent;
if(contentElt.lastChild === trailingLfNode && trailingLfNode.textContent.slice(-1) == '\n') {
newTextContent = newTextContent.slice(0, -1);
}
2014-04-29 20:23:22 +00:00
newTextContent = newTextContent.replace(/\r\n?/g, '\n'); // Mac/DOS to Unix
2014-04-26 00:53:06 +00:00
if(fileChanged === false) {
if(newTextContent == textContent) {
// User has removed the empty section
if(contentElt.children.length === 0) {
contentElt.innerHTML = '';
sectionList.forEach(function(section) {
contentElt.appendChild(section.elt);
});
addTrailingLfNode();
}
return;
}
undoMgr.currentMode = undoMgr.currentMode || 'typing';
var discussionList = _.values(fileDesc.discussionList);
fileDesc.newDiscussion && discussionList.push(fileDesc.newDiscussion);
var updateDiscussionList = adjustCommentOffsets(textContent, newTextContent, discussionList);
textContent = newTextContent;
if(updateDiscussionList === true) {
fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage
}
fileDesc.content = textContent;
selectionMgr.saveSelectionState();
eventMgr.onContentChanged(fileDesc, textContent);
updateDiscussionList && eventMgr.onCommentsChanged(fileDesc);
undoMgr.saveState();
2014-07-24 12:20:56 +00:00
triggerSpellCheck();
2014-04-26 00:53:06 +00:00
}
else {
textContent = newTextContent;
fileDesc.content = textContent;
selectionMgr.setSelectionStartEnd(fileDesc.editorStart, fileDesc.editorEnd);
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates();
undoMgr.saveSelectionState();
2014-04-26 00:53:06 +00:00
eventMgr.onFileOpen(fileDesc, textContent);
previewElt.scrollTop = fileDesc.previewScrollTop;
scrollTop = fileDesc.editorScrollTop;
inputElt.scrollTop = scrollTop;
fileChanged = false;
}
}
function adjustCommentOffsets(oldTextContent, newTextContent, discussionList) {
if(!discussionList.length) {
return;
}
var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent);
var changed = false;
var startOffset = 0;
changes.forEach(function(change) {
var changeType = change[0];
var changeText = change[1];
if(changeType === 0) {
startOffset += changeText.length;
return;
}
var endOffset = startOffset;
var diffOffset = changeText.length;
if(changeType === -1) {
endOffset += diffOffset;
diffOffset = -diffOffset;
}
discussionList.forEach(function(discussion) {
// selectionEnd
if(discussion.selectionEnd > endOffset) {
discussion.selectionEnd += diffOffset;
discussion.discussionIndex && (changed = true);
}
else if(discussion.selectionEnd > startOffset) {
discussion.selectionEnd = startOffset;
discussion.discussionIndex && (changed = true);
}
// selectionStart
if(discussion.selectionStart >= endOffset) {
discussion.selectionStart += diffOffset;
discussion.discussionIndex && (changed = true);
}
else if(discussion.selectionStart > startOffset) {
discussion.selectionStart = startOffset;
discussion.discussionIndex && (changed = true);
}
});
if(changeType === 1) {
startOffset += changeText.length;
}
});
return changed;
}
editor.adjustCommentOffsets = adjustCommentOffsets;
editor.init = function() {
inputElt = document.getElementById('wmd-input');
$inputElt = $(inputElt);
contentElt = inputElt.querySelector('.editor-content');
$contentElt = $(contentElt);
marginElt = inputElt.querySelector('.editor-margin');
$marginElt = $(marginElt);
previewElt = document.querySelector('.preview-container');
$inputElt.addClass(settings.editorFontClass);
watcher.startWatching();
$(inputElt).scroll(function() {
scrollTop = inputElt.scrollTop;
if(fileChanged === false) {
fileDesc.editorScrollTop = scrollTop;
}
});
$(previewElt).scroll(function() {
if(fileChanged === false) {
fileDesc.previewScrollTop = previewElt.scrollTop;
}
});
// See https://gist.github.com/shimondoodkin/1081133
if(/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)) {
var $editableFix = $('<input style="width:1px;height:1px;border:none;margin:0;padding:0;" tabIndex="-1">').appendTo('html');
$contentElt.blur(function() {
$editableFix[0].setSelectionRange(0, 0);
$editableFix.blur();
});
}
inputElt.focus = focus;
2014-08-10 23:44:54 +00:00
inputElt.adjustCursorPosition = adjustCursorPosition;
2014-04-26 00:53:06 +00:00
Object.defineProperty(inputElt, 'value', {
get: function() {
return textContent;
},
set: setValue
});
Object.defineProperty(inputElt, 'selectionStart', {
get: function() {
return Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
},
set: function(value) {
selectionMgr.setSelectionStartEnd(value);
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates();
2014-04-26 00:53:06 +00:00
},
enumerable: true,
configurable: true
});
Object.defineProperty(inputElt, 'selectionEnd', {
get: function() {
return Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
},
set: function(value) {
selectionMgr.setSelectionStartEnd(undefined, value);
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates();
2014-04-26 00:53:06 +00:00
},
enumerable: true,
configurable: true
});
var clearNewline = false;
$contentElt
.on('keydown', function(evt) {
if(
evt.which === 17 || // Ctrl
evt.which === 91 || // Cmd
evt.which === 18 || // Alt
evt.which === 16 // Shift
) {
return;
}
selectionMgr.saveSelectionState();
2014-05-21 22:54:50 +00:00
adjustCursorPosition();
2014-04-26 00:53:06 +00:00
var cmdOrCtrl = evt.metaKey || evt.ctrlKey;
switch(evt.which) {
case 9: // Tab
if(!cmdOrCtrl) {
action('indent', {
inverse: evt.shiftKey
});
evt.preventDefault();
}
break;
case 13:
action('newline');
evt.preventDefault();
break;
}
if(evt.which !== 13) {
clearNewline = false;
}
})
2014-05-04 17:30:14 +00:00
.on('compositionstart', function() {
2014-05-04 18:13:55 +00:00
isComposing++;
2014-05-04 17:30:14 +00:00
})
.on('compositionend', function() {
2014-05-04 18:13:55 +00:00
setTimeout(function() {
isComposing--;
}, 0);
2014-05-04 17:30:14 +00:00
})
2014-05-21 22:54:50 +00:00
.on('mouseup', _.bind(selectionMgr.saveSelectionState, selectionMgr, true, false))
2014-06-09 11:58:18 +00:00
.on('paste', function(evt) {
2014-04-26 00:53:06 +00:00
undoMgr.currentMode = 'paste';
2014-06-12 20:13:48 +00:00
evt.preventDefault();
var data = (evt.originalEvent || evt).clipboardData.getData('text/plain') || prompt('Paste something...');
data = escape(data);
2014-04-26 00:53:06 +00:00
adjustCursorPosition();
2014-06-12 20:13:48 +00:00
document.execCommand('insertHtml', false, data);
2014-04-26 00:53:06 +00:00
})
.on('cut', function() {
undoMgr.currentMode = 'cut';
adjustCursorPosition();
2014-07-13 21:45:55 +00:00
})
.on('focus', function() {
selectionMgr.hasFocus = true;
})
.on('blur', function() {
selectionMgr.hasFocus = false;
2014-04-26 00:53:06 +00:00
});
var action = function(action, options) {
var textContent = getValue();
2014-05-27 23:36:37 +00:00
var min = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
var max = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
2014-04-26 00:53:06 +00:00
var state = {
2014-05-27 23:36:37 +00:00
selectionStart: min,
selectionEnd: max,
before: textContent.slice(0, min),
after: textContent.slice(max),
selection: textContent.slice(min, max)
2014-04-26 00:53:06 +00:00
};
2014-05-27 23:36:37 +00:00
actions[action](state, options || {});
2014-04-26 00:53:06 +00:00
setValue(state.before + state.selection + state.after);
selectionMgr.setSelectionStartEnd(state.selectionStart, state.selectionEnd);
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
2014-04-26 00:53:06 +00:00
};
2014-05-25 18:38:26 +00:00
var indentRegex = /^ {0,3}>[ ]*|^[ \t]*(?:[*+\-]|(\d+)\.)[ \t]|^\s+/;
2014-04-26 00:53:06 +00:00
var actions = {
indent: function(state, options) {
function strSplice(str, i, remove, add) {
remove = +remove || 0;
add = add || '';
return str.slice(0, i) + add + str.slice(i + remove);
}
var lf = state.before.lastIndexOf('\n') + 1;
if(options.inverse) {
if(/\s/.test(state.before.charAt(lf))) {
state.before = strSplice(state.before, lf, 1);
state.selectionStart--;
state.selectionEnd--;
}
state.selection = state.selection.replace(/^[ \t]/gm, '');
} else {
2014-05-25 18:38:26 +00:00
var previousLine = state.before.slice(lf);
if(state.selection || previousLine.match(indentRegex)) {
state.before = strSplice(state.before, lf, 0, '\t');
state.selection = state.selection.replace(/\r?\n(?=[\s\S])/g, '\n\t');
state.selectionStart++;
state.selectionEnd++;
} else {
state.before += '\t';
state.selectionStart++;
state.selectionEnd++;
return;
}
2014-04-26 00:53:06 +00:00
}
state.selectionEnd = state.selectionStart + state.selection.length;
},
newline: function(state) {
var lf = state.before.lastIndexOf('\n') + 1;
if(clearNewline) {
state.before = state.before.substring(0, lf);
state.selection = '';
state.selectionStart = lf;
state.selectionEnd = lf;
clearNewline = false;
return;
}
clearNewline = false;
var previousLine = state.before.slice(lf);
2014-05-25 18:38:26 +00:00
var indentMatch = previousLine.match(indentRegex);
2014-04-26 00:53:06 +00:00
var indent = (indentMatch || [''])[0];
if(indentMatch && indentMatch[1]) {
var number = parseInt(indentMatch[1], 10);
indent = indent.replace(/\d+/, number + 1);
}
if(indent.length) {
clearNewline = true;
}
undoMgr.currentMode = 'newlines';
state.before += '\n' + indent;
state.selection = '';
state.selectionStart += indent.length + 1;
state.selectionEnd = state.selectionStart;
}
};
};
var sectionList = [];
var sectionsToRemove = [];
var modifiedSections = [];
var insertBeforeSection;
function updateSectionList(newSectionList) {
modifiedSections = [];
sectionsToRemove = [];
insertBeforeSection = undefined;
// Render everything if file changed
if(fileChanged === true) {
sectionsToRemove = sectionList;
sectionList = newSectionList;
modifiedSections = newSectionList;
return;
}
// Find modified section starting from top
var leftIndex = sectionList.length;
_.some(sectionList, function(section, index) {
var newSection = newSectionList[index];
if(index >= newSectionList.length ||
// Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter ||
// Check that section has not been detached or moved
section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent != newSection.textWithFrontMatter) {
leftIndex = index;
return true;
}
});
// Find modified section starting from bottom
var rightIndex = -sectionList.length;
_.some(sectionList.slice().reverse(), function(section, index) {
var newSection = newSectionList[newSectionList.length - index - 1];
if(index >= newSectionList.length ||
// Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter ||
// Check that section has not been detached or moved
section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent != newSection.textWithFrontMatter) {
rightIndex = -index;
return true;
}
});
if(leftIndex - rightIndex > sectionList.length) {
// Prevent overlap
rightIndex = leftIndex - sectionList.length;
}
// Create an array composed of left unmodified, modified, right
// unmodified sections
var leftSections = sectionList.slice(0, leftIndex);
modifiedSections = newSectionList.slice(leftIndex, newSectionList.length + rightIndex);
var rightSections = sectionList.slice(sectionList.length + rightIndex, sectionList.length);
insertBeforeSection = _.first(rightSections);
sectionsToRemove = sectionList.slice(leftIndex, sectionList.length + rightIndex);
sectionList = leftSections.concat(modifiedSections).concat(rightSections);
}
function highlightSections() {
var newSectionEltList = document.createDocumentFragment();
modifiedSections.forEach(function(section) {
highlight(section);
newSectionEltList.appendChild(section.elt);
});
watcher.noWatch(function() {
if(fileChanged === true) {
contentElt.innerHTML = '';
contentElt.appendChild(newSectionEltList);
}
else {
// Remove outdated sections
sectionsToRemove.forEach(function(section) {
// section can be already removed
section.elt.parentNode === contentElt && contentElt.removeChild(section.elt);
});
if(insertBeforeSection !== undefined) {
contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt);
}
else {
contentElt.appendChild(newSectionEltList);
}
// Remove unauthorized nodes (text nodes outside of sections or duplicated sections via copy/paste)
var childNode = contentElt.firstChild;
while(childNode) {
var nextNode = childNode.nextSibling;
if(!childNode.generated) {
contentElt.removeChild(childNode);
}
childNode = nextNode;
}
}
addTrailingLfNode();
2014-05-21 22:54:50 +00:00
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates();
2014-04-26 00:53:06 +00:00
});
}
function addTrailingLfNode() {
trailingLfNode = crel('span', {
class: 'token lf'
});
trailingLfNode.textContent = '\n';
contentElt.appendChild(trailingLfNode);
}
var escape = (function() {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
"\u00a0": ' '
};
return function(str) {
return str.replace(/[&<\u00a0]/g, function(s) {
return entityMap[s];
});
};
})();
function highlight(section) {
var text = escape(section.text);
if(!window.viewerMode) {
text = Prism.highlight(text, Prism.languages.md);
}
var frontMatter = section.textWithFrontMatter.substring(0, section.textWithFrontMatter.length - section.text.length);
if(frontMatter.length) {
// Front matter highlighting
frontMatter = escape(frontMatter);
frontMatter = frontMatter.replace(/\n/g, '<span class="token lf">\n</span>');
text = '<span class="token md">' + frontMatter + '</span>' + text;
}
var sectionElt = crel('span', {
id: 'wmd-input-section-' + section.id,
class: 'wmd-input-section'
});
sectionElt.generated = true;
sectionElt.innerHTML = text;
section.elt = sectionElt;
}
eventMgr.onEditorCreated(editor);
return editor;
2014-03-19 00:33:57 +00:00
});