Skip to content
Snippets Groups Projects
Commit dd809cc8 authored by Marius Dumitru Florea's avatar Marius Dumitru Florea
Browse files

XWIKI-22899: The page is scrolled to the bottom when switching to Source while editing in-place

* Improve the code that scrolls the selection into view.

(cherry picked from commit d8a92eb2)
parent 3014e603
No related branches found
No related tags found
No related merge requests found
......@@ -661,20 +661,41 @@ define('textSelection', ['jquery', 'xwiki-diff-service', 'scrollUtils'], functio
}
function scrollSelectionIntoView(element, range) {
const padding = 65;
if (isTextInput(element)) {
// See https://bugs.chromium.org/p/chromium/issues/detail?id=331233
var fullText = element.value;
const fullText = element.value;
const styleBackup = element.style.cssText;
// Determine the scroll offset corresponding to the start and end of the text selection.
element.style.height = element.style.minHeight = 0;
element.style.overflowY = 'hidden';
// Cut the text after the selection start.
element.value = fullText.substring(0, range.startOffset);
const scrollStartOfset = element.scrollHeight;
// Cut the text after the selection end.
element.value = fullText.substring(0, range.endOffset);
// Scroll to the bottom.
element.scrollTop = element.scrollHeight;
var canScroll = element.scrollHeight > element.clientHeight;
const scrollEndOffset = element.scrollHeight;
const selectionHeight = scrollEndOffset - scrollStartOfset;
// Restore the full text and the text area styles.
element.value = fullText;
if (canScroll) {
// Scroll to center the selection.
element.scrollTop += element.clientHeight / 2;
element.style.cssText = styleBackup;
const canScrollVertically = element.scrollHeight > element.clientHeight;
if (canScrollVertically) {
if (selectionHeight < element.clientHeight) {
// Center the selection inside the text area.
element.scrollTop = scrollStartOfset - (element.clientHeight - selectionHeight) / 2;
} else {
// Align the selection start to the top of the text area.
element.scrollTop = scrollStartOfset;
}
} else {
scrollUtils.centerVertically(element, padding, {
startOffset: scrollStartOfset,
endOffset: scrollEndOffset
});
}
} else {
scrollUtils.centerVertically(getScrollTarget(range), 65);
scrollUtils.centerVertically(getScrollTarget(range), padding);
}
}
......@@ -789,16 +810,26 @@ define('scrollUtils', ['jquery'], function($) {
* @param padding the amount of pixels from the top and from the bottom of the scroll parent that delimits the center
* area; when specified, the element is centered vertically only if it's not already in the center area
* defined by this padding
* @param verticalRange the vertical segment of the given element that should be centered, defaults to the entire
* element
*/
var centerVertically = function(element, padding) {
var verticalScrollParent = getVerticalScrollParent(element);
var centerVertically = function(
element,
padding = 0,
verticalRange = {startOffset: 0, endOffset: element.clientHeight}
) {
const verticalScrollParent = getVerticalScrollParent(element);
if (verticalScrollParent) {
var relativeTopOffset = getRelativeTopOffset(element, verticalScrollParent);
const rangeHeight = verticalRange.endOffset - verticalRange.startOffset;
const relativeTopOffset = getRelativeTopOffset(element, verticalScrollParent) + verticalRange.startOffset;
if (!padding || !isCenteredVertically(verticalScrollParent, padding, relativeTopOffset)) {
// Center the element by removing half of the scroll parent height (i.e. half of the visible vertical space)
// from the element's relative position. If this is a negative value then the browser will use 0 instead.
var scrollTop = relativeTopOffset - (verticalScrollParent.clientHeight / 2);
verticalScrollParent.scrollTop = scrollTop;
if (rangeHeight < verticalScrollParent.clientHeight) {
// Center the specified vertical range inside the scroll parent.
verticalScrollParent.scrollTop = relativeTopOffset - (verticalScrollParent.clientHeight - rangeHeight) / 2;
} else {
// Align the top of the specified vertical range to the top of the scroll parent.
verticalScrollParent.scrollTop = relativeTopOffset;
}
}
}
};
......
......@@ -28,6 +28,7 @@
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.StaleElementReferenceException;
......@@ -634,4 +635,38 @@ public void waitUntilWidgetSelected()
{
waitUntilContentContains("cke_widget_selected");
}
/**
* @return the text selected in the rich text area, or an empty string if no text is selected
* @since 16.10.5
* @since 17.1.0
*/
public String getSelectedText()
{
return (String) getDriver().executeScript(
"return CKEDITOR.instances[arguments[0]].getSelection().getSelectedText()", this.editor.getName());
}
/**
* @return the size of the rich text area
* @since 16.10.5
* @since 17.1.0
*/
public Dimension getSize()
{
return this.container.getSize();
}
/**
* @param xOffset the horizontal offset from the text area's left border
* @param yOffset the vertical offset from the text area's top border
* @return {@code true} if the specified point inside the text area is visible (i.e. inside the viewport),
* {@code false} otherwise
* @since 16.10.5
* @since 17.1.0
*/
public boolean isVisible(int xOffset, int yOffset)
{
return getDriver().isVisible(this.container, xOffset, yOffset);
}
}
......@@ -345,4 +345,95 @@ void macroPlaceholder(TestUtils setup, TestReference testReference)
assertEquals("No pages found\nmacro:id", richTextArea.getText());
viewPage.cancel();
}
@Test
@Order(7)
void selectionRestoreOnSwitchToSource(TestUtils setup, TestReference testReference)
{
// We test using the in-place editor because the Source area doesn't have the vertical scrollbar (as it happens
// with the standalone editor) so the way the restored selection is scrolled into view is different.
// Enter in-place edit mode.
InplaceEditablePage viewPage = new InplaceEditablePage().editInplace();
CKEditor ckeditor = new CKEditor("content");
RichTextAreaElement richTextArea = ckeditor.getRichTextArea();
richTextArea.clear();
// Insert some long text (vertically).
for (int i = 1; i < 50; i++) {
richTextArea.sendKeys(String.valueOf(i), Keys.ENTER);
}
richTextArea.sendKeys("50");
// Go back to the start of the content, on the second line (paragraph).
richTextArea.sendKeys(Keys.HOME, Keys.PAGE_UP, Keys.PAGE_UP, Keys.PAGE_UP, Keys.DOWN);
// Select the text on the second line.
richTextArea.sendKeys(Keys.chord(Keys.SHIFT, Keys.END));
// Switch to Source mode.
ckeditor.getToolBar().toggleSourceMode();
WebElement sourceTextArea = ckeditor.getSourceTextArea();
// Verify that the selection is restored.
assertEquals("2", sourceTextArea.getDomProperty("selectionStart"));
assertEquals("4", sourceTextArea.getDomProperty("selectionEnd"));
// Verify that the top left corner of the Source text area is visible (inside the viewport).
assertTrue(setup.getDriver().isVisible(sourceTextArea, 0, 0));
// Select something from the middle of the edited content.
for (int i = 0; i < 46; i++) {
sourceTextArea.sendKeys(Keys.DOWN);
}
sourceTextArea.sendKeys(Keys.HOME);
sourceTextArea.sendKeys(Keys.chord(Keys.SHIFT, Keys.END));
// Switch back to WYSIWYG mode.
ckeditor.getToolBar().toggleSourceMode();
// Verify that the selection is restored.
assertEquals("25", richTextArea.getSelectedText());
// Verify that the restored selection is visible.
assertTrue(richTextArea.isVisible(0, richTextArea.getSize().height / 2));
// Switch back to Source.
richTextArea.sendKeys(Keys.DOWN);
richTextArea.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME));
ckeditor.getToolBar().toggleSourceMode();
sourceTextArea = ckeditor.getSourceTextArea();
// Verify that the selection is restored.
int selectionStart = Integer.parseInt(sourceTextArea.getDomProperty("selectionStart"));
int selectionEnd = Integer.parseInt(sourceTextArea.getDomProperty("selectionEnd"));
assertEquals("26", sourceTextArea.getDomProperty("value").substring(selectionStart, selectionEnd));
// Verify that the restored selection is visible (inside the viewport).
assertTrue(setup.getDriver().isVisible(sourceTextArea, 0, sourceTextArea.getSize().height / 2));
sourceTextArea.sendKeys(Keys.PAGE_DOWN, Keys.UP, Keys.UP);
sourceTextArea.sendKeys(Keys.chord(Keys.SHIFT, Keys.HOME));
// Switch back to WYSIWYG mode.
ckeditor.getToolBar().toggleSourceMode();
// Verify that the selection is restored.
assertEquals("49", richTextArea.getSelectedText());
// Verify that the restored selection is visible.
// Note that we have to subtract 1 from the height because the floating toolbar is overlapping the text area.
assertTrue(richTextArea.isVisible(0, richTextArea.getSize().height - 1));
// Switch back to Source.
richTextArea.sendKeys(Keys.DOWN);
richTextArea.sendKeys(Keys.chord(Keys.SHIFT, Keys.END));
ckeditor.getToolBar().toggleSourceMode();
sourceTextArea = ckeditor.getSourceTextArea();
// Verify that the selection is restored.
selectionStart = Integer.parseInt(sourceTextArea.getDomProperty("selectionStart"));
selectionEnd = Integer.parseInt(sourceTextArea.getDomProperty("selectionEnd"));
assertEquals("50", sourceTextArea.getDomProperty("value").substring(selectionStart, selectionEnd));
// Verify that the restored selection is visible (inside the viewport).
// Note that we have to subtract 1 from the height because the floating toolbar is overlapping the text area.
assertTrue(setup.getDriver().isVisible(sourceTextArea, 0, sourceTextArea.getSize().height - 1));
viewPage.cancel();
}
}
......@@ -972,4 +972,32 @@ private void waitUntilCondition(WebElement element, Predicate<WebElement> condit
}
});
}
/**
* @param element the element to check if it is visible
* @param xOffset the horizontal offset from the element's left border
* @param yOffset the vertical offset from the element's top border
* @return {@code true} if the specified point inside the given element is visible (inside the viewport),
* {@code false} otherwise
* @since 16.10.5
* @since 17.1.0
*/
public boolean isVisible(WebElement element, int xOffset, int yOffset)
{
StringBuilder script = new StringBuilder();
script.append("const element = arguments[0];\n");
script.append("const xOffset = arguments[1];\n");
script.append("const yOffset = arguments[2];\n");
script.append("const box = element.getBoundingClientRect();\n");
script.append("const x = box.left + xOffset;\n");
script.append("const y = box.top + yOffset;\n");
script.append("let target = document.elementFromPoint(x, y);\n");
script.append("for (; target; target = target.parentElement) {\n");
script.append(" if (target === element) {\n");
script.append(" return true;\n");
script.append(" }\n");
script.append("}\n");
script.append("return false;\n");
return (boolean) executeScript(script.toString(), element, xOffset, yOffset);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment