فهرست منبع

[visual] Add decoration for footnotes (#13428)

GitOrigin-RevId: b0c8e475e9d8b4a19977a48b615596b88ce65797
Alf Eaton 3 سال پیش
والد
کامیت
8402081d9b

+ 38 - 0
services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts

@@ -47,6 +47,7 @@ import getMeta from '../../../../utils/meta'
 import { EditableGraphicsWidget } from './visual-widgets/editable-graphics'
 import { EditableGraphicsWidget } from './visual-widgets/editable-graphics'
 import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
 import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
 import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
 import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
+import { FootnoteWidget } from './visual-widgets/footnote'
 
 
 type Options = {
 type Options = {
   fileTreeManager: {
   fileTreeManager: {
@@ -857,6 +858,43 @@ export const atomicDecorations = (options: Options) => {
                   )
                   )
                   return false
                   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') {
               } else if (commandName === '\\LaTeX') {
                 if (shouldDecorate(state, nodeRef)) {
                 if (shouldDecorate(state, nodeRef)) {
                   decorations.push(
                   decorations.push(

+ 18 - 0
services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts

@@ -360,4 +360,22 @@ export const visualTheme = EditorView.theme({
     top: '18px',
     top: '18px',
     right: '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',
+    },
+  },
 })
 })

+ 35 - 0
services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/footnote.ts

@@ -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'
+  }
+}

+ 44 - 0
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-readonly.spec.tsx

@@ -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')
+  })
+})

+ 13 - 1
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx

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