mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #12370 from overleaf/td-history-basic-editor
Basic document diff viewer for history view GitOrigin-RevId: a6919cc003f8f7fad24126e407313013cf489b63
This commit is contained in:
parent
95c8a1aeea
commit
8ee8f2ded3
4 changed files with 354 additions and 0 deletions
|
@ -0,0 +1,92 @@
|
|||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import withErrorBoundary from '../../../../infrastructure/error-boundary'
|
||||
import { ErrorBoundaryFallback } from '../../../../shared/components/error-boundary-fallback'
|
||||
import { EditorState, Extension } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { lineNumbers } from '../../../../../../modules/source-editor/frontend/js/extensions/line-numbers'
|
||||
import { indentationMarkers } from '@replit/codemirror-indentation-markers'
|
||||
import { highlights, setHighlightsEffect } from '../extensions/highlights'
|
||||
import useScopeValue from '../../../../shared/hooks/use-scope-value'
|
||||
import {
|
||||
FontFamily,
|
||||
LineHeight,
|
||||
OverallTheme,
|
||||
} from '../../../../../../modules/source-editor/frontend/js/extensions/theme'
|
||||
import { theme, Options } from '../extensions/theme'
|
||||
import { indentUnit } from '@codemirror/language'
|
||||
|
||||
interface Range {
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
|
||||
type HighlightType = 'addition' | 'deletion'
|
||||
|
||||
export interface Highlight {
|
||||
label: string
|
||||
hue: number
|
||||
range: Range
|
||||
type: HighlightType
|
||||
}
|
||||
|
||||
function extensions(themeOptions: Options): Extension[] {
|
||||
return [
|
||||
EditorView.editable.of(false),
|
||||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
indentUnit.of(' '),
|
||||
indentationMarkers({ hideFirstIndent: true, highlightActiveBlock: false }),
|
||||
highlights(),
|
||||
theme(themeOptions),
|
||||
]
|
||||
}
|
||||
|
||||
const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
|
||||
doc,
|
||||
highlights,
|
||||
}) => {
|
||||
const [fontFamily] = useScopeValue<FontFamily>('settings.fontFamily')
|
||||
const [fontSize] = useScopeValue<number>('settings.fontSize')
|
||||
const [lineHeight] = useScopeValue<LineHeight>('settings.lineHeight')
|
||||
const [overallTheme] = useScopeValue<OverallTheme>('settings.overallTheme')
|
||||
|
||||
const [state] = useState(() => {
|
||||
return EditorState.create({
|
||||
doc,
|
||||
extensions: extensions({
|
||||
fontFamily,
|
||||
fontSize,
|
||||
lineHeight,
|
||||
overallTheme,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const viewRef = useRef<EditorView | null>(null)
|
||||
if (viewRef.current === null) {
|
||||
viewRef.current = new EditorView({
|
||||
state,
|
||||
})
|
||||
}
|
||||
|
||||
const view = viewRef.current
|
||||
|
||||
// Append the editor view dom to the container node when mounted
|
||||
const containerRef = useCallback(
|
||||
node => {
|
||||
if (node) {
|
||||
node.appendChild(view.dom)
|
||||
}
|
||||
},
|
||||
[view]
|
||||
)
|
||||
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: doc },
|
||||
effects: setHighlightsEffect.of(highlights),
|
||||
})
|
||||
|
||||
return <div ref={containerRef} style={{ height: '100%' }} />
|
||||
}
|
||||
|
||||
export default withErrorBoundary(DocumentDiffViewer, ErrorBoundaryFallback)
|
|
@ -0,0 +1,99 @@
|
|||
import { StateEffect, StateField } from '@codemirror/state'
|
||||
import { Decoration, EditorView, hoverTooltip, Tooltip } from '@codemirror/view'
|
||||
import { Highlight } from '../editor/document-diff-viewer'
|
||||
|
||||
export const setHighlightsEffect = StateEffect.define<Highlight[]>()
|
||||
|
||||
function highlightToMarker(highlight: Highlight) {
|
||||
const className =
|
||||
highlight.type === 'addition' ? 'ol-addition-marker' : 'ol-deletion-marker'
|
||||
const { from, to } = highlight.range
|
||||
|
||||
return Decoration.mark({
|
||||
class: className,
|
||||
attributes: {
|
||||
style: `--hue: ${highlight.hue}`,
|
||||
},
|
||||
}).range(from, to)
|
||||
}
|
||||
|
||||
const theme = EditorView.baseTheme({
|
||||
'.ol-addition-marker': {
|
||||
backgroundColor: 'hsl(var(--hue), 70%, 85%)',
|
||||
},
|
||||
'.ol-deletion-marker': {
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
'&.overall-theme-dark .ol-deletion-marker': {
|
||||
color: 'hsl(var(--hue), 100%, 60%)',
|
||||
},
|
||||
'&.overall-theme-light .ol-deletion-marker': {
|
||||
color: 'hsl(var(--hue), 70%, 40%)',
|
||||
},
|
||||
'.cm-tooltip-hover': {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
},
|
||||
'.ol-cm-highlight-tooltip': {
|
||||
backgroundColor: 'hsl(var(--hue), 70%, 50%)',
|
||||
borderRadius: '4px',
|
||||
padding: '4px',
|
||||
color: '#fff',
|
||||
},
|
||||
})
|
||||
|
||||
const tooltip = (view: EditorView, pos: number, side: any): Tooltip | null => {
|
||||
const highlights = view.state.field(highlightsField)
|
||||
const highlight = highlights.find(highlight => {
|
||||
const { from, to } = highlight.range
|
||||
return !(
|
||||
pos < from ||
|
||||
pos > to ||
|
||||
(pos === from && side < 0) ||
|
||||
(pos === to && side > 0)
|
||||
)
|
||||
})
|
||||
|
||||
if (!highlight) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
pos: highlight.range.from,
|
||||
end: highlight.range.to,
|
||||
above: true,
|
||||
create: () => {
|
||||
const dom = document.createElement('div')
|
||||
dom.classList.add('ol-cm-highlight-tooltip')
|
||||
dom.style.setProperty('--hue', highlight.hue.toString())
|
||||
dom.textContent = highlight.label
|
||||
|
||||
return { dom }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const highlightsField = StateField.define<Highlight[]>({
|
||||
create() {
|
||||
return []
|
||||
},
|
||||
update(highlightMarkers, tr) {
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setHighlightsEffect)) {
|
||||
return effect.value
|
||||
}
|
||||
}
|
||||
return highlightMarkers
|
||||
},
|
||||
provide: field => [
|
||||
EditorView.decorations.from(field, highlights =>
|
||||
Decoration.set(highlights.map(highlight => highlightToMarker(highlight)))
|
||||
),
|
||||
theme,
|
||||
hoverTooltip(tooltip, { hoverTime: 0 }),
|
||||
],
|
||||
})
|
||||
|
||||
export function highlights() {
|
||||
return highlightsField
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { EditorView } from '@codemirror/view'
|
||||
import { Compartment } from '@codemirror/state'
|
||||
|
||||
export type FontFamily = 'monaco' | 'lucida'
|
||||
export type LineHeight = 'compact' | 'normal' | 'wide'
|
||||
export type OverallTheme = '' | 'light-'
|
||||
|
||||
export type Options = {
|
||||
fontSize: number
|
||||
fontFamily: FontFamily
|
||||
lineHeight: LineHeight
|
||||
overallTheme: OverallTheme
|
||||
}
|
||||
|
||||
const optionsThemeConf = new Compartment()
|
||||
|
||||
export const theme = (options: Options) => [
|
||||
optionsThemeConf.of(createThemeFromOptions(options)),
|
||||
]
|
||||
|
||||
export const lineHeights: Record<LineHeight, number> = {
|
||||
compact: 1.33,
|
||||
normal: 1.6,
|
||||
wide: 2,
|
||||
}
|
||||
|
||||
const fontFamilies: Record<FontFamily, string[]> = {
|
||||
monaco: ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'monospace'],
|
||||
lucida: ['Lucida Console', 'Source Code Pro', 'monospace'],
|
||||
}
|
||||
|
||||
const createThemeFromOptions = ({
|
||||
fontSize = 12,
|
||||
fontFamily = 'monaco',
|
||||
lineHeight = 'normal',
|
||||
overallTheme = '',
|
||||
}: Options) => {
|
||||
// Theme styles that depend on settings
|
||||
return [
|
||||
EditorView.editorAttributes.of({
|
||||
class: overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light',
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&.cm-editor': {
|
||||
'--font-size': `${fontSize}px`,
|
||||
'--source-font-family': fontFamilies[fontFamily]?.join(', '),
|
||||
'--line-height': lineHeights[lineHeight],
|
||||
},
|
||||
'.cm-content': {
|
||||
fontSize: 'var(--font-size)',
|
||||
fontFamily: 'var(--source-font-family)',
|
||||
lineHeight: 'var(--line-height)',
|
||||
color: '#000',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
fontSize: 'var(--font-size)',
|
||||
lineHeight: 'var(--line-height)',
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
// Set variables for tooltips, which are outside the editor
|
||||
'--font-size': `${fontSize}px`,
|
||||
'--source-font-family': fontFamilies[fontFamily]?.join(', '),
|
||||
// NOTE: fontFamily is not set here, as most tooltips use the UI font
|
||||
fontSize: 'var(--font-size)',
|
||||
},
|
||||
'.cm-lineNumbers': {
|
||||
fontFamily: 'var(--source-font-family)',
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import DocumentDiffViewer, {
|
||||
Highlight,
|
||||
} from '../../js/features/history/components/editor/document-diff-viewer'
|
||||
import React from 'react'
|
||||
|
||||
export default {
|
||||
title: 'History / Document Diff Viewer',
|
||||
component: DocumentDiffViewer,
|
||||
decorators: [
|
||||
ScopeDecorator,
|
||||
(Story: React.ComponentType) => (
|
||||
<div style={{ height: '90vh' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Highlights = () => {
|
||||
return <DocumentDiffViewer doc={content} highlights={highlights} />
|
||||
}
|
||||
|
||||
const highlights: Highlight[] = [
|
||||
{
|
||||
type: 'addition',
|
||||
range: { from: 3, to: 10 },
|
||||
hue: 200,
|
||||
label: 'Added by Wombat on Monday',
|
||||
},
|
||||
{
|
||||
type: 'deletion',
|
||||
range: { from: 15, to: 25 },
|
||||
hue: 200,
|
||||
label: 'Deleted by Wombat on Monday',
|
||||
},
|
||||
{
|
||||
type: 'addition',
|
||||
range: { from: 100, to: 400 },
|
||||
hue: 200,
|
||||
label: 'Added by Wombat on Friday',
|
||||
},
|
||||
]
|
||||
|
||||
const content = `\\documentclass{article}
|
||||
|
||||
% Language setting
|
||||
% Replace \`english' with e.g. \`spanish' to change the document language
|
||||
\\usepackage[english]{babel}
|
||||
|
||||
% Set page size and margins
|
||||
% Replace \`letterpaper' with \`a4paper' for UK/EU standard size
|
||||
\\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry}
|
||||
|
||||
% Useful packages
|
||||
\\usepackage{amsmath}
|
||||
\\usepackage{graphicx}
|
||||
\\usepackage[colorlinks=true, allcolors=blue]{hyperref}
|
||||
|
||||
\\title{Your Paper}
|
||||
\\author{You}
|
||||
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
|
||||
\\begin{abstract}
|
||||
Your abstract.
|
||||
\\end{abstract}
|
||||
|
||||
\\section{Introduction}
|
||||
|
||||
Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started.
|
||||
|
||||
Once you're familiar with the editor, you can find various project settings in the Overleaf menu, accessed via the button in the very top left of the editor. To view tutorials, user guides, and further documentation, please visit our \\href{https://www.overleaf.com/learn}{help library}, or head to our plans page to \\href{https://www.overleaf.com/user/subscription/plans}{choose your plan}.
|
||||
|
||||
\\begin{enumerate}
|
||||
\\item The labels consists of sequential numbers
|
||||
\\begin{itemize}
|
||||
\\item The individual entries are indicated with a black dot, a so-called bullet
|
||||
\\item The text in the entries may be of any length
|
||||
\\begin{description}
|
||||
\\item[Note:] I would like to describe something here
|
||||
\\item[Caveat!] And give a warning
|
||||
\\end{description}
|
||||
\\end{itemize}
|
||||
\\item The numbers starts at 1 with each use of the \\text{enumerate} environment
|
||||
\\end{enumerate}
|
||||
|
||||
\\bibliographystyle{alpha}
|
||||
\\bibliography{sample}
|
||||
|
||||
\\end{document}`
|
Loading…
Reference in a new issue