509 lines
18 KiB
JavaScript
509 lines
18 KiB
JavaScript
define([
|
|
'/common/cursor-treesome.js',
|
|
'/bower_components/rangy/rangy-core.min.js'
|
|
], function (Tree, Rangy) {
|
|
var verbose = function (x) { if (window.verboseMode) { console.log(x); } };
|
|
|
|
/* accepts the document used by the editor */
|
|
var Cursor = function (inner) {
|
|
var cursor = {};
|
|
|
|
var getTextNodeValue = function (el) {
|
|
if (!el.data) { return; }
|
|
// We want to transform html entities into their code (non-breaking spaces into $ )
|
|
var div = document.createElement('div');
|
|
div.innerText = el.data;
|
|
return div.innerHTML;
|
|
};
|
|
|
|
// Store the cursor position as an offset from the beginning of the text HTML content
|
|
var offsetRange = cursor.offsetRange = {
|
|
start: 0,
|
|
end: 0
|
|
};
|
|
|
|
// Get the length of the opening tag of an node (<body class="cp"> ==> 17)
|
|
var getOpeningTagLength = function (node) {
|
|
if (node.nodeType === node.TEXT_NODE) { return 0; }
|
|
var html = node.outerHTML;
|
|
var tagRegex = /^(<\s*[a-zA-Z-]*[^>]*>)(.+)/;
|
|
var match = tagRegex.exec(html);
|
|
var res = match && match.length > 1 ? match[1].length : 0;
|
|
return res;
|
|
};
|
|
|
|
// Get the offset recursively. We start with <body> and continue following the
|
|
// path to the range
|
|
var offsetInNode = function (element, offset, path, range) {
|
|
if (path.length === 0) {
|
|
offset += getOpeningTagLength(range.el);
|
|
if (range.el.nodeType === range.el.TEXT_NODE) {
|
|
var div = document.createElement('div');
|
|
div.innerText = range.el.data.slice(0, range.offset);
|
|
return offset + div.innerHTML.length;
|
|
}
|
|
return offset + range.offset;
|
|
}
|
|
offset += getOpeningTagLength(element);
|
|
for (var i = 0; i < element.childNodes.length; i++) {
|
|
if (element.childNodes[i] === path[0]) {
|
|
return offsetInNode(path.shift(), offset, path, range);
|
|
}
|
|
// It is not yet our path, add the length of the text node or tag's outerHTML
|
|
offset += (getTextNodeValue(element.childNodes[i]) || element.childNodes[i].outerHTML).length;
|
|
}
|
|
};
|
|
|
|
// Get the cursor position as a range and transform it into
|
|
// an offset from the beginning of the outer HTML
|
|
var getOffsetFromRange = function (element) {
|
|
var doc = element.ownerDocument || element.document;
|
|
var win = doc.defaultView || doc.parentWindow;
|
|
var o = {
|
|
start: 0,
|
|
end: 0
|
|
};
|
|
if (typeof win.getSelection !== "undefined") {
|
|
var sel = win.getSelection();
|
|
if (sel.rangeCount > 0) {
|
|
var range = win.getSelection().getRangeAt(0);
|
|
// Do it for both start and end
|
|
['start', 'end'].forEach(function (t) {
|
|
var inNode = {
|
|
el: range[t + 'Container'],
|
|
offset: range[t + 'Offset']
|
|
};
|
|
while (inNode.el.nodeType !== Node.TEXT_NODE && inNode.el.childNodes.length > inNode.offset) {
|
|
inNode.el = inNode.el.childNodes[inNode.offset];
|
|
inNode.offset = 0;
|
|
}
|
|
var current = inNode.el;
|
|
var path = [];
|
|
while (current !== element) {
|
|
path.unshift(current);
|
|
current = current.parentNode;
|
|
}
|
|
|
|
if (current === element) { // Should always be the case
|
|
o[t] = offsetInNode(current, 0, path, inNode);
|
|
} else {
|
|
console.error('???');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return o;
|
|
};
|
|
|
|
// Update the value of the offset
|
|
// This should be called before applying changes to the document
|
|
cursor.offsetUpdate = function () {
|
|
try {
|
|
var range = getOffsetFromRange(inner);
|
|
offsetRange.start = range.start;
|
|
offsetRange.end = range.end;
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
// Transform the offset value using the operations from the diff
|
|
// between the old and the new states of the document.
|
|
var offsetTransformRange = function (offset, ops) {
|
|
var transformCursor = function (cursor, op) {
|
|
if (!op) { return cursor; }
|
|
|
|
var pos = op.offset;
|
|
var remove = op.toRemove;
|
|
var insert = op.toInsert.length;
|
|
if (typeof cursor === 'undefined') { return; }
|
|
if (typeof remove === 'number' && pos < cursor) {
|
|
cursor -= Math.min(remove, cursor - pos);
|
|
}
|
|
if (typeof insert === 'number' && pos < cursor) {
|
|
cursor += insert;
|
|
}
|
|
return cursor;
|
|
};
|
|
var c = offset;
|
|
if (Array.isArray(ops)) {
|
|
for (var i = ops.length - 1; i >= 0; i--) {
|
|
c = transformCursor(c, ops[i]);
|
|
}
|
|
offset = c;
|
|
}
|
|
return offset;
|
|
};
|
|
|
|
// Get the range starting from <body> and the offset value.
|
|
// We substract length of HTML content to the offset until we reach a text node or 0.
|
|
// If we reach a text node, it means we're in the final possible child and the
|
|
// current valu of the offset is the range one.
|
|
// If we reach 0 or a negative value, it means the range in is the current tag
|
|
// and we should use offset 0.
|
|
var getFinalRange = function (el, offset) {
|
|
if (el.nodeType === el.TEXT_NODE) {
|
|
// This should be the final text node
|
|
var txt = document.createElement("textarea");
|
|
txt.appendChild(el.cloneNode());
|
|
txt.innerHTML = txt.innerHTML.slice(0, offset);
|
|
return {
|
|
el: el,
|
|
offset: txt.value.length
|
|
};
|
|
}
|
|
if (el.tagName === 'BR') {
|
|
// If the range is in a <br>, we have a brFix that will make it better later
|
|
return {
|
|
el: el,
|
|
offset: 0
|
|
};
|
|
}
|
|
|
|
// Remove the current tag opening length
|
|
offset = offset - getOpeningTagLength(el);
|
|
|
|
if (offset <= 0) {
|
|
// Return the current node...
|
|
return {
|
|
el: el,
|
|
offset: 0
|
|
};
|
|
}
|
|
|
|
// For each child, if they length is greater than the current offset, they are
|
|
// containing the range element we're looking for.
|
|
// Otherwise, our range element is in a later sibling and we can just substract
|
|
// their length.
|
|
var newOffset = offset;
|
|
for (var i = 0; i < el.childNodes.length; i++) {
|
|
try {
|
|
newOffset -= (getTextNodeValue(el.childNodes[i]) || el.childNodes[i].outerHTML).length;
|
|
} catch (e) {
|
|
console.log(el);
|
|
console.log(el.childNodes[i]);
|
|
}
|
|
if (newOffset <= 0) {
|
|
return getFinalRange(el.childNodes[i], offset);
|
|
}
|
|
offset = newOffset;
|
|
}
|
|
|
|
// New offset ends up in the closing tag
|
|
// ==> return the last child...
|
|
if (el.childNodes.length) {
|
|
return getFinalRange(el.childNodes[el.childNodes.length - 1], offset);
|
|
} else {
|
|
return {
|
|
el: el,
|
|
offset: 0
|
|
};
|
|
}
|
|
};
|
|
|
|
// Transform an offset into a range that we can use to restore the cursor
|
|
var getRangeFromOffset = function (element) {
|
|
var range = {
|
|
start: {
|
|
el: null,
|
|
offset: 0
|
|
},
|
|
end: {
|
|
el: null,
|
|
offset: 0
|
|
}
|
|
};
|
|
|
|
['start', 'end'].forEach(function (t) {
|
|
var offset = offsetRange[t];
|
|
var res = getFinalRange(element, offset);
|
|
range[t].el = res.el;
|
|
range[t].offset = res.offset;
|
|
});
|
|
|
|
|
|
return range;
|
|
};
|
|
|
|
cursor.getNewOffset = function (ops) {
|
|
return {
|
|
selectionStart: offsetTransformRange(offsetRange.start, ops),
|
|
selectionEnd: offsetTransformRange(offsetRange.end, ops)
|
|
};
|
|
};
|
|
cursor.getNewRange = function (data, ops) {
|
|
offsetRange.start = offsetTransformRange(data.start, ops);
|
|
offsetRange.end = offsetTransformRange(data.end, ops);
|
|
var range = getRangeFromOffset(inner);
|
|
return range;
|
|
};
|
|
|
|
// Restore the cursor position after applying the changes.
|
|
cursor.restoreOffset = function (ops) {
|
|
try {
|
|
offsetRange.start = offsetTransformRange(offsetRange.start, ops);
|
|
offsetRange.end = offsetTransformRange(offsetRange.end, ops);
|
|
var range = getRangeFromOffset(inner);
|
|
var sel = cursor.makeSelection();
|
|
var r = cursor.makeRange();
|
|
cursor.fixStart(range.start.el, range.start.offset);
|
|
cursor.fixEnd(range.end.el, range.end.offset);
|
|
cursor.fixSelection(sel, r);
|
|
cursor.brFix();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
// there ought to only be one cursor at a time, so let's just
|
|
// keep it internally
|
|
var Range = cursor.Range = {
|
|
start: {
|
|
el: null,
|
|
offset: 0
|
|
},
|
|
end: {
|
|
el: null,
|
|
offset:0
|
|
}
|
|
};
|
|
|
|
/* cursor.update takes notes about wherever the cursor was last seen
|
|
in the event of a cursor loss, the information produced by side
|
|
effects of this function should be used to recover the cursor
|
|
|
|
returns an error string if no range is found
|
|
*/
|
|
cursor.update = function (sel, root) {
|
|
verbose("cursor.update");
|
|
root = root || inner;
|
|
sel = sel || Rangy.getSelection(root);
|
|
|
|
// if the root element has no focus, there will be no range
|
|
if (!sel.rangeCount) { return; }
|
|
var range = sel.getRangeAt(0);
|
|
|
|
// Big R Range is caught in closure, and maintains persistent state
|
|
['start', 'end'].forEach(function (pos) {
|
|
Range[pos].el = range[pos+'Container'];
|
|
Range[pos].offset = range[pos+'Offset'];
|
|
});
|
|
};
|
|
|
|
cursor.exists = function () {
|
|
return (Range.start.el?1:0) | (Range.end.el?2:0);
|
|
};
|
|
|
|
/*
|
|
0 if neither
|
|
1 if start
|
|
2 if end
|
|
3 if start and end
|
|
*/
|
|
cursor.inNode = function (el) {
|
|
var state = ['start', 'end'].map(function (pos, i) {
|
|
return Tree.contains(el, Range[pos].el)? i +1: 0;
|
|
});
|
|
return state[0] | state[1];
|
|
};
|
|
|
|
var confineOffsetToElement = cursor.confineOffsetToElement = function (el, offset) {
|
|
return Math.max(Math.min(offset, el.textContent.length), 0);
|
|
};
|
|
|
|
var makeSelection = cursor.makeSelection = function () {
|
|
var sel = Rangy.getSelection(inner);
|
|
return sel;
|
|
};
|
|
|
|
var makeRange = cursor.makeRange = function () {
|
|
return Rangy.createRange();
|
|
};
|
|
|
|
var fixStart = cursor.fixStart = function (el, offset) {
|
|
Range.start.el = el;
|
|
Range.start.offset = confineOffsetToElement(el,
|
|
(typeof offset !== 'undefined') ? offset : Range.start.offset);
|
|
};
|
|
|
|
var fixEnd = cursor.fixEnd = function (el, offset) {
|
|
Range.end.el = el;
|
|
Range.end.offset = confineOffsetToElement(el,
|
|
(typeof offset !== 'undefined') ? offset : Range.end.offset);
|
|
};
|
|
|
|
var fixSelection = cursor.fixSelection = function (sel, range) {
|
|
try {
|
|
if (Tree.contains(Range.start.el, inner) && Tree.contains(Range.end.el, inner)) {
|
|
var order = Tree.orderOfNodes(Range.start.el, Range.end.el, inner);
|
|
var backward;
|
|
|
|
// this could all be one line but nobody would be able to read it
|
|
if (order === -1) {
|
|
// definitely backward
|
|
backward = true;
|
|
} else if (order === 0) {
|
|
// might be backward, check offsets to know for sure
|
|
backward = (Range.start.offset > Range.end.offset);
|
|
} else {
|
|
// definitely not backward
|
|
backward = false;
|
|
}
|
|
|
|
if (backward) {
|
|
range.setStart(Range.end.el, Range.end.offset);
|
|
range.setEnd(Range.start.el, Range.start.offset);
|
|
} else {
|
|
range.setStart(Range.start.el, Range.start.offset);
|
|
range.setEnd(Range.end.el, Range.end.offset);
|
|
}
|
|
|
|
// actually set the cursor to the new range
|
|
sel.setSingleRange(range);
|
|
} else {
|
|
var errText = "[cursor.fixSelection] At least one of the " +
|
|
"cursor nodes did not exist, could not fix selection";
|
|
//console.error(errText);
|
|
return errText;
|
|
}
|
|
} catch (e) { console.error(e); }
|
|
};
|
|
|
|
cursor.pushDelta = function (oldVal, newVal) {
|
|
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++;
|
|
}
|
|
|
|
var insert = false, remove = false;
|
|
if (oldVal.length !== commonStart + commonEnd) {
|
|
// there was a removal?
|
|
remove = true;
|
|
}
|
|
if (newVal.length !== commonStart + commonEnd) {
|
|
// there was an insertion?
|
|
insert = true;
|
|
}
|
|
|
|
var lengthDelta = newVal.length - oldVal.length;
|
|
|
|
return {
|
|
commonStart: commonStart,
|
|
commonEnd: commonEnd,
|
|
delta: lengthDelta,
|
|
insert: insert,
|
|
remove: remove
|
|
};
|
|
};
|
|
|
|
cursor.transformRange = function (cursorRange, ops) {
|
|
var transformCursor = function (cursor, op) {
|
|
if (!op) { return cursor; }
|
|
|
|
var pos = op.offset;
|
|
var remove = op.toRemove;
|
|
var insert = op.toInsert.length;
|
|
if (typeof cursor === 'undefined') { return; }
|
|
if (typeof remove === 'number' && pos < cursor) {
|
|
cursor -= Math.min(remove, cursor - pos);
|
|
}
|
|
if (typeof insert === 'number' && pos < cursor) {
|
|
cursor += insert;
|
|
}
|
|
return cursor;
|
|
};
|
|
var c = cursorRange.offset;
|
|
if (Array.isArray(ops)) {
|
|
for (var i = ops.length - 1; i >= 0; i--) {
|
|
c = transformCursor(c, ops[i]);
|
|
}
|
|
cursorRange.offset = c;
|
|
}
|
|
};
|
|
|
|
cursor.brFix = function () {
|
|
cursor.update();
|
|
var start = Range.start;
|
|
var end = Range.end;
|
|
if (!start.el) { return; }
|
|
|
|
if (start.el === end.el && start.offset === end.offset) {
|
|
if (start.el.tagName === 'BR') {
|
|
var br = start.el;
|
|
|
|
var P = (Tree.indexOfNode(br) === 0 ?
|
|
br.parentNode: br.previousSibling);
|
|
|
|
[cursor.fixStart, cursor.fixEnd].forEach(function (f) {
|
|
f(P, 0);
|
|
});
|
|
|
|
cursor.fixSelection(cursor.makeSelection(), cursor.makeRange());
|
|
}
|
|
}
|
|
};
|
|
|
|
cursor.lastTextNode = function () {
|
|
var lastEl = Tree.rightmostNode(inner);
|
|
if (lastEl && lastEl.nodeType === 3) { return lastEl; }
|
|
|
|
var firstEl = Tree.leftmostNode(inner);
|
|
|
|
while (lastEl !== firstEl) {
|
|
lastEl = Tree.previousNode(lastEl, inner);
|
|
if (lastEl && lastEl.nodeType === 3) { return lastEl; }
|
|
}
|
|
|
|
return lastEl;
|
|
};
|
|
|
|
cursor.firstTextNode = function () {
|
|
var firstEl = Tree.leftmostNode(inner);
|
|
if (firstEl && firstEl.nodeType === 3) { return firstEl; }
|
|
|
|
var lastEl = Tree.rightmostNode(inner);
|
|
|
|
while (firstEl !== lastEl) {
|
|
firstEl = Tree.nextNode(firstEl, inner);
|
|
if (firstEl && firstEl.nodeType === 3) { return firstEl; }
|
|
}
|
|
return firstEl;
|
|
};
|
|
|
|
cursor.setToStart = function () {
|
|
var el = cursor.firstTextNode();
|
|
if (!el) { return; }
|
|
fixStart(el, 0);
|
|
fixEnd(el, 0);
|
|
fixSelection(makeSelection(), makeRange());
|
|
return el;
|
|
};
|
|
|
|
cursor.setToEnd = function () {
|
|
var el = cursor.lastTextNode();
|
|
if (!el) { return; }
|
|
|
|
var offset = el.textContent.length;
|
|
|
|
fixStart(el, offset);
|
|
fixEnd(el, offset);
|
|
fixSelection(makeSelection(), makeRange());
|
|
return el;
|
|
};
|
|
|
|
return cursor;
|
|
};
|
|
|
|
Cursor.Tree = Tree;
|
|
|
|
return Cursor;
|
|
});
|