[visual] Add decoration for footnotes (#13428)

GitOrigin-RevId: b0c8e475e9d8b4a19977a48b615596b88ce65797
This commit is contained in:
Alf Eaton 2023-06-29 10:33:09 +01:00 committed by Copybot
parent 4d39c31acc
commit 8402081d9b
5 changed files with 148 additions and 1 deletions

View file

@ -47,6 +47,7 @@ import getMeta from '../../../../utils/meta'
import { EditableGraphicsWidget } from './visual-widgets/editable-graphics'
import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
import { FootnoteWidget } from './visual-widgets/footnote'
type Options = {
fileTreeManager: {
@ -857,6 +858,43 @@ export const atomicDecorations = (options: Options) => {
)
return false
}
} else if (
commandName === '\\footnote' ||
commandName === '\\endnote'
) {
if (textArgumentNode) {
if (
state.readOnly &&
selectionIntersects(state.selection, nodeRef)
) {
// a special case for a read-only document:
// always display the content, styled differently from the main content.
decorations.push(
...decorateArgumentBraces(
new BraceWidget(),
textArgumentNode,
nodeRef.from
),
Decoration.mark({
class: 'ol-cm-footnote ol-cm-footnote-view',
}).range(textArgumentNode.from, textArgumentNode.to)
)
} else {
if (shouldDecorate(state, nodeRef)) {
// collapse the footnote when the selection is outside it
decorations.push(
Decoration.replace({
widget: new FootnoteWidget(
commandName === '\\footnote'
? 'footnote'
: 'endnote'
),
}).range(nodeRef.from, nodeRef.to)
)
return false
}
}
}
} else if (commandName === '\\LaTeX') {
if (shouldDecorate(state, nodeRef)) {
decorations.push(

View file

@ -360,4 +360,22 @@ export const visualTheme = EditorView.theme({
top: '18px',
right: '18px',
},
'.ol-cm-footnote': {
display: 'inline-flex',
padding: '0 0.1em',
background: 'rgba(125, 125, 125, 0.25)',
borderRadius: '2px',
height: '1em',
cursor: 'pointer',
verticalAlign: 'text-top',
'&:not(.ol-cm-footnote-view):hover': {
background: 'rgba(125, 125, 125, 0.5)',
},
'&.ol-cm-footnote-view': {
height: 'auto',
verticalAlign: 'unset',
display: 'inline',
padding: '0 0.5em',
},
},
})

View file

@ -0,0 +1,35 @@
import { WidgetType } from '@codemirror/view'
type NoteType = 'footnote' | 'endnote'
const symbols: Record<NoteType, string> = {
footnote: '*',
endnote: '†',
}
export class FootnoteWidget extends WidgetType {
constructor(private type: NoteType = 'footnote') {
super()
}
toDOM() {
const element = document.createElement('span')
element.classList.add('ol-cm-footnote')
element.setAttribute('role', 'button')
element.innerHTML = symbols[this.type]
return element
}
eq(widget: FootnoteWidget) {
return this.type === widget.type
}
updateDOM(element: HTMLElement): boolean {
element.innerHTML = symbols[this.type]
return true
}
ignoreEvent(event: Event) {
return event.type !== 'mousedown' && event.type !== 'mouseup'
}
}

View file

@ -0,0 +1,44 @@
import { mockScope } from '../helpers/mock-scope'
import { EditorProviders } from '../../../helpers/editor-providers'
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
import { FC } from 'react'
const Container: FC = ({ children }) => (
<div style={{ width: 785, height: 785 }}>{children}</div>
)
describe('<CodeMirrorEditor/> in Visual mode with read-only permission', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
window.metaAttributesCache.set(
'ol-mathJax3Path',
'https://unpkg.com/mathjax@3.2.2/es5/tex-svg-full.js'
)
cy.interceptEvents()
cy.interceptSpelling()
})
it('decorates footnote content', function () {
const scope = mockScope('Foo \\footnote{Bar.} ')
scope.permissionsLevel = 'readOnly'
scope.editor.showVisual = true
cy.mount(
<Container>
<EditorProviders scope={scope}>
<CodemirrorEditor />
</EditorProviders>
</Container>
)
// wait for the content to be parsed and revealed
cy.get('.cm-content').should('have.css', 'opacity', '1')
// select the footnote, so it expands
cy.get('.ol-cm-footnote').click()
cy.get('.cm-line').eq(0).as('first-line')
cy.get('@first-line').should('contain', 'Foo')
cy.get('@first-line').should('contain', 'Bar')
})
})

View file

@ -10,7 +10,7 @@ const Container: FC = ({ children }) => (
<div style={{ width: 785, height: 785 }}>{children}</div>
)
describe('<CodeMirrorEditor/> in Rich Text mode', function () {
describe('<CodeMirrorEditor/> in Visual mode', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
window.metaAttributesCache.set(
@ -46,6 +46,10 @@ describe('<CodeMirrorEditor/> in Rich Text mode', function () {
cy.get('@first-line').click()
})
afterEach(function () {
window.metaAttributesCache.clear()
})
forEach(['LaTeX', 'TeX']).it('renders the %s logo', function (logo) {
cy.get('@first-line').type(`\\${logo}{{}}{Enter}`)
cy.get('@first-line').should('have.text', logo)
@ -374,6 +378,14 @@ describe('<CodeMirrorEditor/> in Rich Text mode', function () {
cy.get('.ol-cm-author').should('have.text', 'Author')
})
it('decorates footnotes', function () {
cy.get('@first-line').type('Foo \\footnote{{}Bar.} ')
cy.get('@first-line').should('contain', 'Foo')
cy.get('@first-line').should('not.contain', 'Bar')
cy.get('@first-line').type('{leftArrow}')
cy.get('@first-line').should('have.text', 'Foo \\footnote{Bar.} ')
})
// TODO: \input
// TODO: Math
// TODO: Abstract