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

EcrituresNumeriques / stylo / 12925854411

23 Jan 2025 09:11AM UTC coverage: 25.831% (-4.7%) from 30.523%
12925854411

push

github

web-flow
Merge pull request #1192 from EcrituresNumeriques/feat/vite6

322 of 518 branches covered (62.16%)

Branch coverage included in aggregate %.

3448 of 14077 relevant lines covered (24.49%)

1.66 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 { Loading } from '@geist-ui/core'
×
2
import PropTypes from 'prop-types'
×
3
import Editor from '@monaco-editor/react'
×
4
import throttle from 'lodash.throttle'
×
5
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
×
6
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
×
7
import { MonacoBinding } from 'y-monaco'
×
8
import { applicationConfig } from '../../config.js'
×
9
import * as collaborating from './collaborating.js'
×
10
import CollaborativeEditorStatus from './CollaborativeEditorStatus.jsx'
×
11

12
import styles from './CollaborativeTextEditor.module.scss'
×
13

14
const colors = [
×
15
  // navy
16
  '#70b8ff',
×
17
  // blue
18
  '#75bfff',
×
19
  // aqua
20
  '#7FDBFF',
×
21
  // teal
22
  '#39CCCC',
×
23
  // olive
24
  '#92d3b6',
×
25
  // green
26
  '#97e7a0',
×
27
  // yellow
28
  '#ffeb66',
×
29
  // orange
30
  '#ffbb80',
×
31
  // red
32
  '#ff726b',
×
33
  // maroon
34
  '#ff6666',
×
35
  // fuchsia
36
  '#f674d8',
×
37
  // purple
38
  '#e46ff6',
×
39
  // gray
40
  '#AAAAAA',
×
41
  // silver
42
  '#DDDDDD',
×
43
]
×
44

