You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
264 lines
9.1 KiB
JavaScript
264 lines
9.1 KiB
JavaScript
define(function () {
|
|
|
|
/**
|
|
* Licensed under the standard MIT license:
|
|
*
|
|
* Copyright 2011 Joseph Gentle.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
* See: https://github.com/share/ShareJS/blob/master/LICENSE
|
|
*/
|
|
|
|
/* This contains the textarea binding for ShareJS. This binding is really
|
|
* simple, and a bit slow on big documents (Its O(N). However, it requires no
|
|
* changes to the DOM and no heavy libraries like ace. It works for any kind of
|
|
* text input field.
|
|
*
|
|
* You probably want to use this binding for small fields on forms and such.
|
|
* For code editors or rich text editors or whatever, I recommend something
|
|
* heavier.
|
|
*/
|
|
|
|
|
|
/* applyChange creates the edits to convert oldval -> newval.
|
|
*
|
|
* This function should be called every time the text element is changed.
|
|
* Because changes are always localised, the diffing is quite easy. We simply
|
|
* scan in from the start and scan in from the end to isolate the edited range,
|
|
* then delete everything that was removed & add everything that was added.
|
|
* This wouldn't work for complex changes, but this function should be called
|
|
* on keystroke - so the edits will mostly just be single character changes.
|
|
* Sometimes they'll paste text over other text, but even then the diff
|
|
* generated by this algorithm is correct.
|
|
*
|
|
* This algorithm is O(N). I suspect you could speed it up somehow using regular expressions.
|
|
*/
|
|
var applyChange = function(ctx, oldval, newval) {
|
|
// Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
|
|
if (oldval === newval) return;
|
|
|
|
var commonStart = 0;
|
|
while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
|
|
commonStart++;
|
|
}
|
|
|
|
var commonEnd = 0;
|
|
while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
|
|
commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
|
|
commonEnd++;
|
|
}
|
|
|
|
if (oldval.length !== commonStart + commonEnd) {
|
|
ctx.remove(commonStart, oldval.length - commonStart - commonEnd);
|
|
}
|
|
if (newval.length !== commonStart + commonEnd) {
|
|
ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Fix issues with textarea content which is different per-browser.
|
|
*/
|
|
var cannonicalize = function (content) {
|
|
|
|
return content.replace(/\r\n/g, '\n');
|
|
};
|
|
|
|
// Attach a textarea to a document's editing context.
|
|
//
|
|
// The context is optional, and will be created from the document if its not
|
|
// specified.
|
|
var attachTextarea = function(elem, ctx, cmElem) {
|
|
|
|
// initial state will always fail the !== check in genop.
|
|
var content = {};
|
|
|
|
// Replace the content of the text area with newText, and transform the
|
|
// current cursor by the specified function.
|
|
var replaceText = function(newText, transformCursor, transformCursorCM) {
|
|
var newCursor;
|
|
var newSelection;
|
|
|
|
if(cmElem) {
|
|
// Fix cursor here?
|
|
var cursorCM = cmElem.getCursor();
|
|
var cursorCMStart = cmElem.getCursor('from');
|
|
var cursorCMEnd = cmElem.getCursor('to');
|
|
if(cursorCMStart !== cursorCMEnd) {
|
|
newSelection = [transformCursorCM(elem.value, cursorCMStart), transformCursorCM(elem.value, cursorCMEnd)];
|
|
}
|
|
else {
|
|
newCursor = transformCursorCM(elem.value, cursorCM);
|
|
}
|
|
}
|
|
|
|
if (transformCursor && !cmElem) {
|
|
newSelection = [transformCursor(elem.selectionStart), transformCursor(elem.selectionEnd)];
|
|
}
|
|
|
|
// Fixate the window's scroll while we set the element's value. Otherwise
|
|
// the browser scrolls to the element.
|
|
var scrollTop = elem.scrollTop;
|
|
elem.value = newText;
|
|
if(cmElem) {
|
|
// Fix cursor here?
|
|
cmElem.setValue(newText);
|
|
if(newCursor) {
|
|
cmElem.setCursor(newCursor);
|
|
}
|
|
else {
|
|
cmElem.setSelection(newSelection[0], newSelection[1]);
|
|
}
|
|
}
|
|
content = elem.value; // Not done on one line so the browser can do newline conversion.
|
|
|
|
if(!cmElem) {
|
|
if (elem.scrollTop !== scrollTop) elem.scrollTop = scrollTop;
|
|
|
|
// Setting the selection moves the cursor. We'll just have to let your
|
|
// cursor drift if the element isn't active, though usually users don't
|
|
// care.
|
|
if (newSelection && window.document.activeElement === elem) {
|
|
elem.selectionStart = newSelection[0];
|
|
elem.selectionEnd = newSelection[1];
|
|
}
|
|
}
|
|
};
|
|
|
|
//replaceText(ctx.get());
|
|
|
|
|
|
// *** remote -> local changes
|
|
|
|
ctx.onRemove(function(pos, length) {
|
|
var transformCursor = function(cursor) {
|
|
// If the cursor is inside the deleted region, we only want to move back to the start
|
|
// of the region. Hence the Math.min.
|
|
return pos < cursor ? cursor - Math.min(length, cursor - pos) : cursor;
|
|
};
|
|
var transformCursorCM = function(text, cursor) {
|
|
var newCursor = cursor;
|
|
var textLines = text.substr(0, pos).split("\n");
|
|
var removedTextLineNumber = textLines.length-1;
|
|
var removedTextColumnIndex = textLines[textLines.length-1].length;
|
|
var removedLines = text.substr(pos, length).split("\n").length - 1;
|
|
if(cursor.line > (removedTextLineNumber + removedLines)) {
|
|
newCursor.line -= removedLines;
|
|
}
|
|
else if(removedLines > 0 && cursor.line === (removedTextLineNumber+removedLines)) {
|
|
var lastLineCharsRemoved = text.substr(pos, length).split("\n")[removedLines].length;
|
|
if(cursor.ch >= lastLineCharsRemoved) {
|
|
newCursor.line = removedTextLineNumber;
|
|
newCursor.ch = removedTextColumnIndex + cursor.ch - lastLineCharsRemoved;
|
|
}
|
|
else {
|
|
newCursor.line -= removedLines;
|
|
newCursor.ch = removedTextColumnIndex;
|
|
}
|
|
}
|
|
else if(cursor.line === removedTextLineNumber && cursor.ch > removedTextLineNumber) {
|
|
newCursor.ch -= Math.min(length, cursor.ch-removedTextLineNumber);
|
|
}
|
|
return newCursor;
|
|
};
|
|
replaceText(ctx.getUserDoc(), transformCursor, transformCursorCM);
|
|
});
|
|
|
|
ctx.onInsert(function(pos, text) {
|
|
var transformCursor = function(cursor) {
|
|
return pos < cursor ? cursor + text.length : cursor;
|
|
};
|
|
var transformCursorCM = function(oldtext, cursor) {
|
|
var newCursor = cursor;
|
|
var textLines = oldtext.substr(0, pos).split("\n");
|
|
var addedTextLineNumber = textLines.length-1;
|
|
var addedTextColumnIndex = textLines[textLines.length-1].length;
|
|
var addedLines = text.split("\n").length - 1;
|
|
if(cursor.line > addedTextLineNumber) {
|
|
newCursor.line += addedLines;
|
|
}
|
|
else if(cursor.line === addedTextLineNumber && cursor.ch > addedTextColumnIndex) {
|
|
newCursor.line += addedLines;
|
|
if(addedLines > 0) {
|
|
newCursor.ch = newCursor.ch - addedTextColumnIndex + text.split("\n")[addedLines].length;
|
|
}
|
|
else {
|
|
newCursor.ch += text.split("\n")[addedLines].length;
|
|
}
|
|
}
|
|
return newCursor;
|
|
};
|
|
replaceText(ctx.getUserDoc(), transformCursor, transformCursorCM);
|
|
});
|
|
|
|
|
|
// *** local -> remote changes
|
|
|
|
// This function generates operations from the changed content in the textarea.
|
|
var genOp = function() {
|
|
// In a timeout so the browser has time to propogate the event's changes to the DOM.
|
|
setTimeout(function() {
|
|
var val = elem.value;
|
|
if (val !== content) {
|
|
applyChange(ctx, ctx.getUserDoc(), cannonicalize(val));
|
|
}
|
|
}, 0);
|
|
};
|
|
|
|
var eventNames = ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'];
|
|
for (var i = 0; i < eventNames.length; i++) {
|
|
var e = eventNames[i];
|
|
if (elem.addEventListener) {
|
|
elem.addEventListener(e, genOp, false);
|
|
} else {
|
|
elem.attachEvent('on' + e, genOp);
|
|
}
|
|
}
|
|
window.setTimeout(function() {
|
|
if(cmElem) {
|
|
var elem2 = cmElem;
|
|
elem2.on('change', function() {
|
|
elem2.save();
|
|
genOp();
|
|
});
|
|
}
|
|
else {
|
|
console.log('CM inexistant');
|
|
}
|
|
}, 500);
|
|
|
|
|
|
ctx.detach = function() {
|
|
for (var i = 0; i < eventNames.length; i++) {
|
|
var e = eventNames[i];
|
|
if (elem.removeEventListener) {
|
|
elem.removeEventListener(e, genOp, false);
|
|
} else {
|
|
elem.detachEvent('on' + e, genOp);
|
|
}
|
|
}
|
|
};
|
|
|
|
return ctx;
|
|
};
|
|
|
|
return { attach: attachTextarea };
|
|
});
|