Stackedit/src/services/cledit/cleditSelectionMgr.js
2018-02-16 08:38:33 +00:00

399 lines
13 KiB
JavaScript

import cledit from './cleditCore';
function SelectionMgr(editor) {
const debounce = cledit.Utils.debounce;
const contentElt = editor.$contentElt;
const scrollElt = editor.$scrollElt;
cledit.Utils.createEventHooks(this);
const self = this;
let lastSelectionStart = 0;
let lastSelectionEnd = 0;
this.selectionStart = 0;
this.selectionEnd = 0;
this.cursorCoordinates = {};
this.findContainer = (offset) => {
const result = cledit.Utils.findContainer(contentElt, offset);
if (result.container.nodeValue === '\n') {
const hdLfElt = result.container.parentNode;
if (hdLfElt.className === 'hd-lf' && hdLfElt.previousSibling && hdLfElt.previousSibling.tagName === 'BR') {
result.container = hdLfElt.parentNode;
result.offsetInContainer = Array.prototype.indexOf.call(
result.container.childNodes,
result.offsetInContainer === 0 ? hdLfElt.previousSibling : hdLfElt,
);
}
}
return result;
};
this.createRange = function (start, end) {
const range = document.createRange();
const startContainer = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start);
let endContainer = startContainer;
if (start !== end) {
endContainer = isNaN(end) ? end : this.findContainer(end < 0 ? 0 : end);
}
range.setStart(startContainer.container, startContainer.offsetInContainer);
range.setEnd(endContainer.container, endContainer.offsetInContainer);
return range;
};
let adjustScroll;
const debouncedUpdateCursorCoordinates = debounce(() => {
const coordinates = this.getCoordinates(
this.selectionEnd, this.selectionEndContainer, this.selectionEndOffset);
if (this.cursorCoordinates.top !== coordinates.top ||
this.cursorCoordinates.height !== coordinates.height ||
this.cursorCoordinates.left !== coordinates.left
) {
this.cursorCoordinates = coordinates;
this.$trigger('cursorCoordinatesChanged', coordinates);
}
if (adjustScroll) {
let scrollEltHeight = scrollElt.clientHeight;
if (typeof adjustScroll === 'number') {
scrollEltHeight -= adjustScroll;
}
const adjustment = (scrollEltHeight / 2) * editor.options.getCursorFocusRatio();
let cursorTop = this.cursorCoordinates.top + (this.cursorCoordinates.height / 2);
// Adjust cursorTop with contentElt position relative to scrollElt
cursorTop += (contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top)
+ scrollElt.scrollTop;
const minScrollTop = cursorTop - adjustment;
const maxScrollTop = (cursorTop + adjustment) - scrollEltHeight;
if (scrollElt.scrollTop > minScrollTop) {
scrollElt.scrollTop = minScrollTop;
} else if (scrollElt.scrollTop < maxScrollTop) {
scrollElt.scrollTop = maxScrollTop;
}
}
adjustScroll = false;
});
this.updateCursorCoordinates = function (adjustScrollParam) {
adjustScroll = adjustScroll || adjustScrollParam;
debouncedUpdateCursorCoordinates();
};
let oldSelectionRange;
function checkSelection(selectionRange) {
if (!oldSelectionRange ||
oldSelectionRange.startContainer !== selectionRange.startContainer ||
oldSelectionRange.startOffset !== selectionRange.startOffset ||
oldSelectionRange.endContainer !== selectionRange.endContainer ||
oldSelectionRange.endOffset !== selectionRange.endOffset
) {
oldSelectionRange = selectionRange;
self.$trigger('selectionChanged', self.selectionStart, self.selectionEnd, selectionRange);
return true;
}
return false;
}
this.hasFocus = () => contentElt === document.activeElement;
this.restoreSelection = () => {
const min = Math.min(this.selectionStart, this.selectionEnd);
const max = Math.max(this.selectionStart, this.selectionEnd);
const selectionRange = this.createRange(min, max);
if (!document.contains(selectionRange.commonAncestorContainer)) {
return null;
}
const selection = window.getSelection();
selection.removeAllRanges();
const isBackward = this.selectionStart > this.selectionEnd;
if (isBackward && selection.extend) {
const beginRange = selectionRange.cloneRange();
beginRange.collapse(false);
selection.addRange(beginRange);
selection.extend(selectionRange.startContainer, selectionRange.startOffset);
} else {
selection.addRange(selectionRange);
}
checkSelection(selectionRange);
return selectionRange;
};
const saveLastSelection = debounce(() => {
lastSelectionStart = self.selectionStart;
lastSelectionEnd = self.selectionEnd;
}, 50);
function setSelection(start = self.selectionStart, end = this.selectionEnd) {
self.selectionStart = start < 0 ? 0 : start;
self.selectionEnd = end < 0 ? 0 : end;
saveLastSelection();
}
this.setSelectionStartEnd = (start, end) => {
setSelection(start, end);
return this.hasFocus() && this.restoreSelection();
};
this.saveSelectionState = (() => {
// Credit: https://github.com/timdown/rangy
function arrayContains(arr, val) {
let i = arr.length;
while (i) {
i -= 1;
if (arr[i] === val) {
return true;
}
}
return false;
}
function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
let p;
let n = selfIsAncestor ? node : node.parentNode;
while (n) {
p = n.parentNode;
if (p === ancestor) {
return n;
}
n = p;
}
return null;
}
function getNodeIndex(node) {
let i = 0;
let previousSibling = node.previousSibling;
while (previousSibling) {
i += 1;
previousSibling = node.previousSibling;
}
return i;
}
function getCommonAncestor(node1, node2) {
const ancestors = [];
let n;
for (n = node1; n; n = n.parentNode) {
ancestors.push(n);
}
for (n = node2; n; n = n.parentNode) {
if (arrayContains(ancestors, n)) {
return n;
}
}
return null;
}
function comparePoints(nodeA, offsetA, nodeB, offsetB) {
// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
let n;
if (nodeA === nodeB) {
// Case 1: nodes are the same
if (offsetA === offsetB) {
return 0;
}
return offsetA < offsetB ? -1 : 1;
}
let nodeC = getClosestAncestorIn(nodeB, nodeA, true);
if (nodeC) {
// Case 2: node C (container B or an ancestor) is a child node of A
return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
}
nodeC = getClosestAncestorIn(nodeA, nodeB, true);
if (nodeC) {
// Case 3: node C (container A or an ancestor) is a child node of B
return getNodeIndex(nodeC) < offsetB ? -1 : 1;
}
const root = getCommonAncestor(nodeA, nodeB);
if (!root) {
throw new Error('comparePoints error: nodes have no common ancestor');
}
// Case 4: containers are siblings or descendants of siblings
const childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
const childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
if (childA === childB) {
// This shouldn't be possible
throw module.createError('comparePoints got to case 4 and childA and childB are the same!');
} else {
n = root.firstChild;
while (n) {
if (n === childA) {
return -1;
} else if (n === childB) {
return 1;
}
n = n.nextSibling;
}
}
}
function save() {
let result;
if (self.hasFocus()) {
const selectionStart = self.selectionStart;
const selectionEnd = self.selectionEnd;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const selectionRange = selection.getRangeAt(0);
const node = selectionRange.startContainer;
if ((contentElt.compareDocumentPosition(node) & window.Node.DOCUMENT_POSITION_CONTAINED_BY) || contentElt === node) {
var offset = selectionRange.startOffset
if (node.firstChild && offset > 0) {
node = node.childNodes[offset - 1]
offset = node.textContent.length
}
var container = node
while (node !== contentElt) {
while ((node = node.previousSibling)) {
offset += (node.textContent || '').length
}
node = container = container.parentNode
}
var selectionText = selectionRange + ''
// Fix end of line when only br is selected
var brElt = selectionRange.endContainer.firstChild
if (brElt && brElt.tagName === 'BR' && selectionRange.endOffset === 1) {
selectionText += '\n'
}
if (comparePoints(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset) === 1) {
selectionStart = offset + selectionText.length
selectionEnd = offset
} else {
selectionStart = offset
selectionEnd = offset + selectionText.length
}
if (selectionStart === selectionEnd && selectionStart === editor.getContent().length) {
// If cursor is after the trailingNode
selectionStart = --selectionEnd
result = self.setSelectionStartEnd(selectionStart, selectionEnd)
} else {
setSelection(selectionStart, selectionEnd)
result = checkSelection(selectionRange)
result = result || lastSelectionStart !== self.selectionStart // selectionRange doesn't change when selection is at the start of a section
}
}
}
}
return result
}
function saveCheckChange() {
return save() && (lastSelectionStart !== self.selectionStart || lastSelectionEnd !== self.selectionEnd)
}
var nextTickAdjustScroll = false
var debouncedSave = debounce(function () {
self.updateCursorCoordinates(saveCheckChange() && nextTickAdjustScroll)
// In some cases we have to wait a little longer to see the selection change (Cmd+A on Chrome OSX)
longerDebouncedSave()
})
var longerDebouncedSave = debounce(function () {
self.updateCursorCoordinates(saveCheckChange() && nextTickAdjustScroll)
nextTickAdjustScroll = false
}, 10)
return function (debounced, adjustScroll, forceAdjustScroll) {
if (forceAdjustScroll) {
lastSelectionStart = undefined
lastSelectionEnd = undefined
}
if (debounced) {
nextTickAdjustScroll = nextTickAdjustScroll || adjustScroll
return debouncedSave()
} else {
save()
}
}
})()
this.getSelectedText = function () {
var min = Math.min(this.selectionStart, this.selectionEnd)
var max = Math.max(this.selectionStart, this.selectionEnd)
return editor.getContent().substring(min, max)
}
this.getCoordinates = function (inputOffset, container, offsetInContainer) {
if (!container) {
var offset = this.findContainer(inputOffset)
container = offset.container
offsetInContainer = offset.offsetInContainer
}
var containerElt = container
if (!containerElt.hasChildNodes()) {
containerElt = container.parentNode
}
var isInvisible = false
var index = editor.$allElements.indexOf(containerElt)
while (containerElt.offsetHeight === 0 && index > 0) {
isInvisible = true
containerElt = editor.$allElements[--index]
}
var rect
var contentRect
var left = 'left'
if (isInvisible || container.textContent === '\n') {
rect = containerElt.getBoundingClientRect()
} else {
var selectedChar = editor.getContent()[inputOffset]
var startOffset = {
container: container,
offsetInContainer: offsetInContainer
}
var endOffset = {
container: container,
offsetInContainer: offsetInContainer
}
if (inputOffset > 0 && (selectedChar === undefined || selectedChar === '\n')) {
left = 'right'
if (startOffset.offsetInContainer === 0) {
// Need to calculate offset-1
startOffset = inputOffset - 1
} else {
startOffset.offsetInContainer -= 1
}
} else {
if (endOffset.offsetInContainer === container.textContent.length) {
// Need to calculate offset+1
endOffset = inputOffset + 1
} else {
endOffset.offsetInContainer += 1
}
}
var range = this.createRange(startOffset, endOffset)
rect = range.getBoundingClientRect()
}
contentRect = contentElt.getBoundingClientRect()
return {
top: Math.round(rect.top - contentRect.top + contentElt.scrollTop),
height: Math.round(rect.height),
left: Math.round(rect[left] - contentRect.left + contentElt.scrollLeft)
}
}
this.getClosestWordOffset = function (offset) {
var offsetStart = 0
var offsetEnd = 0
var nextOffset = 0
editor.getContent().split(/\s/).cl_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
}
}
}
cledit.SelectionMgr = SelectionMgr