• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

EcrituresNumeriques / stylo / 18648081431

20 Oct 2025 09:36AM UTC coverage: 39.784% (+0.6%) from 39.162%
18648081431

push

github

web-flow
feat: commandes ack, dedi, endnote, epigraph, inlinequote, notepre, question, ans, quote-alt, sig, smallcaps et sponsor (#1718)

Co-authored-by: Thomas Parisot <thom4parisot@users.noreply.github.com>

613 of 860 branches covered (71.28%)

Branch coverage included in aggregate %.

141 of 243 new or added lines in 4 files covered. (58.02%)

6091 of 15991 relevant lines covered (38.09%)

2.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/front/src/components/collaborative/CollaborativeTextEditor.jsx
1
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
×
2
import { Helmet } from 'react-helmet'
×
3
import { useTranslation } from 'react-i18next'
×
4
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
×
5
import { MonacoBinding } from 'y-monaco'
×
NEW
6
import { Separator } from 'monaco-editor/esm/vs/base/common/actions.js'
×
NEW
7
import { StandaloneServices } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneServices.js';
×
NEW
8
import { IContextMenuService } from 'monaco-editor/esm/vs/platform/contextview/browser/contextView.js';
×
9

NEW
10
import { actions, bindAction, MetopesMenu } from './actions'
×
11
import { DiffEditor } from '@monaco-editor/react'
×
12
import throttle from 'lodash.throttle'
×
13

14
import { useArticleVersion, useEditableArticle } from '../../hooks/article.js'
×
15
import { useBibliographyCompletion } from '../../hooks/bibliography.js'
×
16
import { useCollaboration } from '../../hooks/collaboration.js'
×
17
import { useStyloExportPreview } from '../../hooks/stylo-export.js'
×
18
import defaultEditorOptions from '../Write/providers/monaco/options.js'
×
19
import { onDropIntoEditor } from '../Write/providers/monaco/support.js'
×
20

21
import Alert from '../molecules/Alert.jsx'
×
22
import Loading from '../molecules/Loading.jsx'
×
23
import MonacoEditor from '../molecules/MonacoEditor.jsx'
×
24
import CollaborativeEditorArticleHeader from './CollaborativeEditorArticleHeader.jsx'
×
25
import CollaborativeEditorWebSocketStatus from './CollaborativeEditorWebSocketStatus.jsx'
×
26

27
import styles from './CollaborativeTextEditor.module.scss'
×
NEW
28
import 'monaco-editor/esm/vs/base/browser/ui/codicons/codicon/codicon.css'
×
29

30
/**
31
 * @typedef {import('monaco-editor').editor.IStandaloneCodeEditor} IStandaloneCodeEditor
32
 * @typedef {import('monaco-editor')} monaco
33
 */
34

35
/**
36
 * @param {object} props
37
 * @param {string} props.articleId
38
 * @param {string|undefined} props.versionId
39
 * @param {'write' | 'compare' | 'preview'} props.mode
40
 * @returns {Element}
41
 */
42
export default function CollaborativeTextEditor({
×
43
  articleId,
×
44
  versionId,
×
45
  mode,
×
46
}) {
×
47
  const { yText, awareness, websocketStatus, dynamicStyles } = useCollaboration(
×
48
    { articleId, versionId }
×
49
  )
×
50
  const { t } = useTranslation('editor')
×
51

52
  const {
×
53
    version,
×
54
    error,
×
55
    isLoading: isVersionLoading,
×
56
  } = useArticleVersion({ versionId })
×
57
  const { provider: bibliographyCompletionProvider } =
×
58
    useBibliographyCompletion()
×
59
  const {
×
60
    article,
×
61
    bibliography,
×
62
    isLoading: isWorkingVersionLoading,
×
63
  } = useEditableArticle({
×
64
    articleId,
×
65
    versionId,
×
66
  })
×
67

68
  const { html: __html, isLoading: isPreviewLoading } = useStyloExportPreview({
×
69
    ...(mode === 'preview'
×
70
      ? {
×
71
          md_content: versionId ? version.md : yText?.toString(),
×
72
          yaml_content: versionId
×
73
            ? version.yaml
×
74
            : article?.workingVersion?.yaml,
×
75
          bib_content: versionId ? version.bib : article?.workingVersion?.bib,
×
76
        }
×
77
      : {}),
×
78
    with_toc: true,
×
79
    with_nocite: true,
×
80
    with_link_citations: true,
×
81
  })
×
82

83
  const dispatch = useDispatch()
×
84
  const editorRef = useRef(null)
×
85
  const editorCursorPosition = useSelector(
×
86
    (state) => state.editorCursorPosition,
×
87
    shallowEqual
×
88
  )
×
89

90
  const hasVersion = useMemo(() => !!versionId, [versionId])
×
91
  const isLoading =
×
92
    yText === null ||
×
93
    isPreviewLoading ||
×
94
    isWorkingVersionLoading ||
×
95
    isVersionLoading
×
96

97
  const options = useMemo(
×
98
    () => ({
×
99
      ...defaultEditorOptions,
×
100
      contextmenu: hasVersion ? false : websocketStatus === 'connected',
×
101
      readOnly: hasVersion ? true : websocketStatus !== 'connected',
×
102
      dropIntoEditor: {
×
103
        enabled: true,
×
104
      },
×
105
    }),
×
106
    [websocketStatus, hasVersion]
×
107
  )
×
108

109
  const updateArticleStructureAndStats = useCallback(
×
110
    throttle(
×
111
      ({ text: md }) => {
×
112
        dispatch({ type: 'UPDATE_ARTICLE_STATS', md })
×
113
        dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md })
×
114
      },
×
115
      250,
×
116
      { leading: false, trailing: true }
×
117
    ),
×
118
    []
×
119
  )
