From 4f1c26961d8e127488174a0172be380b75f30dce Mon Sep 17 00:00:00 2001 From: Evan Henley Date: Mon, 4 Dec 2017 13:27:54 -0600 Subject: [PATCH] refactor scroll logic (#1428) --- .../src/utils/scroll-to-selection.js | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/packages/slate-react/src/utils/scroll-to-selection.js b/packages/slate-react/src/utils/scroll-to-selection.js index 44c0f24f7..25c340fda 100644 --- a/packages/slate-react/src/utils/scroll-to-selection.js +++ b/packages/slate-react/src/utils/scroll-to-selection.js @@ -20,13 +20,13 @@ const OVERFLOWS = [ * @param {el} Element */ -function findScrollContainer(el) { - const window = getWindow(el) +function findScrollContainer(el, window) { let parent = el.parentNode let scroller while (!scroller) { if (!parent.parentNode) break + const style = window.getComputedStyle(parent) const { overflowY } = style @@ -38,7 +38,14 @@ function findScrollContainer(el) { parent = parent.parentNode } - if (!scroller) return window + // COMPAT: Because Chrome does not allow doucment.body.scrollTop, we're + // assuming that window.scrollTo() should be used if the scrollable element + // turns out to be document.body or document.documentElement. This will work + // unless body is intentionally set to scrollable by restricting its height + // (e.g. height: 100vh). + if (!scroller) { + return window.document.body + } return scroller } @@ -53,41 +60,88 @@ function scrollToSelection(selection) { if (!selection.anchorNode) return const window = getWindow(selection.anchorNode) - const scroller = findScrollContainer(selection.anchorNode) - const isWindow = scroller == window + const scroller = findScrollContainer(selection.anchorNode, window) + const isWindow = scroller == window.document.body || scroller == window.document.documentElement const backward = isBackward(selection) + const range = selection.getRangeAt(0) - const rect = range.getBoundingClientRect() + let selectionRect = range.getBoundingClientRect() + + // COMPAT: range.getBoundingClientRect() returns 0s in Safari when range is + // collapsed. Expanding the range by 1 is a relatively effective workaround + // for vertical scroll, although horizontal may be off by 1 character. + // https://bugs.webkit.org/show_bug.cgi?id=138949 + // https://bugs.chromium.org/p/chromium/issues/detail?id=435438 + if (range.collapsed && selectionRect.top == 0 && selectionRect.height == 0) { + if (range.startOffset == 0) { + range.setEnd(range.endContainer, 1) + } else { + range.setStart(range.startContainer, range.startOffset - 1) + } + + selectionRect = range.getBoundingClientRect() + } + let width let height let yOffset let xOffset + let scrollerTop = 0 + let scrollerLeft = 0 + let scrollerBordersY = 0 + let scrollerBordersX = 0 if (isWindow) { - const { innerWidth, innerHeight, pageYOffset, pageXOffset } = scroller + const { innerWidth, innerHeight, pageYOffset, pageXOffset } = window width = innerWidth height = innerHeight yOffset = pageYOffset xOffset = pageXOffset } else { const { offsetWidth, offsetHeight, scrollTop, scrollLeft } = scroller + const { + borderTopWidth, + borderBottomWidth, + borderLeftWidth, + borderRightWidth, + } = window.getComputedStyle(scroller) + const scrollerRect = scroller.getBoundingClientRect() width = offsetWidth height = offsetHeight - yOffset = scrollTop - scrollerRect.top - xOffset = scrollLeft - scrollerRect.left + scrollerTop = scrollerRect.top + parseInt(borderTopWidth, 10) + scrollerLeft = scrollerRect.left + parseInt(borderLeftWidth, 10) + scrollerBordersY = parseInt(borderTopWidth, 10) + parseInt(borderBottomWidth, 10) + scrollerBordersX = parseInt(borderLeftWidth, 10) + parseInt(borderRightWidth, 10) + yOffset = scrollTop + xOffset = scrollLeft } - const top = (backward ? rect.top : rect.bottom) + yOffset - const left = (backward ? rect.left : rect.right) + xOffset + const selectionFocusTop = backward ? selectionRect.top : selectionRect.bottom + const selectionTop = selectionFocusTop + yOffset - scrollerTop - const x = left < yOffset || (width + xOffset) < left - ? left - width / 2 - : xOffset + const selectionFocusLeft = backward ? selectionRect.left : selectionRect.right + const selectionLeft = selectionFocusLeft + xOffset - scrollerLeft + + let x = xOffset + let y = yOffset + + if (selectionLeft < xOffset) { + // selection to the left of viewport + x = selectionLeft + } else if (selectionLeft + selectionRect.width + scrollerBordersX > xOffset + width) { + // selection to the right of viewport + x = selectionLeft + scrollerBordersX - width + } + + if (selectionTop < yOffset) { + // selection above viewport + y = selectionTop + } else if (selectionTop + selectionRect.height + scrollerBordersY > yOffset + height) { + // selection below viewport + y = selectionTop + scrollerBordersY + selectionRect.height - height + } - const y = top < yOffset || (height + yOffset) < top - ? top - height / 2 - : yOffset if (isWindow) { window.scrollTo(x, y)