mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Move source editor out of module (#12457)
* Update Copybara options in preparation for open-sourcing the source editor * Move files * Update paths * Remove source-editor module and checks for its existence * Explicitly mention CM6 license in files that contain code adapted from CM6 GitOrigin-RevId: 89b7cc2b409db01ad103198ccbd1b126ab56349b
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { BlockInfo, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'
|
||||
import { throttle } from 'lodash'
|
||||
import customLocalStorage from '../../../infrastructure/local-storage'
|
||||
import {
|
||||
EditorSelection,
|
||||
StateEffect,
|
||||
Text,
|
||||
TransactionSpec,
|
||||
} from '@codemirror/state'
|
||||
import { toggleVisualEffect } from './visual/visual'
|
||||
|
||||
const buildStorageKey = (docId: string) => `doc.position.${docId}`
|
||||
|
||||
type LineInfo = {
|
||||
first: BlockInfo
|
||||
middle: BlockInfo
|
||||
}
|
||||
|
||||
export const scrollPosition = ({
|
||||
currentDoc: { doc_id: docId },
|
||||
}: {
|
||||
currentDoc: { doc_id: string }
|
||||
}) => {
|
||||
// store lineInfo for use on unload, when the DOM has already been unmounted
|
||||
let lineInfo: LineInfo
|
||||
|
||||
const scrollHandler = throttle(
|
||||
(event, view) => {
|
||||
// exclude a scroll event with no target, which happens when switching docs
|
||||
if (event.target === view.scrollDOM) {
|
||||
lineInfo = calculateLineInfo(view)
|
||||
dispatchScrollPosition(lineInfo, view)
|
||||
}
|
||||
},
|
||||
// long enough to capture intent, but short enough that the selected heading in the outline appears current
|
||||
120,
|
||||
{ trailing: true }
|
||||
)
|
||||
|
||||
return [
|
||||
// store/dispatch scroll position
|
||||
ViewPlugin.define(
|
||||
view => {
|
||||
const unloadListener = () => {
|
||||
if (lineInfo) {
|
||||
storeScrollPosition(lineInfo, view, docId)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('unload', unloadListener)
|
||||
|
||||
return {
|
||||
update: (update: ViewUpdate) => {
|
||||
for (const tr of update.transactions) {
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(toggleVisualEffect)) {
|
||||
// store the scroll position when switching between source and rich text
|
||||
if (lineInfo) {
|
||||
storeScrollPosition(lineInfo, view, docId)
|
||||
}
|
||||
} else if (effect.is(restoreScrollPositionEffect)) {
|
||||
// restore the scroll position
|
||||
window.setTimeout(() => {
|
||||
view.dispatch(scrollStoredLineToTop(tr.state.doc, docId))
|
||||
window.dispatchEvent(
|
||||
new Event('editor:scroll-position-restored')
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
scrollHandler.cancel()
|
||||
window.removeEventListener('unload', unloadListener)
|
||||
unloadListener()
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
eventHandlers: {
|
||||
scroll: scrollHandler,
|
||||
},
|
||||
}
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
const restoreScrollPositionEffect = StateEffect.define()
|
||||
|
||||
export const restoreScrollPosition = () => {
|
||||
return {
|
||||
effects: restoreScrollPositionEffect.of(null),
|
||||
}
|
||||
}
|
||||
|
||||
const calculateLineInfo = (view: EditorView) => {
|
||||
// the top of the scrollDOM element relative to the top of the document
|
||||
const { top, height } = view.scrollDOM.getBoundingClientRect()
|
||||
const distanceFromDocumentTop = top - view.documentTop
|
||||
|
||||
return {
|
||||
first: view.lineBlockAtHeight(distanceFromDocumentTop),
|
||||
// top plus half the height of the scrollDOM element
|
||||
middle: view.lineBlockAtHeight(distanceFromDocumentTop + height / 2),
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch the middle visible line number (for the outline)
|
||||
const dispatchScrollPosition = (lineInfo: LineInfo, view: EditorView) => {
|
||||
const middleVisibleLine = view.state.doc.lineAt(lineInfo.middle.from).number
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('scroll:editor:update', {
|
||||
detail: middleVisibleLine,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// store the scroll position (first visible line number, for restoring on load)
|
||||
const storeScrollPosition = (
|
||||
lineInfo: LineInfo,
|
||||
view: EditorView,
|
||||
docId: string
|
||||
) => {
|
||||
const key = buildStorageKey(docId)
|
||||
const data = customLocalStorage.getItem(key)
|
||||
const firstVisibleLine = view.state.doc.lineAt(lineInfo.first.from).number
|
||||
|
||||
customLocalStorage.setItem(key, { ...data, firstVisibleLine })
|
||||
}
|
||||
|
||||
// restore the scroll position using the stored first visible line number
|
||||
const scrollStoredLineToTop = (doc: Text, docId: string): TransactionSpec => {
|
||||
try {
|
||||
const key = buildStorageKey(docId)
|
||||
const data = customLocalStorage.getItem(key)
|
||||
|
||||
// restore the scroll position to its original position, or the last line of the document
|
||||
const firstVisibleLine = Math.min(data?.firstVisibleLine ?? 1, doc.lines)
|
||||
|
||||
const line = doc.line(firstVisibleLine)
|
||||
|
||||
const selectionRange = EditorSelection.cursor(line.from)
|
||||
|
||||
return {
|
||||
effects: EditorView.scrollIntoView(selectionRange, {
|
||||
y: 'start',
|
||||
yMargin: 0,
|
||||
}),
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore invalid line number
|
||||
console.error(e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user