@podlite/editor-react 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@podlite/editor-react",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "Podlite React component",
5
- "main": "lib/index.js",
5
+ "main": "./src/index.tsx",
6
6
  "types": "lib/index.d.ts",
7
7
  "typings": "./lib/index.d.ts",
8
- "module": "lib/index.esm.js",
9
8
  "license": "MIT",
10
9
  "sideEffects": true,
11
10
  "files": [
@@ -32,7 +31,8 @@
32
31
  "./lib/index.css": "./lib/index.css"
33
32
  },
34
33
  "publishConfig": {
35
- "access": "public"
34
+ "access": "public",
35
+ "module": "lib/index.esm.js"
36
36
  },
37
37
  "scripts": {
38
38
  "clean": "rm -rf dist lib tsconfig.tsbuildinfo esm",
@@ -56,5 +56,6 @@
56
56
  "peerDependencies": {
57
57
  "react": "^16.0.0 || ^17.0.0 ",
58
58
  "react-dom": "^16.0.0 || ^17.0.0"
59
- }
59
+ },
60
+ "module": "lib/index.esm.js"
60
61
  }
package/src/index.tsx ADDED
@@ -0,0 +1,372 @@
1
+ import React from 'react'
2
+ import { Controlled as CodeMirrorControlled, UnControlled as CodeMirror } from 'react-codemirror2'
3
+ import CMirror from 'codemirror'
4
+ import dictionary from './dict'
5
+ import { useState, useEffect, useRef } from 'react'
6
+ import { isElement } from 'react-is'
7
+
8
+ // TODO: use bundler to add into package
9
+ // import '../../../node_modules/codemirror/lib/codemirror.css';
10
+ import 'codemirror/mode/gfm/gfm'
11
+ import 'codemirror/addon/hint/show-hint'
12
+ import 'codemirror/addon/hint/show-hint.css'
13
+ import './Editor.css'
14
+ import { addVMargin, getSuggestionContextForLine, templateGetSelectionPos } from './helpers'
15
+
16
+ //@ts-ignore
17
+ function useDebouncedEffect(fn, deps, time) {
18
+ const dependencies = [...deps, time]
19
+ useEffect(() => {
20
+ const timeout = setTimeout(fn, time)
21
+ return () => {
22
+ clearTimeout(timeout)
23
+ }
24
+ }, dependencies)
25
+ }
26
+
27
+ /* set window title */
28
+ // @ts-ignore
29
+ // const setWindowTitle = (title: string) => { vmd.setWindowTitle(title) }
30
+ export interface ConverterResult {
31
+ errors?: any
32
+ result: any
33
+ }
34
+
35
+ let instanceCM = null
36
+ type Props = {
37
+ content: string
38
+ onChangeSource: Function
39
+ sourceType?: 'pod6' | 'md'
40
+ onConvertSource: (source: string) => ConverterResult
41
+ onSavePressed?: Function
42
+ isDarkTheme?: boolean
43
+ isLineNumbers?: boolean
44
+ isAutoComplete?: boolean
45
+ isPreviewModeEnabled?: boolean
46
+ isControlled?: boolean
47
+ }
48
+
49
+ export const Editor = ({
50
+ onChangeSource = () => {},
51
+ content,
52
+ isDarkTheme = false,
53
+ isLineNumbers = false,
54
+ isPreviewModeEnabled = false,
55
+ onConvertSource,
56
+ onSavePressed = () => {},
57
+ sourceType = 'pod6',
58
+ isControlled = false,
59
+ isAutoComplete = true,
60
+ }: Props) => {
61
+ const [text, updateText] = useState(content)
62
+
63
+ const [marks, updateMarks] = useState([])
64
+ const [, updateScrollMap] = useState([])
65
+
66
+ const [isPreviewMode, setPreviewMode] = useState(isPreviewModeEnabled)
67
+
68
+ const [isPreviewScroll, setPreviewScrolling] = useState(false)
69
+ const refValue = useRef(isPreviewScroll)
70
+ const [showTree, setShowTree] = useState(false)
71
+
72
+ const [filePath, setFilePath] = useState('')
73
+ const [fileName, setFileName] = useState('')
74
+ const [fileExt, setFileExt] = useState('')
75
+ const [isChanged, setChanged] = useState(false)
76
+
77
+ const [fileLoading, setFileLoading] = useState(true)
78
+
79
+ useEffect(() => {
80
+ updateText(content)
81
+ }, [content])
82
+
83
+ const [result, updateResult] = useState<ConverterResult>()
84
+ useDebouncedEffect(
85
+ () => {
86
+ updateResult(onConvertSource(text))
87
+ },
88
+ [text],
89
+ 50,
90
+ )
91
+
92
+ const inputEl = useRef(null)
93
+
94
+ // hot keys
95
+ useEffect(() => {
96
+ const saveFileAction = () => {
97
+ if (isChanged) {
98
+ console.warn('Save File')
99
+ onSavePressed(text)
100
+ }
101
+ }
102
+ })
103
+
104
+ useEffect(() => {
105
+ refValue.current = isPreviewScroll
106
+ })
107
+ var options: CMirror.EditorConfiguration = {
108
+ lineNumbers: isLineNumbers,
109
+ inputStyle: 'contenteditable',
110
+ //@ts-ignore
111
+ spellcheck: true,
112
+ autofocus: true,
113
+ lineWrapping: true,
114
+ viewportMargin: Infinity,
115
+ mode:
116
+ sourceType !== 'md'
117
+ ? null
118
+ : {
119
+ name: 'gfm',
120
+ tokenTypeOverrides: {
121
+ emoji: 'emoji',
122
+ },
123
+ },
124
+ theme: isDarkTheme ? 'duotone-dark' : 'default',
125
+ }
126
+
127
+ const previewEl = useRef(null)
128
+
129
+ useEffect(() => {
130
+ //@ts-ignore
131
+ const newScrollMap = [...document.querySelectorAll('.line-src')].map(n => {
132
+ const line = parseInt(n.getAttribute('data-line'), 10)
133
+ //@ts-ignore
134
+ const offsetTop = n.offsetTop
135
+ return { line, offsetTop }
136
+ })
137
+ //@ts-ignore
138
+ updateScrollMap(newScrollMap)
139
+ //@ts-ignore
140
+ const listener = e => {
141
+ if (!isPreviewScroll) {
142
+ return
143
+ }
144
+ let element = e.target
145
+ //@ts-ignore
146
+ const getLine = offset => {
147
+ const c = newScrollMap.filter(i => i.offsetTop > offset)
148
+ const lineElement = c.shift() || newScrollMap[newScrollMap.length - 1]
149
+ if (!lineElement) {
150
+ console.warn(`[podlite-editor] can't get line for offset. Forget add .line-src ?`)
151
+ }
152
+ return lineElement.line
153
+ }
154
+ const line = getLine(element.scrollTop)
155
+ if (instanceCM) {
156
+ const t = element.scrollTop === 0 ? 0 : instanceCM.charCoords({ line: line, ch: 0 }, 'local').top
157
+ instanceCM.scrollTo(null, t)
158
+ }
159
+ return true
160
+ }
161
+ if (previewEl && previewEl.current) {
162
+ //@ts-ignore
163
+ previewEl.current.addEventListener('scroll', listener)
164
+ }
165
+ return () => {
166
+ // @ts-ignore
167
+ previewEl && previewEl.current && previewEl && previewEl.current.removeEventListener('scroll', listener)
168
+ }
169
+ }, [text, isPreviewScroll])
170
+
171
+ useEffect(() => {
172
+ //@ts-ignore
173
+ let cm = instanceCM
174
+ if (!cm) {
175
+ return
176
+ }
177
+ //@ts-ignore
178
+ marks.forEach(marker => marker.clear())
179
+ //@ts-ignore
180
+ let cmMrks: Array<never> = []
181
+ //@ts-ignore
182
+ if (result && result.errors) {
183
+ //@ts-ignore
184
+ result.errors.map((loc: any) => {
185
+ // @ts-ignore
186
+ let from = { line: loc.start.line - 1, ch: loc.start.column - 1 - (loc.start.offset === loc.end.offset) }
187
+ let to = { line: loc.end.line - 1, ch: loc.end.column - 1 }
188
+
189
+ cmMrks.push(
190
+ //@ts-ignore
191
+ cm.markText(from, to, {
192
+ className: 'syntax-error',
193
+ title: ';data.error.message',
194
+ css: 'color : red',
195
+ }),
196
+ )
197
+ })
198
+ }
199
+ updateMarks(cmMrks)
200
+ }, [text, result])
201
+
202
+ const previewHtml = (
203
+ <div
204
+ className={'Editorright ' + (isDarkTheme ? 'dark' : '')}
205
+ onMouseEnter={() => setPreviewScrolling(true)}
206
+ onMouseMove={() => setPreviewScrolling(true)}
207
+ ref={previewEl}
208
+ >
209
+ {result ? (
210
+ isElement(result.result) ? (
211
+ <div className="content">{result.result}</div>
212
+ ) : (
213
+ <div dangerouslySetInnerHTML={{ __html: result.result }} className="content"></div>
214
+ )
215
+ ) : (
216
+ ''
217
+ )}
218
+ </div>
219
+ )
220
+ //@ts-ignore
221
+ const scrollEditorHandler = editor => {
222
+ if (refValue.current) {
223
+ return
224
+ }
225
+ let scrollInfo = editor.getScrollInfo()
226
+ // get line number of the top line in the page
227
+ let lineNumber = editor.lineAtHeight(scrollInfo.top, 'local') + 1
228
+ if (previewEl) {
229
+ const el = previewEl.current
230
+ const elementId = `#line-${lineNumber}`
231
+ const scrollToElement = document.querySelector(elementId)
232
+ if (scrollToElement) {
233
+ //@ts-ignore
234
+ const scrollTo = scrollToElement.offsetTop
235
+ //@ts-ignore
236
+ el.scrollTo({
237
+ top: scrollTo,
238
+ left: 0,
239
+ behavior: 'smooth',
240
+ })
241
+ }
242
+ }
243
+ }
244
+ const [instanceCMLocal, updateInstanceCM] = useState<any>()
245
+
246
+ useEffect(() => {
247
+ if (!instanceCMLocal) return
248
+ if (!isAutoComplete) return
249
+ var onChange = function (instance, object) {
250
+ // Check if the last inserted character is `=`.
251
+ if (
252
+ object.text[0] === '=' &&
253
+ // start directive
254
+ instance.getRange({ ch: 0, line: object.to.line }, object.to).match(/^\s*$/)
255
+ ) {
256
+ CMirror.showHint(instanceCMLocal, CMirror.hint.dictionaryHint)
257
+ }
258
+ }
259
+ instanceCMLocal.on('change', onChange)
260
+
261
+ CMirror.registerHelper('hint', 'dictionaryHint', function (editor) {
262
+ var cur = editor.getCursor()
263
+ var curLine = editor.getLine(cur.line)
264
+ var start = cur.ch
265
+ var end = start
266
+ while (end < curLine.length && /[\w$]/.test(curLine.charAt(end))) ++end
267
+ while (start && /[^=]/.test(curLine.charAt(start - 1))) --start
268
+ var curWord = start !== end && curLine.slice(start, end)
269
+ var regex = new RegExp('' + curWord, 'i')
270
+ // filter dict by regex and sort by mostly nearness
271
+ const filterDictByRegex = (arr, regex) => {
272
+ const dict = arr.reduce((acc, item) => {
273
+ if (item === null) {
274
+ return acc
275
+ }
276
+ const result =
277
+ typeof item === 'object' && !Array.isArray(item) ? item.displayText.match(regex) : item.match(regex)
278
+ if (result) {
279
+ acc.push({ item, index: result.index })
280
+ }
281
+ return acc
282
+ }, [])
283
+ return dict.sort((a, b) => a.index - b.index).map(i => i.item)
284
+ }
285
+ const langMode = sourceType === 'md' ? 'md' : getSuggestionContextForLine(editor.getValue(), cur.line + 1)
286
+ const langDict = dictionary.filter(({ lang = 'pod6' }) => lang === langMode)
287
+ // apply hint
288
+ const resultDict = (!curWord ? langDict : filterDictByRegex(langDict, regex)).map(item => {
289
+ return {
290
+ ...item,
291
+ hint: function (cm, data, completion) {
292
+ const from = completion.from || data.from
293
+ const to = completion.to || data.to
294
+ // add vMargin
295
+ const text = addVMargin(from.ch, typeof completion == 'string' ? completion : completion.text)
296
+ const selFromTemplate = templateGetSelectionPos(text)
297
+ console.log({ from, to, text })
298
+ if (selFromTemplate) {
299
+ const { text, start, end } = selFromTemplate
300
+ cm.replaceRange(text, from, to, 'complete')
301
+ cm.setSelection(
302
+ { line: start.line + from.line, ch: start.offset + from.ch },
303
+ { line: end.line + to.line, ch: end.offset + to.ch - 1 },
304
+ )
305
+ } else {
306
+ cm.replaceRange(text, from, to, 'complete')
307
+ }
308
+ },
309
+ }
310
+ })
311
+ return {
312
+ list: resultDict,
313
+ from: CMirror.Pos(cur.line, start - 1),
314
+ to: CMirror.Pos(cur.line, end),
315
+ }
316
+ })
317
+ instanceCMLocal.refresh()
318
+ return () => {
319
+ //@ts-ignore
320
+ instanceCMLocal.off('change', onChange)
321
+ }
322
+ }, [instanceCMLocal, isAutoComplete])
323
+
324
+ return (
325
+ <div className="EditorApp">
326
+ <div className={isPreviewModeEnabled ? 'layoutPreview' : 'layout'}>
327
+ <div
328
+ className="Editorleft"
329
+ onMouseEnter={() => setPreviewScrolling(false)}
330
+ onMouseMove={() => setPreviewScrolling(false)}
331
+ >
332
+ {isControlled ? (
333
+ <CodeMirrorControlled
334
+ value={content}
335
+ editorDidMount={editor => {
336
+ instanceCM = editor
337
+ updateInstanceCM(editor)
338
+ }}
339
+ onBeforeChange={(editor, data, value) => {
340
+ setChanged(true)
341
+ // updateText(value);
342
+ onChangeSource(value)
343
+ }}
344
+ onScroll={scrollEditorHandler}
345
+ options={options}
346
+ className="editorApp"
347
+ />
348
+ ) : (
349
+ <CodeMirror
350
+ value={content}
351
+ editorDidMount={editor => {
352
+ instanceCM = editor
353
+ updateInstanceCM(editor)
354
+ }}
355
+ onChange={(editor, data, value) => {
356
+ setChanged(true)
357
+ updateText(value)
358
+ onChangeSource(value)
359
+ }}
360
+ onScroll={scrollEditorHandler}
361
+ options={options}
362
+ className="editorApp"
363
+ />
364
+ )}
365
+ </div>
366
+ {previewHtml}
367
+ </div>
368
+ </div>
369
+ )
370
+ }
371
+
372
+ export default Editor