×
120

121
  const handleCollaborativeEditorDidMount = useCallback(
×
NEW
122
    (/** @type {IStandaloneCodeEditor} */ editor, /** @type {monaco} */ monaco) => {
×
123
      editorRef.current = editor
×
124

125
      editor.onDropIntoEditor(onDropIntoEditor(editor))
×
NEW
126
      const contextMenuService = StandaloneServices.get(IContextMenuService);
×
127

NEW
128
      editor.onContextMenu((e) => {
×
NEW
129
        e.event.preventDefault()
×
130

NEW
131
        return contextMenuService.showContextMenu({
×
NEW
132
          getAnchor: () => ({
×
NEW
133
            x: e.event.browserEvent.pageX,
×
NEW
134
            y: e.event.browserEvent.pageY
×
NEW
135
          }),
×
NEW
136
          getActions: () => [
×
NEW
137
            MetopesMenu({ editor, t }),
×
138
            // new Separator(),
NEW
139
          ],
×
NEW
140
        })
×
NEW
141
      })
×
142

143
      // // Command Palette commands
NEW
144
      const _bindAction = bindAction.bind(null, editor, t)
×
NEW
145
      editor.addAction(_bindAction(actions.acknowledgement))
×
146

147
      const completionProvider = bibliographyCompletionProvider.register(monaco)
×
148
      editor.onDidDispose(() => completionProvider.dispose())
×
149

150
      const model = editor.getModel()
×
151
      // Set EOL to LF otherwise it causes synchronization issues due to inconsistent EOL between Windows and Linux.
152
      // https://github.com/yjs/y-monaco/issues/27
153
      model.setEOL(monaco.editor.EndOfLineSequence.LF)
×
154
      if (yText && awareness) {
×
155
        new MonacoBinding(yText, model, new Set([editor]), awareness)
×
156
      }
×
157
    },
×
158
    [yText, awareness]
×
159
  )
×
160

161
  const handleEditorDidMount = useCallback((editor) => {
×
162
    editorRef.current = editor
×
163
  }, [])
×
164

165
  let timeoutId
×
166
  useEffect(() => {
×
167
    if (yText) {
×
168
      updateArticleStructureAndStats({ text: yText.toString() })
×
169
      yText.observe(function (yTextEvent, transaction) {
×
170
        dispatch({
×
171
          type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
172
          status: 'syncing',
×
173
        })
×
174
        if (timeoutId) {
×
175
          clearTimeout(timeoutId)
×
176
        }
×
177
        timeoutId = setTimeout(() => {
×
178
          dispatch({
×
179
            type: 'UPDATE_ARTICLE_WORKING_COPY_STATUS',
×
180
            status: 'synced',
×
181
          })
×
182
        }, 4000)
×
183

184
        updateArticleStructureAndStats({ text: yText.toString() })
×
185
      })
×
186
    }
×
187
  }, [articleId, versionId, yText])
×
188

189
  useEffect(() => {
×
190
    if (versionId) {
×
191
      dispatch({ type: 'UPDATE_ARTICLE_STATS', md: version.md })
×
192
      dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md: version.md })
×
193
    }
×
194
  }, [versionId])
×
195

196
  useEffect(() => {
×
197
    if (bibliography) {
×
198
      bibliographyCompletionProvider.bibTeXEntries = bibliography.entries
×
199
    }
×
200
  }, [bibliography])
×
201

202
  useEffect(() => {
×
203
    const line = editorCursorPosition.lineNumber
×
204
    const editor = editorRef.current
×
205
    editor?.focus()
×
206
    const endOfLineColumn = editor?.getModel()?.getLineMaxColumn(line + 1)
×
207
    editor?.setPosition({ lineNumber: line + 1, column: endOfLineColumn })
×
208
    editor?.revealLineNearTop(line + 1, 1) // smooth
×
209
  }, [editorRef, editorCursorPosition])
×
210

211
  if (isLoading) {
×
212
    return <Loading />
×
213
  }
×
214

215
  if (error) {
×
216
    return <Alert message={error.message} />
×
217
  }
×
218

219
  return (
×
220
    <>
×
221
      <style>{dynamicStyles}</style>
×
222
      <Helmet>
×
223
        <title>{article.title}</title>
×
224
      </Helmet>
×
225

226
      <CollaborativeEditorArticleHeader
×
227
        articleTitle={article.title}
×
228
        versionId={versionId}
×
229
      />
×
230

231
      <CollaborativeEditorWebSocketStatus
×
232
        className={styles.inlineStatus}
×
233
        status={websocketStatus}
×
234
      />
×
235

236
      {mode === 'preview' && (
×
237
        <section
×
238
          className={styles.previewPage}
×
239
          dangerouslySetInnerHTML={{ __html }}
×
240
        />
×
241
      )}
242

243
      {mode === 'compare' && (
×
244
        <div className={styles.collaborativeEditor}>
×
245
          <DiffEditor
×
246
            className={styles.editor}
×
247
            width={'100%'}
×
248
            height={'auto'}
×
249
            modified={article.workingVersion?.md}
×
250
            original={version.md}
×
251
            language="markdown"
×
252
            options={defaultEditorOptions}
×
253
          />
×
254
        </div>
×
255
      )}
256

257
      <div className={styles.collaborativeEditor} hidden={mode !== 'write'}>
×
258
        <MonacoEditor
×
259
          width={'100%'}
×
260
          height={'auto'}
×
261
          options={options}
×
262
          className={styles.editor}
×
263
          defaultLanguage="markdown"
×
264
          {...(hasVersion
×
265
            ? { value: version.md, onMount: handleEditorDidMount }
×
266
            : { onMount: handleCollaborativeEditorDidMount })}
×
267
        />
×
268
      </div>
×
269
    </>
×
270
  )
271
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc