getCountOfVisibleCharsInViewport functionality

page-down-up-bugfix
webzwo0i 2021-01-08 08:19:15 +01:00
parent 01fe885adc
commit 996f0e8a1d
3 changed files with 166 additions and 71 deletions

View File

@ -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) {
const line = lineAndChar[0];
let charsLeft = lineAndChar[1];
@ -1455,6 +1462,7 @@ function Ace2Inner() {
const lineNode = lineEntry.lineNode;
let n = lineNode;
let after = false;
// at [x, 0] of a line with line attributes
if (charsLeft === 0) {
const index = 0;
return {

View File

@ -372,6 +372,7 @@ Scroll.prototype.movePage = function (direction) {
Scroll.prototype.getFirstVisibleCharacter = function (direction, rep) {
const viewport = this._getViewPortTopBottom();
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 currentLine = $(editor).contents().find('#innerdocbody');
const currentLine = rep.lines.atIndex(rep.selEnd[0]);
@ -413,72 +414,149 @@ Scroll.prototype.getFirstVisibleCharacter = function (direction, rep) {
return modifiedRep;
};
// line is a DOM Line
// returned is the number of characters in that index that are currently visible
// IE 120,240
/**
* The fully visible characters of a DOM line.
* If the whole line is visible, then all characters inside that line are visible, too.
* It works by comparing the top and bottom of DOM line parts and the viewport.
*
* The returned array is of the form:
* - first character visible: [0, x]
* - first character not visible: [x, y] where x > 0
* - last character visible: [x, y] where y == text length of the line
* - last character not visible: [x, y] where y < text length of the line
* - null, if no character of the line is visible
*
* 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
* 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
* middle of a line in the viewport (though it can return characters that are in the middle of a
* DOM line)
*
*TODO rtl languages
*
*
* @param {HTMLElement} line A DOM line that can be wrapped across multiple visible lines
* @param {{top: number, bottom: number}} viewport
* @returns {[number, number]|null} fully visible characters in the DOM line
*/
Scroll.prototype.getCountOfVisibleCharsInViewport = (line, viewport) => {
const range = document.createRange();
const chars = line.text.split(''); // split "abc" into ["a","b","c"]
const parentElement = document.getElementById(line.domInfo.node.id).childNodes;
const charNumber = [];
// top.console.log(parentElement);
for (const node of parentElement) {
// each span..
// top.console.log('span', node); // shows all nodes from the collection
// top.console.log('span length', node.offsetTop); // shows all nodes from the collection
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;
// each character
/*
let i = 0;
console.log(node);
if (!node || !node.childNodes) return;
node = node.childNodes[0];
if (!node) return; // temp patch to be removed.
if (node.childNodes && node.childNodes[1].length === 0) return;
console.log(node);
console.log(node.wholeText.length);
while (i < node.wholeText.length) {
// 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++;
// 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;
}
top.console.log('charNumber', charNumber);
*/
return; // TEMPJM CAKE remove once stable
}
return 1000;
// 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];
}
};
/**
* 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);

View File

@ -21,17 +21,20 @@ describe('Page Up & Page Down', function () {
it('scrolls up on key stroke', async function () {
await helper.edit('Line 80', 80);
await helper.waitForPromise(() => 81 === helper.caretLineNumber());
// for some reason the page isn't inline with the edit
helper.padOuter$('#outerdocbody').parent().scrollTop(1000);
// because we don't send the edit via key events but using `sendkeys` the viewport is
// 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();
helper.pageUp();
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber());
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber() &&
lineOffset > helper.padOuter$('#outerdocbody').parent().scrollTop());
intitialLineNumber = helper.caretLineNumber();
lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
helper.pageUp();
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber());
await helper.waitForPromise(
() => helper.padOuter$('#outerdocbody').parent().scrollTop() < 1000
);
await helper.waitForPromise(() => intitialLineNumber > helper.caretLineNumber() &&
lineOffset > helper.padOuter$('#outerdocbody').parent().scrollTop());
});
// scrolls down 3 times
it('scrolls down on key stroke', async function () {
@ -39,15 +42,21 @@ describe('Page Up & Page Down', function () {
await helper.edit('Line 1', 1);
let currentLineNumber = helper.caretLineNumber();
let lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
helper.pageDown();
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber());
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber() &&
lineOffset < helper.padOuter$('#outerdocbody').parent().scrollTop());
currentLineNumber = helper.caretLineNumber();
lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
helper.pageDown();
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber());
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber() &&
lineOffset < helper.padOuter$('#outerdocbody').parent().scrollTop());
currentLineNumber = helper.caretLineNumber();
lineOffset = helper.padOuter$('#outerdocbody').parent().scrollTop();
helper.pageDown();
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber());
await helper.waitForPromise(() => currentLineNumber < helper.caretLineNumber() &&
lineOffset < helper.padOuter$('#outerdocbody').parent().scrollTop());
});
});