45
export default function CollaborativeTextEditor({
×
46
  articleId,
×
47
  collaborativeSessionCreatorId,
×
48
  collaborativeSessionId,
×
49
  onCollaborativeSessionStateUpdated,
×
50
}) {
×
51
  const connectingRef = useRef(false)
×
52
  const [dynamicStyles, setDynamicStyles] = useState('')
×
53
  const [websocketStatus, setWebsocketStatus] = useState('')
×
54
  const [collaborativeSessionState, setCollaborativeSessionState] = useState('')
×
55
  const [yText, setYText] = useState(null)
×
56
  const [awareness, setAwareness] = useState(null)
×
57
  const { websocketEndpoint } = applicationConfig
×
58
  const activeUser = useSelector(
×
59
    (state) => ({
×
60
      _id: state.activeUser._id,
×
61
      email: state.activeUser.email,
×
62
      displayName: state.activeUser.displayName,
×
63
      username: state.activeUser.username,
×
64
    }),
×
65
    shallowEqual
×
66
  )
×
67
  const dispatch = useDispatch()
×
68
  const editorRef = useRef()
×
69
  const editorCursorPosition = useSelector(
×
70
    (state) => state.editorCursorPosition,
×
71
    shallowEqual
×
72
  )
×
73

74
  const options = useMemo(
×
75
    () => ({
×
76
      automaticLayout: true,
×
77
      readOnly:
×
78
        websocketStatus !== 'connected' ||
×
79
        collaborativeSessionState !== 'started',
×
80
      contextmenu: websocketStatus === 'connected',
×
81
      autoClosingBrackets: 'never',
×
82
      wordBasedSuggestions: false,
×
83
      overviewRulerLanes: 0,
×
84
      hideCursorInOverviewRuler: true,
×
85
      overviewRulerBorder: false,
×
86
      scrollBeyondLastLine: false,
×
87
      wordWrap: 'on',
×
88
      wrappingIndent: 'none',
×
89
      minimap: {
×
90
        enabled: false,
×
91
      },
×
92
    }),
×
93
    [websocketStatus]
×
94
  )
×
95

96
  const handleUpdateArticleStructureAndStats = throttle(
×
97
    ({ text }) => {
×
98
      dispatch({ type: 'UPDATE_ARTICLE_STATS', md: text })
×
99
      dispatch({ type: 'UPDATE_ARTICLE_STRUCTURE', md: text })
×
100
    },
×
101
    250,
×
102
    { leading: false, trailing: true }
×
103
  )
×
104

105
  const writerInfo = useMemo(
×
106
    () => ({
×
107
      id: activeUser._id,
×
108
      email: activeUser.email,
×
109
      displayName: activeUser.displayName,
×
110
      username: activeUser.username,
×
111
      color: colors[Math.floor(Math.random() * 14)],
×
112
    }),
×
113
    [activeUser]
×
114
  )
×
115

116
  const handleWritersUpdated = useCallback(
×
117
    ({ states }) => {
×
118
      const writers = Object.fromEntries(states)
×
119
      dispatch({ type: 'UPDATE_ARTICLE_WRITERS', articleWriters: writers })
×
120
      setDynamicStyles(
×
121
        Object.entries(writers)
×
122
          .map(([key, writer]) => {
×
123
            const color = writer.user.color
×
124
            return `
×
125
.yRemoteSelection-${key} {
×
126
  background-color: ${color};
×
127
}
128
.yRemoteSelectionHead-${key} {
×
129
  border-left: ${color} solid 2px;
×
130
  border-top: ${color} solid 2px;
×
131
  border-bottom: ${color} solid 2px;
×
132
}`
133
          })
×
134
          .join('\n')
×
135
      )
×
136
    },
×
137
    [setDynamicStyles]
×
138
  )
×
139

140
  const handleWebsocketStatusUpdated = useCallback(
×
141
    (status) => {
×
142
      setWebsocketStatus(status)
×
143
    },
×
144
    [setWebsocketStatus]
×
145
  )
×
146

147
  const handleEditorDidMount = useCallback(
×
148
    (editor) => {
×
149
      editorRef.current = editor
×
150
      new MonacoBinding(yText, editor.getModel(), new Set([editor]), awareness)
×
151
    },
×
152
    [yText, awareness]
×
153
  )
×
154

155
  useEffect(() => {
×
156
    if (connectingRef.current) {
×
157
      return
×
158
    }
×
159
    connectingRef.current = true
×
160
    const {
×
161
      awareness,
×
162
      doc: yDocument,
×
163
      wsProvider,
×
164
    } = collaborating.connect({
×
165
      roomName: collaborativeSessionId,
×
166
      websocketEndpoint,
×
167
      user: writerInfo,
×
168
      onChange: handleWritersUpdated,
×
169
      onStatusUpdated: handleWebsocketStatusUpdated,
×
170
    })
×
171
    const yText = yDocument.getText('main')
×
172
    const yState = yDocument.getText('state')
×
173
    yText.observe(function () {
×
174
      handleUpdateArticleStructureAndStats({ text: yText.toString() })
×
175
    })
×
176
    yState.observe(function () {
×
177
      setCollaborativeSessionState(yState.toString())
×
178
      onCollaborativeSessionStateUpdated({ state: yState.toString() })
×
179
    })
×
180
    setAwareness(awareness)
×
181
    setYText(yText)
×
182
    return () => {
×
183
      connectingRef.current = false
×
184
      awareness.destroy()
×
185
      wsProvider.disconnect()
×
186
      wsProvider.destroy()
×
187
    }
×
188
  }, [collaborativeSessionId, websocketEndpoint, writerInfo])
×
189

190
  useEffect(() => {
×
191
    const line = editorCursorPosition.lineNumber
×
192
    const editor = editorRef.current
×
193
    editor?.focus()
×
194
    const endOfLineColumn = editor?.getModel()?.getLineMaxColumn(line + 1)
×
195
    editor?.setPosition({ lineNumber: line + 1, column: endOfLineColumn })
×
196
    editor?.revealLine(line + 1, 1) // smooth
×
197
  }, [editorRef, editorCursorPosition])
×
198

199
  if (!yText) {
×
200
    return <Loading />
×
201
  }
×
202

203
  return (
×
204
    <>
×
205
      <style>{dynamicStyles}</style>
×
206
      <CollaborativeEditorStatus
×
207
        articleId={articleId}
×
208
        collaborativeSessionState={collaborativeSessionState}
×
209
        websocketStatus={websocketStatus}
×
210
        collaborativeSessionCreatorId={collaborativeSessionCreatorId}
×
211
      />
×
212
      <Editor
×
213
        options={options}
×
214
        className={styles.editor}
×
215
        defaultLanguage="markdown"
×
216
        onMount={handleEditorDidMount}
×
217
      />
×
218
    </>
×
219
  )
220
}
×
221

222
CollaborativeTextEditor.propTypes = {
×
223
  articleId: PropTypes.string.isRequired,
×
224
  collaborativeSessionId: PropTypes.string.isRequired,
×
225
  collaborativeSessionCreatorId: PropTypes.string.isRequired,
×
226
  onCollaborativeSessionStateUpdated: PropTypes.func,
×
227
}
×
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