getCountOfVisibleCharsInViewport functionality
parent
01fe885adc
commit
996f0e8a1d
|
@ -1441,6 +1441,13 @@ function Ace2Inner() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the node and index into this node that corresponds to a given line number and character
|
||||||
|
* position.
|
||||||
|
*
|
||||||
|
* @params {[number, number]} lineAndChar an array of the form [row, col]
|
||||||
|
* @returns {{node: HTMLElement, index: number, maxIndex: number}}
|
||||||
|
*/
|
||||||
function getPointForLineAndChar(lineAndChar) {
|
function getPointForLineAndChar(lineAndChar) {
|
||||||
const line = lineAndChar[0];
|
const line = lineAndChar[0];
|
||||||
let charsLeft = lineAndChar[1];
|
let charsLeft = lineAndChar[1];
|
||||||
|
@ -1455,6 +1462,7 @@ function Ace2Inner() {
|
||||||
const lineNode = lineEntry.lineNode;
|
const lineNode = lineEntry.lineNode;
|
||||||
let n = lineNode;
|
let n = lineNode;
|
||||||
let after = false;
|
let after = false;
|
||||||
|
// at [x, 0] of a line with line attributes
|
||||||
if (charsLeft === 0) {
|
if (charsLeft === 0) {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -372,6 +372,7 @@ Scroll.prototype.movePage = function (direction) {
|
||||||
Scroll.prototype.getFirstVisibleCharacter = function (direction, rep) {
|
Scroll.prototype.getFirstVisibleCharacter = function (direction, rep) {
|
||||||
const viewport = this._getViewPortTopBottom();
|
const viewport = this._getViewPortTopBottom();
|
||||||
const editor = parent.document.getElementsByTagName('iframe');
|
const editor = parent.document.getElementsByTagName('iframe');
|
||||||
|
// TODO can we make a better guess here or do we need to iterate over every line?
|
||||||
const lines = $(editor).contents().find('div');
|
const lines = $(editor).contents().find('div');
|
||||||
// const currentLine = $(editor).contents().find('#innerdocbody');
|
// const currentLine = $(editor).contents().find('#innerdocbody');
|
||||||
const currentLine = rep.lines.atIndex(rep.selEnd[0]);
|
const currentLine = rep.lines.atIndex(rep.selEnd[0]);
|
||||||
|
@ -413,72 +414,149 @@ Scroll.prototype.getFirstVisibleCharacter = function (direction, rep) {
|
||||||
return modifiedRep;
|
return modifiedRep;
|
||||||
};
|
};
|
||||||
|
|
||||||
// line is a DOM Line
|
/**
|
||||||
// returned is the number of characters in that index that are currently visible
|
* The fully visible characters of a DOM line.
|
||||||
// IE 120,240
|
* If the whole line is visible, then all characters inside that line are visible, too.
|
||||||
Scroll.prototype.getCountOfVisibleCharsInViewport = (line, viewport) => {
|
* It works by comparing the top and bottom of DOM line parts and the viewport.
|
||||||
const range = document.createRange();
|
*
|
||||||
const chars = line.text.split(''); // split "abc" into ["a","b","c"]
|
* The returned array is of the form:
|
||||||
const parentElement = document.getElementById(line.domInfo.node.id).childNodes;
|
* - first character visible: [0, x]
|
||||||
const charNumber = [];
|
* - first character not visible: [x, y] where x > 0
|
||||||
// top.console.log(parentElement);
|
* - last character visible: [x, y] where y == text length of the line
|
||||||
for (const node of parentElement) {
|
* - last character not visible: [x, y] where y < text length of the line
|
||||||
// each span..
|
* - null, if no character of the line is visible
|
||||||
// top.console.log('span', node); // shows all nodes from the collection
|
*
|
||||||
// top.console.log('span length', node.offsetTop); // shows all nodes from the collection
|
* Note that only whole lines count, ie in case of subscript/superscript or different font sizes
|
||||||
|
* inside a visible line, the upper or lower most pixels of the union of all characters must
|
||||||
// each character
|
* be visible. In other words: the first visible character of a line will always be the first
|
||||||
/*
|
* character of that line and this function won't return an array of characters, that are in the
|
||||||
let i = 0;
|
* middle of a line in the viewport (though it can return characters that are in the middle of a
|
||||||
console.log(node);
|
* DOM line)
|
||||||
if (!node || !node.childNodes) return;
|
*
|
||||||
node = node.childNodes[0];
|
*TODO rtl languages
|
||||||
if (!node) return; // temp patch to be removed.
|
*
|
||||||
if (node.childNodes && node.childNodes[1].length === 0) return;
|
*
|
||||||
console.log(node);
|
* @param {HTMLElement} line A DOM line that can be wrapped across multiple visible lines
|
||||||
console.log(node.wholeText.length);
|
* @param {{top: number, bottom: number}} viewport
|
||||||
while (i < node.wholeText.length) {
|
* @returns {[number, number]|null} fully visible characters in the DOM line
|
||||||
// top.console.log(i, node.textContent[i]);
|
|
||||||
const range = document.createRange();
|
|
||||||
let failed = false;
|
|
||||||
try {
|
|
||||||
range.setStart(node, i);
|
|
||||||
} catch (e) {
|
|
||||||
failed = true;
|
|
||||||
console.log('fail', e);
|
|
||||||
// console.log('node', node);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
range.setEnd(node, i + 1);
|
|
||||||
} catch (e) {
|
|
||||||
failed = true;
|
|
||||||
console.log('fail', e);
|
|
||||||
console.log('node', node);
|
|
||||||
}
|
|
||||||
// console.log('range', range);
|
|
||||||
let char;
|
|
||||||
if (!failed) char = range.getClientRects();
|
|
||||||
console.log(node);
|
|
||||||
console.log('charr????', char);
|
|
||||||
if (char) return;
|
|
||||||
if (char && char.length && char[0]) {
|
|
||||||
const topOffset = char[0].y;
|
|
||||||
charNumber.push(topOffset);
|
|
||||||
// is this element in view?
|
|
||||||
console.log('topOffset', topOffset, 'viewport', viewport);
|
|
||||||
if (topOffset > viewport.top) {
|
|
||||||
console.log('can put rep here!', i);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
top.console.log('charNumber', charNumber);
|
|
||||||
*/
|
*/
|
||||||
return; // TEMPJM CAKE remove once stable
|
Scroll.prototype.getCountOfVisibleCharsInViewport = (line, viewport) => {
|
||||||
|
const node = document.getElementById(line.domInfo.node.id);
|
||||||
|
const nodeTop = node.offsetTop;
|
||||||
|
const nodeHeight = node.offsetHeight;
|
||||||
|
const nodeBottom = nodeTop + nodeHeight;
|
||||||
|
const nodeLength = node.textContent.length;
|
||||||
|
|
||||||
|
// we can't compare viewport.bottom > nodeTop+lineHeight because that would not work on long lines
|
||||||
|
const startVisible = viewport.top < nodeTop && viewport.bottom > nodeTop;
|
||||||
|
const endVisible = viewport.bottom > nodeBottom && viewport.top < nodeBottom;
|
||||||
|
|
||||||
|
// the whole line is visible
|
||||||
|
if (startVisible && endVisible) return [0, nodeLength];
|
||||||
|
if (!startVisible && !endVisible) {
|
||||||
|
if (nodeTop < viewport.top && nodeBottom > viewport.bottom) {
|
||||||
|
return null;
|
||||||
|
// TODO only some chars visible in the middle of very long line that fills the whole viewport
|
||||||
|
} else {
|
||||||
|
// no character is visible
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if we are here we know that at least some pixel of the line are visible. If some pixel in a
|
||||||
|
// non-wrapped line are not visible, the whole line is considered not visible.
|
||||||
|
|
||||||
|
// is the line wrapped at viewport top or bottom?
|
||||||
|
let wrapAt;
|
||||||
|
if (startVisible && !endVisible) {
|
||||||
|
wrapAt = 'bottom';
|
||||||
|
} else if (!startVisible && endVisible) {
|
||||||
|
wrapAt = 'top';
|
||||||
|
}
|
||||||
|
const texts = [];
|
||||||
|
textNodes(node, texts);
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
if (wrapAt === 'top') {
|
||||||
|
const lastNode = texts[texts.length - 1];
|
||||||
|
range.setEnd(lastNode, lastNode.length - 1);
|
||||||
|
|
||||||
|
// text node we're working on
|
||||||
|
let textIndex = 0;
|
||||||
|
// characters in texts[textIndex]
|
||||||
|
let charIndex = 0;
|
||||||
|
// how many chars we need to skip to reach the first visible char
|
||||||
|
let skippedChars = 0;
|
||||||
|
// forward direction
|
||||||
|
range.setStart(texts[textIndex], charIndex);
|
||||||
|
|
||||||
|
let bb = range.getBoundingClientRect();
|
||||||
|
|
||||||
|
while (bb.top < viewport.top) {
|
||||||
|
if (texts[textIndex].length - 1 > charIndex) {
|
||||||
|
// we are not at the end of this text node yet
|
||||||
|
charIndex += 1;
|
||||||
|
skippedChars += 1;
|
||||||
|
} else if (texts.length - 1 > textIndex) {
|
||||||
|
// we have more text nodes
|
||||||
|
textIndex += 1;
|
||||||
|
charIndex = 0;
|
||||||
|
skippedChars += 1;
|
||||||
|
} else {
|
||||||
|
// all text nodes consumed, but none is fully visible
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
range.setStart(texts[textIndex], charIndex);
|
||||||
|
bb = range.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
return [skippedChars, nodeLength - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrapAt === 'bottom') {
|
||||||
|
range.setStart(texts[0], 0);
|
||||||
|
|
||||||
|
// text node we're working on
|
||||||
|
let textIndex = texts.length - 1;
|
||||||
|
// character in texts[textIndex]
|
||||||
|
let charIndex = texts[textIndex].length - 1;
|
||||||
|
// how many chars we need to skip to reach the first visible char
|
||||||
|
let skippedChars = 0;
|
||||||
|
// backward direction
|
||||||
|
range.setEnd(texts[textIndex], charIndex);
|
||||||
|
while (range.getBoundingClientRect().bottom > viewport.bottom) {
|
||||||
|
if (charIndex > 0) {
|
||||||
|
// we are not at the beginning of the text node yet
|
||||||
|
charIndex -= 1;
|
||||||
|
skippedChars += 1;
|
||||||
|
} else if (textIndex > 0) {
|
||||||
|
// we have more text nodes
|
||||||
|
textIndex -= 1;
|
||||||
|
charIndex = texts[textIndex].length;
|
||||||
|
skippedChars += 1;
|
||||||
|
} else {
|
||||||
|
// all text nodes consumed, but none is fully visible
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
range.setEnd(texts[textIndex], charIndex);
|
||||||
|
}
|
||||||
|
return [0, nodeLength - skippedChars - 1];
|
||||||
}
|
}
|
||||||
return 1000;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over a node and returns all text node descendants
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node A DOM line
|
||||||
|
*/
|
||||||
|
function textNodes(node, texts) {
|
||||||
|
node.childNodes.forEach((child) => {
|
||||||
|
// lists somehow end up as a text node here, but they don't have a nodeValue
|
||||||
|
if (child.nodeType === 3 && child.nodeValue !== '') {
|
||||||
|
texts.push(child);
|
||||||
|
} else {
|
||||||
|
textNodes(child, texts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.init = (outerWin) => new Scroll(outerWin);
|
exports.init = (outerWin) => new Scroll(outerWin);
|
||||||
|
|
|
@ -21,17 +21,20 @@ describe('Page Up & Page Down', function () {
|
||||||
it('scrolls up on key stroke', async function () {
|
it('scrolls up on key stroke', async function () {
|
||||||
await helper.edit('Line 80', 80);
|
await helper.edit('Line 80', 80);
|
||||||
await helper.waitForPromise(() => 81 === helper.caretLineNumber());
|
await helper.waitForPromise(() => 81 === helper.caretLineNumber());
|
||||||
// for some reason the page isn't inline with the edit
|
// because we don't send the edit via key events but using `sendkeys` the viewport is
|
||||||
helper.padOuter$('#outerdocbody').parent().scrollTop(1000);
|
// not automatically scrolled. The line below puts the viewport top exactly to where
|
||||||
|
// the caret is.
|
||||||
|
let lineOffset = helper.linesDiv()[80][0].offsetTop;
|
||||||
|
helper.padOuter$('#outerdocbody').parent().scrollTop(lineOffset);
|
||||||
let intitialLineNumber = helper.caretLineNumber();
|
let intitialLineNumber = helper.caretLineNumber();
|
||||||
helper.pageUp();
|
helper.pageUp();
|
||||||
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber());
|
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber() &&
|
||||||
|
lineOffset > helper.padOuter$('#outerdocbody').parent().scrollTop());
|
||||||
intitialLineNumber = helper.caretLineNumber();
|
intitialLineNumber = helper.caretLineNumber();
|
||||||
|
lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
|
||||||
helper.pageUp();
|
helper.pageUp();
|
||||||
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber());
|
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber() &&
|
||||||
await helper.waitForPromise(
|
lineOffset > helper.padOuter$('#outerdocbody').parent().scrollTop());
|
||||||
() => helper.padOuter$('#outerdocbody').parent().scrollTop() < 1000
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
// scrolls down 3 times
|
// scrolls down 3 times
|
||||||
it('scrolls down on key stroke', async function () {
|
it('scrolls down on key stroke', async function () {
|
||||||
|
@ -39,15 +42,21 @@ describe('Page Up & Page Down', function () {
|
||||||
await helper.edit('Line 1', 1);
|
await helper.edit('Line 1', 1);
|
||||||
|
|
||||||
let currentLineNumber = helper.caretLineNumber();
|
let currentLineNumber = helper.caretLineNumber();
|
||||||
|
let lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
|
||||||
helper.pageDown();
|
helper.pageDown();
|
||||||
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber());
|
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber() &&
|
||||||
|
lineOffset < helper.padOuter$('#outerdocbody').parent().scrollTop());
|
||||||
|
|
||||||
currentLineNumber = helper.caretLineNumber();
|
currentLineNumber = helper.caretLineNumber();
|
||||||
|
lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
|
||||||
helper.pageDown();
|
helper.pageDown();
|
||||||
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber());
|
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber() &&
|
||||||
|
lineOffset < helper.padOuter$('#outerdocbody').parent().scrollTop());
|
||||||
|
|
||||||
currentLineNumber = helper.caretLineNumber();
|
currentLineNumber = helper.caretLineNumber();
|
||||||
|
lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
|
||||||
helper.pageDown();
|
helper.pageDown();
|
||||||
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber());
|
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber() &&
|
||||||
|
lineOffset < helper.padOuter$('#outerdocbody').parent().scrollTop());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue