| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import {
- EditorSelection,
- Range,
- StateEffect,
- StateField,
- } from '@codemirror/state'
- import { Decoration, EditorView, WidgetType } from '@codemirror/view'
- import { undo } from '@codemirror/commands'
- import { ancestorNodeOfType } from '../../utils/tree-operations/ancestors'
- import ReactDOM from 'react-dom'
- import { PastedContentMenu } from '../../components/paste-html/pasted-content-menu'
- import { SplitTestProvider } from '../../../../shared/context/split-test-context'
- export type PastedContent = { latex: string; text: string }
- const pastedContentEffect = StateEffect.define<{
- content: PastedContent
- formatted: boolean
- }>()
- export const insertPastedContent = (
- view: EditorView,
- { latex, text }: PastedContent
- ) =>
- view.state.changeByRange(range => {
- // avoid pasting formatted content into a math container
- if (ancestorNodeOfType(view.state, range.anchor, '$MathContainer')) {
- return {
- range: EditorSelection.cursor(range.from + text.length),
- changes: { from: range.from, to: range.to, insert: text },
- }
- }
- return {
- range: EditorSelection.cursor(range.from + latex.length),
- changes: { from: range.from, to: range.to, insert: latex },
- }
- })
- export const storePastedContent = (
- content: PastedContent,
- formatted: boolean
- ) => ({
- effects: pastedContentEffect.of({ content, formatted }),
- })
- const pastedContentTheme = EditorView.baseTheme({
- '.ol-cm-pasted-content-menu-toggle': {
- background: 'none',
- borderRadius: '8px',
- border: '1px solid rgb(125, 125, 125)',
- margin: '0 4px',
- opacity: '0.7',
- '&:hover': {
- opacity: '1',
- },
- },
- '.ol-cm-pasted-content-menu-popover': {
- maxWidth: 'unset',
- '& .popover-content': {
- padding: 0,
- },
- },
- '&dark .ol-cm-pasted-content-menu-popover': {
- background: 'rgba(0, 0, 0)',
- },
- '.ol-cm-pasted-content-menu': {
- display: 'flex',
- flexDirection: 'column',
- boxSizing: 'border-box',
- fontSize: '14px',
- fontFamily: '"Lato", sans-serif',
- },
- '.ol-cm-pasted-content-menu-item': {
- border: 'none',
- background: 'none',
- padding: '8px 16px',
- width: '100%',
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- whiteSpace: 'nowrap',
- gap: '12px',
- '&[aria-disabled="true"]': {
- color: 'rgba(125, 125, 125, 0.5)',
- },
- '&:hover': {
- backgroundColor: 'rgba(125, 125, 125, 0.2)',
- },
- },
- '.ol-cm-pasted-content-menu-item-label': {
- flex: 1,
- textAlign: 'left',
- },
- '.ol-cm-pasted-content-menu-item-shortcut': {
- textAlign: 'right',
- },
- })
- export const pastedContent = StateField.define<{
- content: PastedContent
- formatted: boolean
- selection: EditorSelection
- } | null>({
- create() {
- return null
- },
- update(value, tr) {
- if (tr.docChanged) {
- // TODO: exclude remote changes (if they don't intersect with changed ranges)?
- value = null
- } else {
- for (const effect of tr.effects) {
- if (effect.is(pastedContentEffect)) {
- value = {
- ...effect.value,
- selection: tr.state.selection,
- }
- }
- }
- }
- return value
- },
- provide(field) {
- return [
- EditorView.decorations.compute([field], state => {
- const value = state.field(field)
- if (!value) {
- return Decoration.none
- }
- const decorations: Range<Decoration>[] = []
- const { content, selection, formatted } = value
- decorations.push(
- Decoration.widget({
- widget: new PastedContentMenuWidget(content, formatted),
- side: 1,
- }).range(selection.main.to)
- )
- return Decoration.set(decorations, true)
- }),
- pastedContentTheme,
- ]
- },
- })
- class PastedContentMenuWidget extends WidgetType {
- constructor(
- private pastedContent: PastedContent,
- private formatted: boolean
- ) {
- super()
- }
- toDOM(view: EditorView) {
- const element = document.createElement('span')
- ReactDOM.render(
- <SplitTestProvider>
- <PastedContentMenu
- insertPastedContent={this.insertPastedContent}
- view={view}
- formatted={this.formatted}
- pastedContent={this.pastedContent}
- />
- </SplitTestProvider>,
- element
- )
- return element
- }
- insertPastedContent(
- view: EditorView,
- pastedContent: PastedContent,
- formatted: boolean
- ) {
- undo(view)
- view.dispatch(
- insertPastedContent(view, {
- latex: formatted ? pastedContent.latex : pastedContent.text,
- text: pastedContent.text,
- })
- )
- view.dispatch(storePastedContent(pastedContent, formatted))
- view.focus()
- }
- eq(widget: PastedContentMenuWidget) {
- return (
- widget.pastedContent === this.pastedContent &&
- widget.formatted === this.formatted
- )
- }
- }
|