mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #14628 from overleaf/td-review-panel-scroll-jump
Fix scroll jump and improvements to React review panel layout and performance GitOrigin-RevId: 3123f90abeb1bf9712ba4025a272e13990aebbd9
This commit is contained in:
parent
0d932a226b
commit
5cce9683fa
11 changed files with 354 additions and 189 deletions
|
@ -27,20 +27,20 @@ function AddCommentEntry({ entryId }: AddCommentEntryProps) {
|
||||||
|
|
||||||
const handleStartNewComment = () => {
|
const handleStartNewComment = () => {
|
||||||
setIsAddingComment(true)
|
setIsAddingComment(true)
|
||||||
window.setTimeout(handleLayoutChange, 0)
|
handleLayoutChange({ async: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmitNewComment = () => {
|
const handleSubmitNewComment = () => {
|
||||||
submitNewComment(content)
|
submitNewComment(content)
|
||||||
setIsAddingComment(false)
|
setIsAddingComment(false)
|
||||||
setContent('')
|
setContent('')
|
||||||
window.setTimeout(handleLayoutChange, 0)
|
handleLayoutChange({ async: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancelNewComment = () => {
|
const handleCancelNewComment = () => {
|
||||||
setIsAddingComment(false)
|
setIsAddingComment(false)
|
||||||
setContent('')
|
setContent('')
|
||||||
window.setTimeout(handleLayoutChange, 0)
|
handleLayoutChange({ async: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -111,8 +111,7 @@ function CommentEntry({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!submitting) {
|
if (!submitting) {
|
||||||
// Ensure everything is rendered in the DOM before updating the layout.
|
// Ensure everything is rendered in the DOM before updating the layout.
|
||||||
// Having to use a timeout seems less than ideal.
|
handleLayoutChange({ async: true })
|
||||||
window.setTimeout(handleLayoutChange, 0)
|
|
||||||
}
|
}
|
||||||
}, [submitting, handleLayoutChange])
|
}, [submitting, handleLayoutChange])
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ function useIndicatorHover() {
|
||||||
setHoverCoords(null)
|
setHoverCoords(null)
|
||||||
setLayoutSuspended(false)
|
setLayoutSuspended(false)
|
||||||
})
|
})
|
||||||
handleLayoutChange(true)
|
handleLayoutChange({ force: true })
|
||||||
}
|
}
|
||||||
}, [handleLayoutChange, layoutToLeft, reviewPanelOpen, setLayoutSuspended])
|
}, [handleLayoutChange, layoutToLeft, reviewPanelOpen, setLayoutSuspended])
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { isEqual } from 'lodash'
|
||||||
type Positions = {
|
type Positions = {
|
||||||
entryTop: number
|
entryTop: number
|
||||||
callout: { top: number; height: number; inverted: boolean }
|
callout: { top: number; height: number; inverted: boolean }
|
||||||
|
inViewport: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryView = {
|
type EntryView = {
|
||||||
|
@ -25,13 +26,26 @@ type EntryView = {
|
||||||
layout: HTMLElement
|
layout: HTMLElement
|
||||||
height: number
|
height: number
|
||||||
entry: ReviewPanelEntry
|
entry: ReviewPanelEntry
|
||||||
visible: boolean
|
hasScreenPos: boolean
|
||||||
positions?: Positions
|
positions?: Positions
|
||||||
previousPositions?: Positions
|
previousPositions?: Positions
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryPositions = Pick<EntryView, 'entryId' | 'positions'>
|
type EntryPositions = Pick<EntryView, 'entryId' | 'positions'>
|
||||||
|
|
||||||
|
type PositionedEntriesProps = {
|
||||||
|
entries: Array<[keyof ReviewPanelDocEntries, ReviewPanelEntry]>
|
||||||
|
contentHeight: number
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialLayoutInfo = {
|
||||||
|
focusedEntryIndex: 0,
|
||||||
|
overflowTop: 0,
|
||||||
|
height: 0,
|
||||||
|
positions: [] as EntryPositions[],
|
||||||
|
}
|
||||||
|
|
||||||
function css(el: HTMLElement, props: React.CSSProperties) {
|
function css(el: HTMLElement, props: React.CSSProperties) {
|
||||||
Object.assign(el.style, props)
|
Object.assign(el.style, props)
|
||||||
}
|
}
|
||||||
|
@ -58,18 +72,11 @@ function positionsEqual(
|
||||||
return isEqual(entryPos1, entryPos2)
|
return isEqual(entryPos1, entryPos2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateEntryViewPositions = (
|
function updateEntryPositions(
|
||||||
entryViews: EntryView[],
|
entryView: EntryView,
|
||||||
lineHeight: number,
|
entryTop: number,
|
||||||
calculateTop: (originalTop: number, height: number) => number
|
lineHeight: number
|
||||||
) => {
|
) {
|
||||||
for (const entryView of entryViews) {
|
|
||||||
entryView.wrapper.classList.toggle('rp-entry-hidden', !entryView.visible)
|
|
||||||
if (entryView.visible) {
|
|
||||||
const entryTop = calculateTop(
|
|
||||||
entryView.entry.screenPos.y,
|
|
||||||
entryView.height
|
|
||||||
)
|
|
||||||
const callout = calculateCalloutPosition(
|
const callout = calculateCalloutPosition(
|
||||||
entryView.entry.screenPos,
|
entryView.entry.screenPos,
|
||||||
entryTop,
|
entryTop,
|
||||||
|
@ -78,23 +85,118 @@ const calculateEntryViewPositions = (
|
||||||
entryView.positions = {
|
entryView.positions = {
|
||||||
entryTop,
|
entryTop,
|
||||||
callout,
|
callout,
|
||||||
}
|
inViewport: entryView.entry.inViewport,
|
||||||
}
|
|
||||||
debugConsole.log('ENTRY', { entry: entryView.entry, top })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PositionedEntriesProps = {
|
function calculateEntryViewPositions(
|
||||||
entries: Array<[keyof ReviewPanelDocEntries, ReviewPanelEntry]>
|
entryViews: EntryView[],
|
||||||
contentHeight: number
|
lineHeight: number,
|
||||||
children: React.ReactNode
|
calculateTop: (originalTop: number, height: number) => number
|
||||||
|
) {
|
||||||
|
for (const entryView of entryViews) {
|
||||||
|
if (entryView.hasScreenPos) {
|
||||||
|
const entryTop = calculateTop(
|
||||||
|
entryView.entry.screenPos.y,
|
||||||
|
entryView.height
|
||||||
|
)
|
||||||
|
updateEntryPositions(entryView, entryTop, lineHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialLayoutInfo = {
|
function hideOrShowEntries(entryViews: EntryView[]) {
|
||||||
focusedEntryIndex: 0,
|
for (const entryView of entryViews) {
|
||||||
overflowTop: 0,
|
// Completely hide any entry that has no screen position
|
||||||
height: 0,
|
entryView.wrapper.classList.toggle(
|
||||||
positions: [] as EntryPositions[],
|
'rp-entry-hidden',
|
||||||
|
!entryView.hasScreenPos
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEntryTop(entryView: EntryView, top: number) {
|
||||||
|
entryView.box.style.top = top + 'px'
|
||||||
|
|
||||||
|
if (entryView.indicator) {
|
||||||
|
entryView.indicator.style.top = top + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEntryVisibility(entryView: EntryView) {
|
||||||
|
// The entry element is invisible by default, to avoid flickering when
|
||||||
|
// positioning for the first time. Here we make sure it becomes visible after
|
||||||
|
// acquiring a screen position.
|
||||||
|
if (entryView.entry.inViewport) {
|
||||||
|
entryView.box.style.visibility = 'visible'
|
||||||
|
entryView.callout.style.visibility = 'visible'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position everything where it was before, taking into account the new top
|
||||||
|
// overflow
|
||||||
|
function moveEntriesToInitialPosition(
|
||||||
|
entryViews: EntryView[],
|
||||||
|
overflowTop: number
|
||||||
|
) {
|
||||||
|
for (const entryView of entryViews) {
|
||||||
|
const { callout: calloutEl, positions } = entryView
|
||||||
|
if (positions) {
|
||||||
|
const { entryTop, callout } = positions
|
||||||
|
|
||||||
|
// Position the main wrapper in its original position, if it had
|
||||||
|
// one, or its new position otherwise
|
||||||
|
const entryTopInitial = entryView.previousPositions
|
||||||
|
? entryView.previousPositions.entryTop
|
||||||
|
: entryTop
|
||||||
|
|
||||||
|
applyEntryVisibility(entryView)
|
||||||
|
applyEntryTop(entryView, entryTopInitial + overflowTop)
|
||||||
|
|
||||||
|
// Position the callout element in its original position, if it had
|
||||||
|
// one, or its new position otherwise
|
||||||
|
calloutEl.classList.toggle('rp-entry-callout-inverted', callout.inverted)
|
||||||
|
const calloutTopInitial = entryView.previousPositions
|
||||||
|
? entryView.previousPositions.callout.top
|
||||||
|
: callout.top
|
||||||
|
|
||||||
|
css(calloutEl, {
|
||||||
|
top: calloutTopInitial + overflowTop + 'px',
|
||||||
|
height: callout.height + 'px',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveEntriesToFinalPositions(
|
||||||
|
entryViews: EntryView[],
|
||||||
|
overflowTop: number,
|
||||||
|
shouldApplyVisibility: boolean
|
||||||
|
) {
|
||||||
|
for (const entryView of entryViews) {
|
||||||
|
const { callout: calloutEl, positions } = entryView
|
||||||
|
if (positions) {
|
||||||
|
const { entryTop, callout } = positions
|
||||||
|
|
||||||
|
if (shouldApplyVisibility) {
|
||||||
|
applyEntryVisibility(entryView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position the main wrapper, if it's moved
|
||||||
|
if (entryView.previousPositions?.entryTop !== entryTop) {
|
||||||
|
entryView.box.style.top = entryTop + overflowTop + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryView.indicator) {
|
||||||
|
entryView.indicator.style.top = entryTop + overflowTop + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position the callout element
|
||||||
|
if (entryView.previousPositions?.callout.top !== callout.top) {
|
||||||
|
calloutEl.style.top = callout.top + overflowTop + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function PositionedEntries({
|
function PositionedEntries({
|
||||||
|
@ -106,10 +208,9 @@ function PositionedEntries({
|
||||||
useReviewPanelValueContext()
|
useReviewPanelValueContext()
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||||
const { reviewPanelOpen } = useLayoutContext()
|
const { reviewPanelOpen } = useLayoutContext()
|
||||||
const animationTimerRef = useRef<number | null>(null)
|
|
||||||
const previousLayoutInfoRef = useRef(initialLayoutInfo)
|
const previousLayoutInfoRef = useRef(initialLayoutInfo)
|
||||||
|
|
||||||
const layout = () => {
|
const layout = (animate = true) => {
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return
|
return
|
||||||
|
@ -150,7 +251,7 @@ function PositionedEntries({
|
||||||
const previousPositions = previousLayoutInfoRef.current?.positions.find(
|
const previousPositions = previousLayoutInfoRef.current?.positions.find(
|
||||||
pos => pos.entryId === entryId
|
pos => pos.entryId === entryId
|
||||||
)?.positions
|
)?.positions
|
||||||
const visible = Boolean(entry.screenPos)
|
const hasScreenPos = Boolean(entry.screenPos)
|
||||||
entryViews.push({
|
entryViews.push({
|
||||||
entryId,
|
entryId,
|
||||||
wrapper,
|
wrapper,
|
||||||
|
@ -158,7 +259,7 @@ function PositionedEntries({
|
||||||
box,
|
box,
|
||||||
callout,
|
callout,
|
||||||
layout: layoutElement,
|
layout: layoutElement,
|
||||||
visible,
|
hasScreenPos,
|
||||||
height: 0,
|
height: 0,
|
||||||
entry,
|
entry,
|
||||||
previousPositions,
|
previousPositions,
|
||||||
|
@ -184,29 +285,26 @@ function PositionedEntries({
|
||||||
// - Measure the height of each entry
|
// - Measure the height of each entry
|
||||||
// - Move each entry without animation to their original position
|
// - Move each entry without animation to their original position
|
||||||
// relative to the editor content
|
// relative to the editor content
|
||||||
// - In the next animation frame, re-enable animation and position each
|
// - Re-enable animation and position each entry
|
||||||
// entry
|
|
||||||
//
|
//
|
||||||
// The idea is to batch DOM reads and writes to avoid layout thrashing. In
|
// The idea is to batch DOM reads and writes to avoid layout thrashing. In
|
||||||
// this case, the best we can do is a write phase, a read phase then a
|
// this case, the best we can do is a write phase, a read phase then a
|
||||||
// final write phase.
|
// final write phase.
|
||||||
// See https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/
|
// See https://web.dev/avoid-large-complex-layouts-and-layout-thrashing/
|
||||||
|
|
||||||
// First, update visibility for each entry that needs it
|
// First, update display for each entry that needs it
|
||||||
for (const entryView of entryViews) {
|
hideOrShowEntries(entryViews)
|
||||||
entryView.wrapper.classList.toggle('rp-entry-hidden', !entryView.visible)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, measure the height of each entry
|
// Next, measure the height of each entry
|
||||||
for (const entryView of entryViews) {
|
for (const entryView of entryViews) {
|
||||||
if (entryView.visible) {
|
if (entryView.hasScreenPos) {
|
||||||
entryView.height = entryView.layout.offsetHeight
|
entryView.height = entryView.layout.offsetHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate positions for all visible entries, starting by calculating the
|
// Calculate positions for all positioned entries, starting by calculating
|
||||||
// entry to put in its desired position and anchor everything else around.
|
// which entry to put in its desired position and anchor everything else
|
||||||
// If there is an explicitly focused entry, use that.
|
// around. If there is an explicitly focused entry, use that.
|
||||||
let focusedEntryIndex = entryViews.findIndex(view => view.entry.focused)
|
let focusedEntryIndex = entryViews.findIndex(view => view.entry.focused)
|
||||||
if (focusedEntryIndex === -1) {
|
if (focusedEntryIndex === -1) {
|
||||||
// There is no explicitly focused entry, so use the focused entry from the
|
// There is no explicitly focused entry, so use the focused entry from the
|
||||||
|
@ -216,30 +314,30 @@ function PositionedEntries({
|
||||||
previousLayoutInfoRef.current.focusedEntryIndex,
|
previousLayoutInfoRef.current.focusedEntryIndex,
|
||||||
entryViews.length - 1
|
entryViews.length - 1
|
||||||
)
|
)
|
||||||
// If the entry from the previous layout is not visible, fall back to the
|
// If the entry from the previous layout has no screen position, fall back
|
||||||
// first visible entry in the list
|
// to the first entry in the list that does.
|
||||||
if (!entryViews[focusedEntryIndex].visible) {
|
if (!entryViews[focusedEntryIndex].hasScreenPos) {
|
||||||
focusedEntryIndex = entryViews.findIndex(view => view.visible)
|
focusedEntryIndex = entryViews.findIndex(view => view.hasScreenPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no visible entry, bail out
|
// If there is no entry with a screen position, bail out
|
||||||
if (focusedEntryIndex === -1) {
|
if (focusedEntryIndex === -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusedEntryView = entryViews[focusedEntryIndex]
|
const focusedEntryView = entryViews[focusedEntryIndex]
|
||||||
if (!focusedEntryView.visible) {
|
if (!focusedEntryView.hasScreenPos) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the focused entry has no screenPos, we can't position other
|
// If the focused entry has no screenPos, we can't position other
|
||||||
// entryViews relative to it, so we position all other entryViews as
|
// entryViews relative to it, so we position all other entryViews as
|
||||||
// though the focused entry is at the top and the rest follow it
|
// though the focused entry is at the top and the rest follow it
|
||||||
const entryViewsAfter = focusedEntryView.visible
|
const entryViewsAfter = focusedEntryView.hasScreenPos
|
||||||
? entryViews.slice(focusedEntryIndex + 1)
|
? entryViews.slice(focusedEntryIndex + 1)
|
||||||
: [...entryViews]
|
: [...entryViews]
|
||||||
const entryViewsBefore = focusedEntryView.visible
|
const entryViewsBefore = focusedEntryView.hasScreenPos
|
||||||
? entryViews.slice(0, focusedEntryIndex).reverse() // Work through backwards, starting with the one just above
|
? entryViews.slice(0, focusedEntryIndex).reverse() // Work through backwards, starting with the one just above
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
@ -248,19 +346,11 @@ function PositionedEntries({
|
||||||
let lastEntryBottom = 0
|
let lastEntryBottom = 0
|
||||||
let firstEntryTop = 0
|
let firstEntryTop = 0
|
||||||
|
|
||||||
// Put the focused entry as close to where it wants to be as possible
|
// Put the focused entry as close as possible to where it wants to be
|
||||||
if (focusedEntryView.visible) {
|
if (focusedEntryView.hasScreenPos) {
|
||||||
const focusedEntryScreenPos = focusedEntryView.entry.screenPos
|
const focusedEntryScreenPos = focusedEntryView.entry.screenPos
|
||||||
const entryTop = Math.max(focusedEntryScreenPos.y, toolbarPaddedHeight)
|
const entryTop = Math.max(focusedEntryScreenPos.y, toolbarPaddedHeight)
|
||||||
const callout = calculateCalloutPosition(
|
updateEntryPositions(focusedEntryView, entryTop, lineHeight)
|
||||||
focusedEntryScreenPos,
|
|
||||||
entryTop,
|
|
||||||
lineHeight
|
|
||||||
)
|
|
||||||
focusedEntryView.positions = {
|
|
||||||
entryTop,
|
|
||||||
callout,
|
|
||||||
}
|
|
||||||
lastEntryBottom = entryTop + focusedEntryView.height
|
lastEntryBottom = entryTop + focusedEntryView.height
|
||||||
firstEntryTop = entryTop
|
firstEntryTop = entryTop
|
||||||
}
|
}
|
||||||
|
@ -292,80 +382,6 @@ function PositionedEntries({
|
||||||
// Calculate the new top overflow
|
// Calculate the new top overflow
|
||||||
const overflowTop = Math.max(0, toolbarPaddedHeight - firstEntryTop)
|
const overflowTop = Math.max(0, toolbarPaddedHeight - firstEntryTop)
|
||||||
|
|
||||||
// Position everything where it was before, taking into account the new top
|
|
||||||
// overflow
|
|
||||||
const moveEntriesToInitialPosition = () => {
|
|
||||||
// Prevent CSS animation of position for this phase
|
|
||||||
container.classList.add('no-animate')
|
|
||||||
for (const entryView of entryViews) {
|
|
||||||
const { callout: calloutEl, positions } = entryView
|
|
||||||
if (positions) {
|
|
||||||
const { entryTop, callout } = positions
|
|
||||||
|
|
||||||
// Position the main wrapper in its original position, if it had
|
|
||||||
// one, or its new position otherwise
|
|
||||||
const entryTopInitial = entryView.previousPositions
|
|
||||||
? entryView.previousPositions.entryTop
|
|
||||||
: entryTop
|
|
||||||
|
|
||||||
css(entryView.box, {
|
|
||||||
top: entryTopInitial + overflowTop + 'px',
|
|
||||||
// The entry element is invisible by default, to avoid flickering
|
|
||||||
// when positioning for the first time. Here we make sure it becomes
|
|
||||||
// visible after having a "top" value.
|
|
||||||
visibility: 'visible',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (entryView.indicator) {
|
|
||||||
entryView.indicator.style.top = entryTopInitial + overflowTop + 'px'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position the callout element in its original position, if it had
|
|
||||||
// one, or its new position otherwise
|
|
||||||
calloutEl.classList.toggle(
|
|
||||||
'rp-entry-callout-inverted',
|
|
||||||
callout.inverted
|
|
||||||
)
|
|
||||||
const calloutTopInitial = entryView.previousPositions
|
|
||||||
? entryView.previousPositions.callout.top
|
|
||||||
: callout.top
|
|
||||||
|
|
||||||
css(calloutEl, {
|
|
||||||
top: calloutTopInitial + overflowTop + 'px',
|
|
||||||
height: callout.height + 'px',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveToFinalPositions = () => {
|
|
||||||
// Re-enable CSS animation of position for this phase
|
|
||||||
container.classList.remove('no-animate')
|
|
||||||
|
|
||||||
for (const entryView of entryViews) {
|
|
||||||
const { callout: calloutEl, positions } = entryView
|
|
||||||
if (positions) {
|
|
||||||
const { entryTop, callout } = positions
|
|
||||||
|
|
||||||
// Position the main wrapper, if it's moved
|
|
||||||
if (entryView.previousPositions?.entryTop !== entryTop) {
|
|
||||||
entryView.box.style.top = entryTop + overflowTop + 'px'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entryView.indicator) {
|
|
||||||
entryView.indicator.style.top = entryTop + overflowTop + 'px'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position the callout element
|
|
||||||
if (entryView.previousPositions?.callout.top !== callout.top) {
|
|
||||||
calloutEl.style.top = callout.top + overflowTop + 'px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
animationTimerRef.current = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether the positions of any entry have changed since the last
|
// Check whether the positions of any entry have changed since the last
|
||||||
// layout
|
// layout
|
||||||
const positions = entryViews.map(
|
const positions = entryViews.map(
|
||||||
|
@ -386,10 +402,13 @@ function PositionedEntries({
|
||||||
|
|
||||||
const height = lastEntryBottom + navPaddedHeight
|
const height = lastEntryBottom + navPaddedHeight
|
||||||
const heightChanged = height !== previousLayoutInfoRef.current.height
|
const heightChanged = height !== previousLayoutInfoRef.current.height
|
||||||
|
const isMoveRequired = positionsChanged || overflowTopChanged
|
||||||
|
|
||||||
// Move entries into their initial positions
|
// Move entries into their initial positions, if animating, avoiding
|
||||||
if (positionsChanged || overflowTopChanged) {
|
// animation until the final animated move
|
||||||
moveEntriesToInitialPosition()
|
if (animate && isMoveRequired) {
|
||||||
|
container.classList.add('no-animate')
|
||||||
|
moveEntriesToInitialPosition(entryViews, overflowTop)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inform the editor of the new top overflow and/or height if either has
|
// Inform the editor of the new top overflow and/or height if either has
|
||||||
|
@ -409,8 +428,20 @@ function PositionedEntries({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the final move
|
// Do the final move
|
||||||
if (positionsChanged || overflowTopChanged) {
|
if (isMoveRequired) {
|
||||||
moveToFinalPositions()
|
if (animate) {
|
||||||
|
container.classList.remove('no-animate')
|
||||||
|
moveEntriesToFinalPositions(entryViews, overflowTop, false)
|
||||||
|
} else {
|
||||||
|
container.classList.add('no-animate')
|
||||||
|
moveEntriesToFinalPositions(entryViews, overflowTop, true)
|
||||||
|
|
||||||
|
// Force reflow now to ensure that entries are moved without animation
|
||||||
|
// eslint-disable-next-line no-void
|
||||||
|
void container.offsetHeight
|
||||||
|
|
||||||
|
container.classList.remove('no-animate')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
previousLayoutInfoRef.current = {
|
previousLayoutInfoRef.current = {
|
||||||
|
@ -427,7 +458,7 @@ function PositionedEntries({
|
||||||
if (event.detail.force) {
|
if (event.detail.force) {
|
||||||
previousLayoutInfoRef.current = initialLayoutInfo
|
previousLayoutInfoRef.current = initialLayoutInfo
|
||||||
}
|
}
|
||||||
layout()
|
layout(event.detail.animate)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -437,7 +468,7 @@ function PositionedEntries({
|
||||||
dispatchReviewPanelLayout()
|
dispatchReviewPanelLayout()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Ensure layout is performed after opening or closing the review panel
|
// Ensure a full layout is performed after opening or closing the review panel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
previousLayoutInfoRef.current = initialLayoutInfo
|
previousLayoutInfoRef.current = initialLayoutInfo
|
||||||
}, [reviewPanelOpen])
|
}, [reviewPanelOpen])
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
DocId,
|
DocId,
|
||||||
MainDocument,
|
MainDocument,
|
||||||
} from '../../../../../../../types/project-settings'
|
} from '../../../../../../../types/project-settings'
|
||||||
|
import { dispatchReviewPanelLayout } from '../../../extensions/changes/change-manager'
|
||||||
|
|
||||||
/* eslint-disable no-use-before-define */
|
/* eslint-disable no-use-before-define */
|
||||||
export interface ReviewPanelState {
|
export interface ReviewPanelState {
|
||||||
|
@ -49,7 +50,9 @@ export interface ReviewPanelState {
|
||||||
}
|
}
|
||||||
updaterFns: {
|
updaterFns: {
|
||||||
handleSetSubview: (subView: SubView) => void
|
handleSetSubview: (subView: SubView) => void
|
||||||
handleLayoutChange: (force?: boolean) => void
|
handleLayoutChange: (
|
||||||
|
...args: Parameters<typeof dispatchReviewPanelLayout>
|
||||||
|
) => void
|
||||||
gotoEntry: (docId: DocId, entryOffset: number) => void
|
gotoEntry: (docId: DocId, entryOffset: number) => void
|
||||||
resolveComment: (docId: DocId, entryId: ThreadId) => void
|
resolveComment: (docId: DocId, entryId: ThreadId) => void
|
||||||
deleteComment: (threadId: ThreadId, commentId: CommentId) => void
|
deleteComment: (threadId: ThreadId, commentId: CommentId) => void
|
||||||
|
|
|
@ -2,8 +2,9 @@ import { trackChangesAnnotation } from '../realtime'
|
||||||
import { clearChangeMarkers, buildChangeMarkers } from '../track-changes'
|
import { clearChangeMarkers, buildChangeMarkers } from '../track-changes'
|
||||||
import {
|
import {
|
||||||
setVerticalOverflow,
|
setVerticalOverflow,
|
||||||
updateSetsVerticalOverflow,
|
|
||||||
editorVerticalTopPadding,
|
editorVerticalTopPadding,
|
||||||
|
updateChangesTopPadding,
|
||||||
|
updateSetsVerticalOverflow,
|
||||||
} from '../vertical-overflow'
|
} from '../vertical-overflow'
|
||||||
import { EditorSelection, EditorState } from '@codemirror/state'
|
import { EditorSelection, EditorState } from '@codemirror/state'
|
||||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
|
@ -31,23 +32,53 @@ export const dispatchEditorEvent = (type: string, payload?: unknown) => {
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dispatchReviewPanelLayout = (force = false) => {
|
const dispatchReviewPanelLayoutImmediately = ({
|
||||||
|
force = false,
|
||||||
|
animate = true,
|
||||||
|
} = {}) => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('review-panel:layout', { detail: { force } })
|
new CustomEvent('review-panel:layout', { detail: { force, animate } })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduleDispatchReviewPanelLayout = debounce(
|
const scheduleDispatchReviewPanelLayout = debounce(
|
||||||
dispatchReviewPanelLayout,
|
dispatchReviewPanelLayoutImmediately,
|
||||||
10
|
10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param force If true, forces the entries to be repositioned
|
||||||
|
* @param animate
|
||||||
|
* @param async If true, calls are briefly delayed and debounced
|
||||||
|
*/
|
||||||
|
export const dispatchReviewPanelLayout = ({
|
||||||
|
force = false,
|
||||||
|
animate = true,
|
||||||
|
async = false,
|
||||||
|
} = {}) => {
|
||||||
|
if (async) {
|
||||||
|
scheduleDispatchReviewPanelLayout({ force, animate })
|
||||||
|
} else {
|
||||||
|
dispatchReviewPanelLayoutImmediately({ force, animate })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type ChangeManager = {
|
export type ChangeManager = {
|
||||||
initialize: () => void
|
initialize: () => void
|
||||||
handleUpdate: (update: ViewUpdate) => void
|
handleUpdate: (update: ViewUpdate) => void
|
||||||
destroy: () => void
|
destroy: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateType =
|
||||||
|
| 'edit'
|
||||||
|
| 'selectionChange'
|
||||||
|
| 'geometryChange'
|
||||||
|
| 'viewportChange'
|
||||||
|
| 'acceptChanges'
|
||||||
|
| 'rejectChanges'
|
||||||
|
| 'trackedChangesChange'
|
||||||
|
| 'topPaddingChange'
|
||||||
|
|
||||||
export const createChangeManager = (
|
export const createChangeManager = (
|
||||||
view: EditorView,
|
view: EditorView,
|
||||||
currentDoc: CurrentDoc
|
currentDoc: CurrentDoc
|
||||||
|
@ -58,7 +89,13 @@ export const createChangeManager = (
|
||||||
*
|
*
|
||||||
* Returns a boolean indicating whether the visibility of any entry has changed
|
* Returns a boolean indicating whether the visibility of any entry has changed
|
||||||
*/
|
*/
|
||||||
const recalculateScreenPositions = (entries?: Record<string, any>) => {
|
const recalculateScreenPositions = ({
|
||||||
|
entries,
|
||||||
|
updateType,
|
||||||
|
}: {
|
||||||
|
entries?: Record<string, any>
|
||||||
|
updateType: UpdateType
|
||||||
|
}) => {
|
||||||
const contentRect = view.contentDOM.getBoundingClientRect()
|
const contentRect = view.contentDOM.getBoundingClientRect()
|
||||||
|
|
||||||
const { doc } = view.state
|
const { doc } = view.state
|
||||||
|
@ -88,8 +125,9 @@ export const createChangeManager = (
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.screenPos = { y: y + offsetTop, height, editorPaddingTop }
|
entry.screenPos = { y: y + offsetTop, height, editorPaddingTop }
|
||||||
|
entry.inViewport = true
|
||||||
} else {
|
} else {
|
||||||
entry.screenPos = null
|
entry.inViewport = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allVisible) {
|
if (allVisible) {
|
||||||
|
@ -114,7 +152,7 @@ export const createChangeManager = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return visibilityChanged
|
return { visibilityChanged, updateType }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -303,15 +341,29 @@ export const createChangeManager = (
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'recalculate-screen-positions': {
|
case 'recalculate-screen-positions': {
|
||||||
const changed = recalculateScreenPositions(payload)
|
const { visibilityChanged, updateType } =
|
||||||
if (changed) {
|
recalculateScreenPositions(payload)
|
||||||
|
if (visibilityChanged) {
|
||||||
dispatchEditorEvent('track-changes:visibility_changed')
|
dispatchEditorEvent('track-changes:visibility_changed')
|
||||||
}
|
}
|
||||||
// Ensure the layout is updated once the review panel entries have
|
// Ensure the layout is updated once the review panel entries have
|
||||||
// updated in the React review panel. The use of a timeout is bad but
|
// updated in the React review panel. The use of a timeout is bad but
|
||||||
// the timings are a bit of a mess and will be improved when the review
|
// the timings are a bit of a mess and will be improved when the review
|
||||||
// panel state is migrated away from Angular
|
// panel state is migrated away from Angular. Entries are not animated
|
||||||
scheduleDispatchReviewPanelLayout()
|
// into position when scrolling, or when the editor geometry changes
|
||||||
|
// (e.g. because the window has been resized), or when the top padding
|
||||||
|
// is adjusted
|
||||||
|
const nonAnimatingUpdateTypes: UpdateType[] = [
|
||||||
|
'viewportChange',
|
||||||
|
'geometryChange',
|
||||||
|
'topPaddingChange',
|
||||||
|
]
|
||||||
|
const animate = !nonAnimatingUpdateTypes.includes(updateType)
|
||||||
|
dispatchReviewPanelLayout({
|
||||||
|
async: true,
|
||||||
|
animate,
|
||||||
|
force: false, // updateType === 'geometryChange',
|
||||||
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +373,7 @@ export const createChangeManager = (
|
||||||
broadcastChange()
|
broadcastChange()
|
||||||
// Dispatch a focus:changed event to force the Angular controller to
|
// Dispatch a focus:changed event to force the Angular controller to
|
||||||
// reassemble the list of entries without bulk actions
|
// reassemble the list of entries without bulk actions
|
||||||
dispatchFocusChangedEvent(view.state)
|
scheduleDispatchFocusChanged(view.state, 'acceptChanges')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +382,7 @@ export const createChangeManager = (
|
||||||
broadcastChange()
|
broadcastChange()
|
||||||
// Dispatch a focus:changed event to force the Angular controller to
|
// Dispatch a focus:changed event to force the Angular controller to
|
||||||
// reassemble the list of entries without bulk actions
|
// reassemble the list of entries without bulk actions
|
||||||
dispatchFocusChangedEvent(view.state)
|
scheduleDispatchFocusChanged(view.state, 'rejectChanges')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -368,17 +420,23 @@ export const createChangeManager = (
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'sizes': {
|
case 'sizes': {
|
||||||
|
const editorFullContentHeight = view.contentDOM.clientHeight
|
||||||
// the content height and top overflow of the review panel
|
// the content height and top overflow of the review panel
|
||||||
const { height, overflowTop } = payload
|
const { height, overflowTop } = payload
|
||||||
// the difference between the review panel height and the editor content height
|
// the difference between the review panel height and the editor content height
|
||||||
const heightDiff = height + overflowTop - view.contentDOM.clientHeight
|
const heightDiff = height + overflowTop - editorFullContentHeight
|
||||||
// the height of the block added at the top of the editor to match the review panel
|
// the height of the block added at the top of the editor to match the review panel
|
||||||
const topPadding = editorVerticalTopPadding(view)
|
const topPadding = editorVerticalTopPadding(view)
|
||||||
|
const bottomPadding = view.documentPadding.bottom
|
||||||
|
const contentHeight =
|
||||||
|
editorFullContentHeight - (topPadding + bottomPadding)
|
||||||
|
const newBottomPadding = height - contentHeight
|
||||||
|
|
||||||
if (overflowTop !== topPadding || heightDiff !== 0) {
|
if (overflowTop !== topPadding || heightDiff !== 0) {
|
||||||
view.dispatch(
|
view.dispatch(
|
||||||
setVerticalOverflow({
|
setVerticalOverflow({
|
||||||
top: overflowTop,
|
top: overflowTop,
|
||||||
bottom: heightDiff + view.documentPadding.bottom,
|
bottom: newBottomPadding,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -396,13 +454,27 @@ export const createChangeManager = (
|
||||||
* tell the review panel to update.
|
* tell the review panel to update.
|
||||||
*
|
*
|
||||||
* @param state object
|
* @param state object
|
||||||
|
* @param updateType UpdateType
|
||||||
*/
|
*/
|
||||||
const dispatchFocusChangedEvent = debounce((state: EditorState) => {
|
const dispatchFocusChangedImmediately = (
|
||||||
|
state: EditorState,
|
||||||
|
updateType: UpdateType
|
||||||
|
) => {
|
||||||
// TODO: multiple selections?
|
// TODO: multiple selections?
|
||||||
const { from, to, empty } = state.selection.main
|
const { from, to, empty } = state.selection.main
|
||||||
|
|
||||||
dispatchEditorEvent('focus:changed', { from, to, empty })
|
dispatchEditorEvent('focus:changed', {
|
||||||
}, 50)
|
from,
|
||||||
|
to,
|
||||||
|
empty,
|
||||||
|
updateType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleDispatchFocusChanged = debounce(
|
||||||
|
dispatchFocusChangedImmediately,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the editor is scrolled, tell the review panel so it can scroll in sync.
|
* When the editor is scrolled, tell the review panel so it can scroll in sync.
|
||||||
|
@ -478,15 +550,25 @@ export const createChangeManager = (
|
||||||
broadcastChange()
|
broadcastChange()
|
||||||
},
|
},
|
||||||
handleUpdate(update: ViewUpdate) {
|
handleUpdate(update: ViewUpdate) {
|
||||||
if (update.geometryChanged && !update.docChanged) {
|
const changesTopPadding = updateChangesTopPadding(update)
|
||||||
|
const {
|
||||||
|
geometryChanged,
|
||||||
|
viewportChanged,
|
||||||
|
docChanged,
|
||||||
|
focusChanged,
|
||||||
|
selectionSet,
|
||||||
|
} = update
|
||||||
|
const setsVerticalOverflow = updateSetsVerticalOverflow(update)
|
||||||
|
const ignoringGeometryChanges = Date.now() < ignoreGeometryChangesUntil
|
||||||
|
|
||||||
|
if (geometryChanged && !docChanged && !ignoringGeometryChanges) {
|
||||||
broadcastChange()
|
broadcastChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateSetsVerticalOverflow(update)) {
|
if (
|
||||||
ignoreGeometryChangesUntil = Date.now() + 50 // ignore changes for 50ms
|
!setsVerticalOverflow &&
|
||||||
} else if (
|
(geometryChanged || viewportChanged) &&
|
||||||
(update.geometryChanged || update.viewportChanged) &&
|
ignoringGeometryChanges
|
||||||
Date.now() < ignoreGeometryChangesUntil
|
|
||||||
) {
|
) {
|
||||||
// Ignore a change to the editor geometry or viewport that occurs immediately after
|
// Ignore a change to the editor geometry or viewport that occurs immediately after
|
||||||
// an update to the vertical padding because otherwise it triggers
|
// an update to the vertical padding because otherwise it triggers
|
||||||
|
@ -495,14 +577,25 @@ export const createChangeManager = (
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (changesTopPadding) {
|
||||||
update.docChanged ||
|
scheduleDispatchFocusChanged(update.state, 'topPaddingChange')
|
||||||
update.focusChanged ||
|
} else if (docChanged) {
|
||||||
update.viewportChanged ||
|
scheduleDispatchFocusChanged(update.state, 'edit')
|
||||||
update.selectionSet ||
|
} else if (focusChanged || selectionSet) {
|
||||||
update.geometryChanged
|
scheduleDispatchFocusChanged(update.state, 'selectionChange')
|
||||||
) {
|
} else if (viewportChanged && !geometryChanged) {
|
||||||
dispatchFocusChangedEvent(update.state)
|
// It's better to respond immediately to a viewport change, which
|
||||||
|
// happens when scrolling, and have previously unpositioned entries
|
||||||
|
// appear immediately rather than risk a delay due to debouncing
|
||||||
|
dispatchFocusChangedImmediately(update.state, 'viewportChange')
|
||||||
|
} else if (geometryChanged) {
|
||||||
|
scheduleDispatchFocusChanged(update.state, 'geometryChange')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until after updating the review panel layout before starting the
|
||||||
|
// interval during which to ignore geometry update
|
||||||
|
if (setsVerticalOverflow) {
|
||||||
|
ignoreGeometryChangesUntil = Date.now() + 50
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|
|
@ -235,6 +235,13 @@ export function updateSetsVerticalOverflow(update: ViewUpdate): boolean {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateChangesTopPadding(update: ViewUpdate): boolean {
|
||||||
|
return (
|
||||||
|
update.state.field(overflowPaddingState).top !==
|
||||||
|
update.startState.field(overflowPaddingState).top
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function editorVerticalTopPadding(view: EditorView): number {
|
export function editorVerticalTopPadding(view: EditorView): number {
|
||||||
return view.state.field(overflowPaddingState).top
|
return view.state.field(overflowPaddingState).top
|
||||||
}
|
}
|
||||||
|
|
|
@ -554,7 +554,14 @@ export default App.controller(
|
||||||
const entries = updateEntries(doc_id)
|
const entries = updateEntries(doc_id)
|
||||||
|
|
||||||
$scope.$broadcast('review-panel:recalculate-screen-positions')
|
$scope.$broadcast('review-panel:recalculate-screen-positions')
|
||||||
dispatchReviewPanelEvent('recalculate-screen-positions', entries)
|
dispatchReviewPanelEvent('recalculate-screen-positions', {
|
||||||
|
entries,
|
||||||
|
updateType: 'trackedChangesChange',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure that watchers, such as the React-based review panel component,
|
||||||
|
// are informed of the changes to entries
|
||||||
|
ide.$scope.$apply()
|
||||||
|
|
||||||
return ide.$scope.$broadcast('review-panel:layout')
|
return ide.$scope.$broadcast('review-panel:layout')
|
||||||
})
|
})
|
||||||
|
@ -565,7 +572,13 @@ export default App.controller(
|
||||||
|
|
||||||
$scope.$on(
|
$scope.$on(
|
||||||
'editor:focus:changed',
|
'editor:focus:changed',
|
||||||
function (e, selection_offset_start, selection_offset_end, selection) {
|
function (
|
||||||
|
e,
|
||||||
|
selection_offset_start,
|
||||||
|
selection_offset_end,
|
||||||
|
selection,
|
||||||
|
updateType = null
|
||||||
|
) {
|
||||||
const doc_id = $scope.editor.open_doc_id
|
const doc_id = $scope.editor.open_doc_id
|
||||||
const entries = getDocEntries(doc_id)
|
const entries = getDocEntries(doc_id)
|
||||||
// All selected changes will be added to this array.
|
// All selected changes will be added to this array.
|
||||||
|
@ -655,7 +668,10 @@ export default App.controller(
|
||||||
|
|
||||||
$scope.$broadcast('review-panel:recalculate-screen-positions')
|
$scope.$broadcast('review-panel:recalculate-screen-positions')
|
||||||
|
|
||||||
dispatchReviewPanelEvent('recalculate-screen-positions', entries)
|
dispatchReviewPanelEvent('recalculate-screen-positions', {
|
||||||
|
entries,
|
||||||
|
updateType,
|
||||||
|
})
|
||||||
|
|
||||||
// Ensure that watchers, such as the React-based review panel component,
|
// Ensure that watchers, such as the React-based review panel component,
|
||||||
// are informed of the changes to entries
|
// are informed of the changes to entries
|
||||||
|
@ -1264,6 +1280,11 @@ export default App.controller(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ide.$scope.reviewPanel.commentThreads = threads
|
ide.$scope.reviewPanel.commentThreads = threads
|
||||||
|
// Update entries so that the view has up-to-date information about
|
||||||
|
// the entries when handling the loaded_threads events, which avoids
|
||||||
|
// thrashing
|
||||||
|
updateEntries($scope.editor.open_doc_id)
|
||||||
|
|
||||||
dispatchReviewPanelEvent('loaded_threads')
|
dispatchReviewPanelEvent('loaded_threads')
|
||||||
return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
|
return $timeout(() => ide.$scope.$broadcast('review-panel:layout'))
|
||||||
})
|
})
|
||||||
|
@ -1346,8 +1367,14 @@ export default App.controller(
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'focus:changed': {
|
case 'focus:changed': {
|
||||||
const { from, to, empty } = payload
|
const { from, to, empty, updateType } = payload
|
||||||
$scope.$broadcast('editor:focus:changed', from, to, !empty)
|
$scope.$broadcast(
|
||||||
|
'editor:focus:changed',
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
!empty,
|
||||||
|
updateType
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ function AutoExpandingTextArea({
|
||||||
...rest
|
...rest
|
||||||
}: AutoExpandingTextAreaProps) {
|
}: AutoExpandingTextAreaProps) {
|
||||||
const ref = useRef<HTMLTextAreaElement>(null)
|
const ref = useRef<HTMLTextAreaElement>(null)
|
||||||
const isFirstResizeRef = useRef(true)
|
const previousHeightRef = useRef<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !onResize || !('ResizeObserver' in window)) {
|
if (!ref.current || !onResize || !('ResizeObserver' in window)) {
|
||||||
|
@ -46,12 +46,16 @@ function AutoExpandingTextArea({
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
// Ignore the resize that is triggered when the element is first
|
// Ignore the resize that is triggered when the element is first
|
||||||
// inserted into the DOM
|
// inserted into the DOM
|
||||||
if (isFirstResizeRef.current) {
|
if (!ref.current) {
|
||||||
isFirstResizeRef.current = false
|
return
|
||||||
} else {
|
}
|
||||||
|
const newHeight = ref.current.offsetHeight
|
||||||
|
const heightChanged = newHeight !== previousHeightRef.current
|
||||||
|
previousHeightRef.current = newHeight
|
||||||
|
if (heightChanged) {
|
||||||
// Prevent errors like "ResizeObserver loop completed with undelivered
|
// Prevent errors like "ResizeObserver loop completed with undelivered
|
||||||
// notifications" that occur if onResize does something complicated.
|
// notifications" that occur if onResize triggers another repaint. The
|
||||||
// The cost of this is that onResize lags one frame behind, but it's
|
// cost of this is that onResize lags one frame behind, but it's
|
||||||
// unlikely to matter.
|
// unlikely to matter.
|
||||||
|
|
||||||
// Wrap onResize to prevent extra parameters being passed
|
// Wrap onResize to prevent extra parameters being passed
|
||||||
|
|
|
@ -1275,7 +1275,7 @@ button when (@is-overleaf-light = true) {
|
||||||
|
|
||||||
.rp-entry-list-react {
|
.rp-entry-list-react {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rp-state-current-file & {
|
.rp-state-current-file & {
|
||||||
|
|
|
@ -11,6 +11,7 @@ interface ReviewPanelBaseEntry {
|
||||||
offset: number
|
offset: number
|
||||||
screenPos: ReviewPanelEntryScreenPos
|
screenPos: ReviewPanelEntryScreenPos
|
||||||
visible: boolean
|
visible: boolean
|
||||||
|
inViewport: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReviewPanelInsertOrDeleteEntry {
|
interface ReviewPanelInsertOrDeleteEntry {
|
||||||
|
|
Loading…
Reference in a new issue