Stackedit/src/services/cledit/cleditSelectionMgr.js
2018-03-12 00:45:54 +00:00

425 lines
14 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);
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 = (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 = (adjustScrollParam) => {
adjustScroll = adjustScroll || adjustScrollParam;
debouncedUpdateCursorCoordinates();
};
let oldSelectionRange;
const checkSelection = (selectionRange) => {
if (!oldSelectionRange ||
oldSelectionRange.startContainer !== selectionRange.startContainer ||
oldSelectionRange.startOffset !== selectionRange.startOffset ||
oldSelectionRange.endContainer !== selectionRange.endContainer ||
oldSelectionRange.endOffset !== selectionRange.endOffset
) {
oldSelectionRange = selectionRange;
this.$trigger('selectionChanged', this.selectionStart, this.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 = this.selectionStart;
lastSelectionEnd = this.selectionEnd;
}, 50);
const setSelection = (start = this.selectionStart, end = this.selectionEnd) => {
this.selectionStart = start < 0 ? 0 : start;
this.selectionEnd = end < 0 ? 0 : end;
saveLastSelection();
};
this.setSelectionStartEnd = (start, end, restoreSelection = true) => {
setSelection(start, end);
if (restoreSelection && this.hasFocus()) {
return this.restoreSelection();
}
return null;
};
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!');
}
n = root.firstChild;
while (n) {
if (n === childA) {
return -1;
} else if (n === childB) {
return 1;
}
n = n.nextSibling;
}
return 0;
}
const save = () => {
let result;
if (this.hasFocus()) {
let selectionStart = this.selectionStart;
let selectionEnd = this.selectionEnd;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const selectionRange = selection.getRangeAt(0);
let node = selectionRange.startContainer;
// eslint-disable-next-line no-bitwise
if ((contentElt.compareDocumentPosition(node)
& window.Node.DOCUMENT_POSITION_CONTAINED_BY)
|| contentElt === node
) {
let offset = selectionRange.startOffset;
if (node.firstChild && offset > 0) {
node = node.childNodes[offset - 1];
offset = node.textContent.length;
}
let container = node;
while (node !== contentElt) {
node = node.previousSibling;
while (node) {
offset += (node.textContent || '').length;
node = node.previousSibling;
}
node = container.parentNode;
container = node;
}
let selectionText = `${selectionRange}`;
// Fix end of line when only br is selected
const 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
selectionEnd -= 1;
selectionStart = selectionEnd;
result = this.setSelectionStartEnd(selectionStart, selectionEnd);
} else {
setSelection(selectionStart, selectionEnd);
result = checkSelection(selectionRange);
// selectionRange doesn't change when selection is at the start of a section
result = result || lastSelectionStart !== this.selectionStart;
}
}
}
}
return result;
};
const saveCheckChange = () => save() && (
lastSelectionStart !== this.selectionStart || lastSelectionEnd !== this.selectionEnd);
let nextTickAdjustScroll = false;
const longerDebouncedSave = debounce(() => {
this.updateCursorCoordinates(saveCheckChange() && nextTickAdjustScroll);
nextTickAdjustScroll = false;
}, 10);
const debouncedSave = debounce(() => {
this.updateCursorCoordinates(saveCheckChange() && nextTickAdjustScroll);
// In some cases we have to wait a little longer to see the
// selection change (Cmd+A on Chrome OSX)
longerDebouncedSave();
});
return (debounced, adjustScrollParam, forceAdjustScroll) => {
if (forceAdjustScroll) {
lastSelectionStart = undefined;
lastSelectionEnd = undefined;
}
if (debounced) {
nextTickAdjustScroll = nextTickAdjustScroll || adjustScrollParam;
debouncedSave();
} else {
save();
}
};
})();
this.getSelectedText = () => {
const min = Math.min(this.selectionStart, this.selectionEnd);
const max = Math.max(this.selectionStart, this.selectionEnd);
return editor.getContent().substring(min, max);
};
this.getCoordinates = (inputOffset, containerParam, offsetInContainerParam) => {
let container = containerParam;
let offsetInContainer = offsetInContainerParam;
if (!container) {
const offset = this.findContainer(inputOffset);
container = offset.container;
offsetInContainer = offset.offsetInContainer;
}
let containerElt = container;
if (!containerElt.hasChildNodes() && container.parentNode) {
containerElt = container.parentNode;
}
let isInvisible = false;
while (!containerElt.offsetHeight) {
isInvisible = true;
if (containerElt.previousSibling) {
containerElt = containerElt.previousSibling;
} else {
containerElt = containerElt.parentNode;
if (!containerElt) {
return {
top: 0,
height: 0,
left: 0,
};
}
}
}
let rect;
let left = 'left';
if (isInvisible || container.textContent === '\n') {
rect = containerElt.getBoundingClientRect();
} else {
const selectedChar = editor.getContent()[inputOffset];
let startOffset = {
container,
offsetInContainer,
};
let endOffset = {
container,
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;
}
const range = this.createRange(startOffset, endOffset);
rect = range.getBoundingClientRect();
}
const 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 = (offset) => {
let offsetStart = 0;
let offsetEnd = 0;
let nextOffset = 0;
editor.getContent().split(/\s/).cl_some((word) => {
if (word) {
offsetStart = nextOffset;
offsetEnd = nextOffset + word.length;
if (offsetEnd > offset) {
return true;
}
}
nextOffset += word.length + 1;
return false;
});
return {
start: offsetStart,
end: offsetEnd,
};
};
}
cledit.SelectionMgr = SelectionMgr;