Преглед изворни кода

Merge pull request #12917 from overleaf/td-history-highlight-tooltip-improvements

History migration: Rework change marker tooltips to match Angular version behaviour

GitOrigin-RevId: c34e42db6c310521d04690de41f3bb81c219bfbc
Tim Down пре 3 година
родитељ
комит
f8a2d85126

+ 122 - 39
services/web/frontend/js/features/history/extensions/highlights.ts

@@ -10,19 +10,22 @@ import {
   Decoration,
   Decoration,
   DecorationSet,
   DecorationSet,
   EditorView,
   EditorView,
-  hoverTooltip,
+  showTooltip,
   Tooltip,
   Tooltip,
+  ViewPlugin,
   WidgetType,
   WidgetType,
 } from '@codemirror/view'
 } from '@codemirror/view'
 import { Highlight, HighlightType } from '../services/types/doc'
 import { Highlight, HighlightType } from '../services/types/doc'
 
 
 export const setHighlightsEffect = StateEffect.define<Highlight[]>()
 export const setHighlightsEffect = StateEffect.define<Highlight[]>()
+const ADDITION_MARKER_CLASS = 'ol-cm-addition-marker'
+const DELETION_MARKER_CLASS = 'ol-cm-deletion-marker'
 
 
 function highlightToMarker(highlight: Highlight) {
 function highlightToMarker(highlight: Highlight) {
   const className =
   const className =
     highlight.type === 'addition'
     highlight.type === 'addition'
-      ? 'ol-cm-addition-marker'
-      : 'ol-cm-deletion-marker'
+      ? ADDITION_MARKER_CLASS
+      : DELETION_MARKER_CLASS
   const { from, to } = highlight.range
   const { from, to } = highlight.range
 
 
   return Decoration.mark({
   return Decoration.mark({
@@ -69,18 +72,21 @@ function highlightedLines(highlights: Highlight[], state: EditorState) {
 }
 }
 
 
 const theme = EditorView.baseTheme({
 const theme = EditorView.baseTheme({
-  '.ol-cm-addition-marker': {
+  ['.' + ADDITION_MARKER_CLASS]: {
     paddingTop: 'var(--half-leading)',
     paddingTop: 'var(--half-leading)',
     paddingBottom: 'var(--half-leading)',
     paddingBottom: 'var(--half-leading)',
     backgroundColor: 'hsl(var(--hue), 70%, 85%)',
     backgroundColor: 'hsl(var(--hue), 70%, 85%)',
   },
   },
-  '.ol-cm-deletion-marker': {
+  ['.' + DELETION_MARKER_CLASS]: {
     textDecoration: 'line-through',
     textDecoration: 'line-through',
     color: 'hsl(var(--hue), 70%, 40%)',
     color: 'hsl(var(--hue), 70%, 40%)',
   },
   },
-  '.cm-tooltip-hover': {
+  '.cm-tooltip': {
     backgroundColor: 'transparent',
     backgroundColor: 'transparent',
     borderWidth: 0,
     borderWidth: 0,
+    // Prevent a tooltip getting in the way of hovering over a line that it
+    // obscures
+    pointerEvents: 'none',
   },
   },
   '.ol-cm-highlight-tooltip': {
   '.ol-cm-highlight-tooltip': {
     backgroundColor: 'hsl(var(--hue), 70%, 50%)',
     backgroundColor: 'hsl(var(--hue), 70%, 50%)',
@@ -93,25 +99,9 @@ const theme = EditorView.baseTheme({
   },
   },
 })
 })
 
 
-const tooltip = (view: EditorView, pos: number, side: any): Tooltip | null => {
-  const highlights = view.state.field(highlightDecorationsField).highlights
-  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
-  }
-
+function createHighlightTooltip(pos: number, highlight: Highlight) {
   return {
   return {
-    pos: highlight.range.from,
-    end: highlight.range.to,
+    pos,
     above: true,
     above: true,
     create: () => {
     create: () => {
       const dom = document.createElement('div')
       const dom = document.createElement('div')
@@ -124,6 +114,105 @@ const tooltip = (view: EditorView, pos: number, side: any): Tooltip | null => {
   }
   }
 }
 }
 
 
+const setHighlightTooltipEffect = StateEffect.define<Tooltip | null>()
+
+const tooltipField = StateField.define<Tooltip | null>({
+  create() {
+    return null
+  },
+
+  update(tooltip, transaction) {
+    for (const effect of transaction.effects) {
+      if (effect.is(setHighlightTooltipEffect)) {
+        return effect.value
+      }
+    }
+    return tooltip
+  },
+
+  provide: field => showTooltip.from(field),
+})
+
+function highlightAtPos(state: EditorState, pos: number) {
+  const highlights = state.field(highlightDecorationsField).highlights
+  return highlights.find(highlight => {
+    const { from, to } = highlight.range
+    return pos >= from && pos <= to
+  })
+}
+
+const highlightTooltipPlugin = ViewPlugin.fromClass(
+  class {
+    private lastTooltipPos: number | null = null
+
+    // eslint-disable-next-line no-useless-constructor
+    constructor(readonly view: EditorView) {}
+
+    setHighlightTooltip(tooltip: Tooltip | null) {
+      this.view.dispatch({
+        effects: setHighlightTooltipEffect.of(tooltip),
+      })
+    }
+
+    setTooltipFromEvent(event: MouseEvent) {
+      const pos = this.view.posAtCoords({ x: event.clientX, y: event.clientY })
+      if (pos !== this.lastTooltipPos) {
+        let tooltip = null
+        if (pos !== null) {
+          const highlight = highlightAtPos(this.view.state, pos)
+          if (highlight) {
+            tooltip = createHighlightTooltip(pos, highlight)
+          }
+        }
+        this.setHighlightTooltip(tooltip)
+        this.lastTooltipPos = pos
+      }
+    }
+
+    handleMouseMove(event: MouseEvent) {
+      this.setTooltipFromEvent(event)
+    }
+
+    startHover(event: MouseEvent, el: HTMLElement) {
+      const handleMouseMove = this.handleMouseMove.bind(this)
+      this.view.contentDOM.addEventListener('mousemove', handleMouseMove)
+
+      const handleMouseLeave = () => {
+        this.setHighlightTooltip(null)
+        this.lastTooltipPos = null
+        this.view.contentDOM.removeEventListener('mousemove', handleMouseMove)
+        el.removeEventListener('mouseleave', handleMouseLeave)
+      }
+
+      el.addEventListener('mouseleave', handleMouseLeave)
+      this.setTooltipFromEvent(event)
+    }
+  },
+  {
+    eventHandlers: {
+      mouseover(event) {
+        const el = event.target as HTMLElement
+        const classList = el.classList
+        if (
+          classList.contains(ADDITION_MARKER_CLASS) ||
+          classList.contains(DELETION_MARKER_CLASS) ||
+          // An empty line widget doesn't trigger a mouseover event, so detect
+          // an event on a line element that contains one instead
+          (classList.contains('cm-line') &&
+            el.querySelector(
+              `.ol-cm-empty-line-addition-marker, .ol-cm-empty-line-deletion-marker`
+            ))
+        ) {
+          this.startHover(event, el)
+        }
+      },
+    },
+    provide() {
+      return tooltipField
+    },
+  }
+)
+
 class EmptyLineAdditionMarkerWidget extends WidgetType {
 class EmptyLineAdditionMarkerWidget extends WidgetType {
   constructor(readonly hue: number) {
   constructor(readonly hue: number) {
     super()
     super()
@@ -131,7 +220,10 @@ class EmptyLineAdditionMarkerWidget extends WidgetType {
 
 
   toDOM(view: EditorView): HTMLElement {
   toDOM(view: EditorView): HTMLElement {
     const element = document.createElement('span')
     const element = document.createElement('span')
-    element.className = 'ol-cm-empty-line-addition-marker ol-cm-addition-marker'
+    element.classList.add(
+      'ol-cm-empty-line-addition-marker',
+      ADDITION_MARKER_CLASS
+    )
     element.style.setProperty('--hue', this.hue.toString())
     element.style.setProperty('--hue', this.hue.toString())
 
 
     return element
     return element
@@ -145,7 +237,10 @@ class EmptyLineDeletionMarkerWidget extends WidgetType {
 
 
   toDOM(view: EditorView): HTMLElement {
   toDOM(view: EditorView): HTMLElement {
     const element = document.createElement('span')
     const element = document.createElement('span')
-    element.className = 'ol-cm-empty-line-deletion-marker ol-deletion-marker'
+    element.classList.add(
+      'ol-cm-empty-line-deletion-marker',
+      DELETION_MARKER_CLASS
+    )
     element.style.setProperty('--hue', this.hue.toString())
     element.style.setProperty('--hue', this.hue.toString())
     element.textContent = ' '
     element.textContent = ' '
 
 
@@ -167,21 +262,9 @@ function createEmptyLineHighlightMarkers(lineStatuses: LineStatuses) {
           ? new EmptyLineAdditionMarkerWidget(highlight.hue)
           ? new EmptyLineAdditionMarkerWidget(highlight.hue)
           : new EmptyLineDeletionMarkerWidget(highlight.hue)
           : new EmptyLineDeletionMarkerWidget(highlight.hue)
 
 
-      // In order to make the hover tooltip appear for every empty line,
-      // position the widget after the position if this is the first empty line
-      // in a group or before it otherwise. Always using a value of 1 would
-      // mean that the last empty line in a group of more than one would not
-      // trigger the hover tooltip.
-      const side =
-        lineStatuses.get(lineStatus.line.number - 1)?.highlights[0]?.type ===
-        highlight.type
-          ? -1
-          : 1
-
       markers.push(
       markers.push(
         Decoration.widget({
         Decoration.widget({
           widget,
           widget,
-          side,
         }).range(lineStatus.line.from)
         }).range(lineStatus.line.from)
       )
       )
     }
     }
@@ -228,7 +311,7 @@ export const highlightDecorationsField =
         value => value.emptyLineHighlightMarkers
         value => value.emptyLineHighlightMarkers
       ),
       ),
       theme,
       theme,
-      hoverTooltip(tooltip, { hoverTime: 0 }),
+      highlightTooltipPlugin,
     ],
     ],
   })
   })
 
 

+ 21 - 8
services/web/test/frontend/features/history/components/document-diff-viewer.spec.tsx

@@ -95,13 +95,13 @@ describe('document diff viewer', function () {
     cy.get('@deletion').should('have.text', 'Language')
     cy.get('@deletion').should('have.text', 'Language')
 
 
     // Check hover tooltips
     // Check hover tooltips
-    cy.get('@addition').trigger('mousemove')
+    cy.get('@addition').trigger('mouseover')
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip')
     cy.get('.ol-cm-highlight-tooltip')
       .first()
       .first()
       .should('have.text', 'Added by Wombat on Monday')
       .should('have.text', 'Added by Wombat on Monday')
 
 
-    cy.get('@deletion').trigger('mousemove')
+    cy.get('@deletion').trigger('mouseover')
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip')
     cy.get('.ol-cm-highlight-tooltip')
       .first()
       .first()
@@ -145,26 +145,39 @@ End
     )
     )
 
 
     cy.get('.ol-cm-empty-line-addition-marker').should('have.length', 2)
     cy.get('.ol-cm-empty-line-addition-marker').should('have.length', 2)
-
     cy.get('.ol-cm-empty-line-deletion-marker').should('have.length', 1)
     cy.get('.ol-cm-empty-line-deletion-marker').should('have.length', 1)
-    cy.get('.ol-cm-empty-line-deletion-marker').first().as('deletion')
+
+    // For an empty line marker, we need to trigger mouseover on the containing
+    // line beause the marker itself does not trigger mouseover
+    cy.get('.ol-cm-empty-line-addition-marker')
+      .first()
+      .parent()
+      .as('firstAdditionLine')
+    cy.get('.ol-cm-empty-line-addition-marker')
+      .first()
+      .parent()
+      .as('lastAdditionLine')
+    cy.get('.ol-cm-empty-line-deletion-marker')
+      .last()
+      .parent()
+      .as('deletionLine')
 
 
     // Check hover tooltips
     // Check hover tooltips
-    cy.get('.ol-cm-empty-line-addition-marker').last().trigger('mousemove')
+    cy.get('@lastAdditionLine').trigger('mouseover')
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip')
     cy.get('.ol-cm-highlight-tooltip')
       .first()
       .first()
       .should('have.text', 'Added by Wombat on Monday')
       .should('have.text', 'Added by Wombat on Monday')
 
 
-    cy.get('.ol-cm-empty-line-addition-marker').last().trigger('mouseleave')
+    cy.get('@lastAdditionLine').trigger('mouseleave')
 
 
-    cy.get('.ol-cm-empty-line-addition-marker').first().trigger('mousemove')
+    cy.get('@firstAdditionLine').trigger('mouseover')
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip')
     cy.get('.ol-cm-highlight-tooltip')
       .first()
       .first()
       .should('have.text', 'Added by Wombat on Monday')
       .should('have.text', 'Added by Wombat on Monday')
 
 
-    cy.get('@deletion').trigger('mousemove')
+    cy.get('@deletionLine').trigger('mouseover')
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip').should('have.length', 1)
     cy.get('.ol-cm-highlight-tooltip')
     cy.get('.ol-cm-highlight-tooltip')
       .first()
       .first()