overleaf/services/web/frontend/js/features/source-editor/extensions/vertical-overflow.ts
Alf Eaton 2304536844 Add review panel context providers and components (#19490)
* Tidy up review panel components

* Add ReviewPanel providers

* [web] new design for review panel track change (#19544)

* [web] new design for review panel track change

* fixed mini view

* mini icon style change

* fix icon size

* format date

* useRangesUserContext hook

* remove useRangesUserContext hook

* using full class names

* fix action icons hover

* change wording for tooltips

* added ReviewPanelChangeUser component

* Update header in new review panel

* Extract ReviewPanelTrackChangesMenuButton as a separate component

* Remove wrapper div

* Replace h2 with div for review panel label

* Rename ReviewPanelTools to ReviewPanelHeader

* Rename trackChangesExpanded -> trackChangesMenuExpanded

* Dont break memoisation of ReviewPanelTrackChangesMenuButton

* Fix the width of the track changes arrow icon

* Update how prop types are declared

* Remove new empty state from old review panel

* Add empty state to new review panel

* Add project members and owner to ChangesUsers context (#19624)

---------

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>

* Redesign comment entry in review panel (#19678)

* Redesign comment entry in review panel

* ReviewPanelCommentOptions component

* remove unused prop

* Tidying

* Add conditional import

* Optional changeManager

* Add more split test compatibility

* More split test compatibility

* Fixes

* Improve overview scrolling

* Fix overview scrolling

* Fix & simplify track changes toggle

* Fix overview scrolling

* Fix current file container

* ExpandableContent component for messages in review panel (#19738)

* ExpandableContent component for messages in review panel

* remove isExpanded dependancy

* Delete comment option for new review panel (#19772)

* Delete comment option for new review panel

* dont show thread warning if there are no replies

* fix hasReplies issue

* Implement initial collapsing overview files

* Fix positioning of overview panel

* Small styling changes

* Add count of unresolved comments and tracked chanegs

* More style adjustments

* Move review-panel-overview styles into css file

* Remove unused var

---------

Co-authored-by: Domagoj Kriskovic <dom.kriskovic@overleaf.com>
Co-authored-by: David Powell <david.powell@overleaf.com>
Co-authored-by: David <33458145+davidmcpowell@users.noreply.github.com>
GitOrigin-RevId: e67463443d541f88445a86eed5e2b6ec6040f9c7
2024-08-13 08:04:20 +00:00

251 lines
6.7 KiB
TypeScript

import {
Extension,
Facet,
StateEffect,
StateField,
TransactionSpec,
} from '@codemirror/state'
import {
Decoration,
EditorView,
ViewPlugin,
ViewUpdate,
WidgetType,
} from '@codemirror/view'
/**
* A custom extension which stores values for padding needed
* a) at the top and bottom of the editor, to match the height of the review panel, and
* b) at the bottom of the editor content, so the last line of the document can be scrolled to the top of the editor.
*/
export function verticalOverflow(): Extension {
return [
overflowPaddingState,
minimumBottomPaddingState,
bottomPadding,
topPadding,
contentAttributes,
topPaddingDecoration,
bottomPaddingPlugin,
topPaddingPlugin,
]
}
type VerticalPadding = { top: number; bottom: number }
const setOverflowPaddingEffect = StateEffect.define<VerticalPadding>()
// Store extra padding needed at the top and bottom of the editor to match the height of the review panel.
// The padding needs to allow enough space for tracked changes/comments at the top and/or bottom of the review panel.
const overflowPaddingState = StateField.define<VerticalPadding>({
create() {
return { top: 0, bottom: 0 }
},
update(value, tr) {
for (const effect of tr.effects) {
if (effect.is(setOverflowPaddingEffect)) {
const { top, bottom } = effect.value
// only update the state when the values actually change
if (top !== value.top || bottom !== value.bottom) {
value = { top, bottom }
}
}
}
return value
},
})
const setMinimumBottomPaddingEffect = StateEffect.define<number>()
// Store extra padding needed at the bottom of the editor content.
// The content must have a space at the bottom equivalent to the
// height of the editor content minus one line, so that the last
// line in the document can be scrolled to the top of the editor.
const minimumBottomPaddingState = StateField.define<number>({
create() {
return 0
},
update(value, tr) {
for (const effect of tr.effects) {
if (effect.is(setMinimumBottomPaddingEffect)) {
value = effect.value
}
}
return value
},
})
// Set scrollTop to counteract changes to the top padding.
// This view plugin is needed because the overflowPaddingState StateField doesn't have access to the view.
const topPaddingPlugin = ViewPlugin.define(view => {
let previousTop = 0
return {
update: update => {
const { top } = update.state.field(overflowPaddingState)
if (top !== previousTop) {
const diff = top - previousTop
if (diff < 0) {
// padding is decreasing, scroll now
view.scrollDOM.scrollTop += diff
} else {
// padding is increasing, scroll after it has been applied
view.requestMeasure({
key: 'vertical-overflow-scroll-top',
read() {
// do nothing
},
write(measure, view) {
view.scrollDOM.scrollTop += diff
},
})
}
previousTop = top
}
},
}
})
/**
* When the editor geometry changes, recalculate the amount of padding needed at
* the end of the doc: (the scrollDOM height - 1 line height).
* Adapted from the CodeMirror 6 scrollPastEnd extension, licensed under the MIT
* license:
* https://github.com/codemirror/view/blob/main/src/scrollpastend.ts
*/
const bottomPaddingPlugin = ViewPlugin.define(view => {
let previousHeight = 0
const measure = {
key: 'vertical-overflow-bottom-padding',
read(view: EditorView) {
return view.scrollDOM.clientHeight - view.defaultLineHeight
},
write(height: number, view: EditorView) {
if (height !== previousHeight) {
// dispatch must be wrapped in a timeout to avoid clashing with the current update
window.setTimeout(() =>
view.dispatch({
effects: setMinimumBottomPaddingEffect.of(height),
})
)
previousHeight = height
}
},
}
view.requestMeasure(measure)
return {
update: update => {
if (update.geometryChanged) {
update.view.requestMeasure(measure)
}
},
}
})
const topPaddingFacet = Facet.define<number, number>({
combine(values) {
return Math.max(0, ...values)
},
})
const topPadding = topPaddingFacet.from(overflowPaddingState, state => {
return state.top
})
const bottomPaddingFacet = Facet.define<number, number>({
combine(values) {
return Math.max(0, ...values)
},
})
const bottomPadding = bottomPaddingFacet.computeN(
[overflowPaddingState, minimumBottomPaddingState],
state => {
return [
state.field(minimumBottomPaddingState),
state.field(overflowPaddingState).bottom,
]
}
)
// Set a style attribute on the contentDOM containing the calculated bottom padding.
// This value will be concatenated with style values from any other extensions.
const contentAttributes = EditorView.contentAttributes.compute(
[bottomPaddingFacet],
state => {
const bottom = state.facet(bottomPaddingFacet)
const style = `padding-bottom: ${bottom}px;`
return { style }
}
)
class TopPaddingWidget extends WidgetType {
constructor(private readonly height: number) {
super()
this.height = height
}
toDOM(view: EditorView): HTMLElement {
const element = document.createElement('div')
element.style.height = this.height + 'px'
return element
}
get estimatedHeight() {
return this.height
}
eq(widget: TopPaddingWidget) {
return this.height === widget.height
}
updateDOM(element: HTMLElement, view: EditorView): boolean {
element.style.height = this.height + 'px'
view.requestMeasure()
return true
}
}
const topPaddingDecoration = EditorView.decorations.compute(
[topPaddingFacet],
state => {
const top = state.facet(topPaddingFacet)
return Decoration.set([
Decoration.widget({
widget: new TopPaddingWidget(top),
block: true,
}).range(0),
])
}
)
export function setVerticalOverflow(padding: VerticalPadding): TransactionSpec {
return {
effects: [setOverflowPaddingEffect.of(padding)],
}
}
export function updateSetsVerticalOverflow(update: ViewUpdate): boolean {
return update.transactions.some(tr => {
return tr.effects.some(effect => effect.is(setOverflowPaddingEffect))
})
}
export function updateChangesTopPadding(update: ViewUpdate): boolean {
return (
update.state.field(overflowPaddingState).top !==
update.startState.field(overflowPaddingState).top
)
}
export function editorVerticalTopPadding(view: EditorView): number {
return view.state.field(overflowPaddingState, false)?.top ?? 0
}
export function editorOverflowPadding(view: EditorView) {
return view.state.field(overflowPaddingState, false)
}