mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-21 10:45:20 -04:00
Upgrade to CodeMirror 6 (#1787)
Upgrade to CodeMirror 6 Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
1a09bfa5f1
commit
6a6f6105b9
103 changed files with 1906 additions and 2615 deletions
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor, generateHintListByPrefix } from './index'
|
||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
|
||||
type highlightJsImport = typeof import('../../../common/hljs/hljs')
|
||||
|
||||
const log = new Logger('Autocompletion > CodeBlock')
|
||||
const wordRegExp = /^```((?:\w|-|\+)*)$/
|
||||
let allSupportedLanguages: string[] = []
|
||||
|
||||
/**
|
||||
* Fetches the highlight js chunk.
|
||||
* @return the retrieved highlight js api
|
||||
*/
|
||||
const loadHighlightJs = async (): Promise<highlightJsImport | null> => {
|
||||
try {
|
||||
return await import('../../../common/hljs/hljs')
|
||||
} catch (error) {
|
||||
showErrorNotification('common.errorWhileLoadingLibrary', { name: 'highlight.js' })(error as Error)
|
||||
log.error('Error while loading highlight.js', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the language from the current line in the editor.
|
||||
*
|
||||
* @param editor The editor that contains the search time
|
||||
* @return null if no search term could be found or the found word and the cursor position.
|
||||
*/
|
||||
const extractSearchTerm = (
|
||||
editor: Editor
|
||||
): null | {
|
||||
searchTerm: string
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
} => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
searchTerm: searchResult[1],
|
||||
startIndex: searchTerm.start,
|
||||
endIndex: searchTerm.end
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the list of languages that are supported by highlight js or custom embeddings.
|
||||
* @return An array of language names
|
||||
*/
|
||||
const buildLanguageList = async (): Promise<string[]> => {
|
||||
const highlightJs = await loadHighlightJs()
|
||||
|
||||
if (highlightJs === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (allSupportedLanguages.length === 0) {
|
||||
allSupportedLanguages = highlightJs.default
|
||||
.listLanguages()
|
||||
.concat('csv', 'flow', 'html', 'js', 'markmap', 'abc', 'graphviz', 'mermaid', 'vega-lite')
|
||||
}
|
||||
|
||||
return allSupportedLanguages
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a codemirror autocompletion hint with supported highlight js languages.
|
||||
*
|
||||
* @param editor The codemirror editor that requested the autocompletion
|
||||
* @return The generated {@link Hints} or null if no hints exist.
|
||||
*/
|
||||
const codeBlockHint = async (editor: Editor): Promise<Hints | null> => {
|
||||
const searchResult = extractSearchTerm(editor)
|
||||
if (!searchResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
const languages = await buildLanguageList()
|
||||
if (languages.length === 0) {
|
||||
return null
|
||||
}
|
||||
const suggestions = generateHintListByPrefix(searchResult.searchTerm, languages)
|
||||
if (!suggestions) {
|
||||
return null
|
||||
}
|
||||
const lineIndex = editor.getCursor().line
|
||||
return {
|
||||
list: suggestions.map(
|
||||
(suggestion: string): Hint => ({
|
||||
text: '```' + suggestion + '\n\n```\n',
|
||||
displayText: suggestion
|
||||
})
|
||||
),
|
||||
from: Pos(lineIndex, searchResult.startIndex),
|
||||
to: Pos(lineIndex, searchResult.endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
export const CodeBlockHinter: Hinter = {
|
||||
wordRegExp,
|
||||
hint: codeBlockHint
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor } from './index'
|
||||
|
||||
const wordRegExp = /^(<d(?:e|et|eta|etai|etail|etails)?)$/
|
||||
|
||||
const collapsibleBlockHint = (editor: Editor): Promise<Hints | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = ['<details>\n <summary>Toggle label</summary>\n Toggled content\n</details>']
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map(
|
||||
(suggestion: string): Hint => ({
|
||||
text: suggestion
|
||||
})
|
||||
),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const CollapsibleBlockHinter: Hinter = {
|
||||
wordRegExp,
|
||||
hint: collapsibleBlockHint
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor } from './index'
|
||||
import { alertLevels } from '../../../markdown-renderer/markdown-extension/alert-markdown-extension'
|
||||
|
||||
const wordRegExp = /^:::((?:\w|-|\+)*)$/
|
||||
const spoilerSuggestion: Hint = {
|
||||
text: ':::spoiler Toggle label\nToggled content\n::: \n',
|
||||
displayText: 'spoiler'
|
||||
}
|
||||
const suggestions = alertLevels
|
||||
.map(
|
||||
(suggestion: string): Hint => ({
|
||||
text: ':::' + suggestion + '\n\n::: \n',
|
||||
displayText: suggestion
|
||||
})
|
||||
)
|
||||
.concat(spoilerSuggestion)
|
||||
|
||||
const containerHint = (editor: Editor): Promise<Hints | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.filter((suggestion) => suggestion.displayText?.startsWith(searchResult[1])),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ContainerHinter: Hinter = {
|
||||
wordRegExp,
|
||||
hint: containerHint
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import Database from 'emoji-picker-element/database'
|
||||
import type { Emoji, EmojiClickEventDetail, NativeEmoji } from 'emoji-picker-element/shared'
|
||||
import { emojiPickerConfig } from '../tool-bar/emoji-picker/emoji-picker'
|
||||
import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor } from './index'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
|
||||
const emojiIndex = new Database(emojiPickerConfig)
|
||||
const emojiWordRegex = /^:([\w-_+]*)$/
|
||||
const log = new Logger('Autocompletion > Emoji')
|
||||
|
||||
const findEmojiInDatabase = async (emojiIndex: Database, term: string): Promise<Emoji[]> => {
|
||||
try {
|
||||
if (term === '') {
|
||||
return await emojiIndex.getTopFavoriteEmoji(7)
|
||||
}
|
||||
const queryResult = await emojiIndex.getEmojiBySearchQuery(term)
|
||||
if (queryResult.length === 0) {
|
||||
return await emojiIndex.getTopFavoriteEmoji(7)
|
||||
} else {
|
||||
return queryResult
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error while searching for emoji', term, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const convertEmojiEventToHint = (emojiData: EmojiClickEventDetail): Hint | undefined => {
|
||||
const shortCode = getEmojiShortCode(emojiData)
|
||||
if (!shortCode) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
text: shortCode,
|
||||
render: (parent: HTMLLIElement) => {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerHTML = `${getEmojiIcon(emojiData)} ${shortCode}`
|
||||
parent.appendChild(wrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateEmojiHints = async (editor: Editor): Promise<Hints | null> => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = emojiWordRegex.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
return null
|
||||
}
|
||||
const suggestionList: Emoji[] = await findEmojiInDatabase(emojiIndex, searchResult[1])
|
||||
const cursor = editor.getCursor()
|
||||
const skinTone = await emojiIndex.getPreferredSkinTone()
|
||||
const emojiEventDetails: EmojiClickEventDetail[] = suggestionList
|
||||
.filter((emoji) => !!emoji.shortcodes)
|
||||
.map((emoji) => ({
|
||||
emoji,
|
||||
skinTone: skinTone,
|
||||
unicode: (emoji as NativeEmoji).unicode ? (emoji as NativeEmoji).unicode : undefined,
|
||||
name: emoji.name
|
||||
}))
|
||||
|
||||
const hints = emojiEventDetails.map(convertEmojiEventToHint).filter((o) => !!o) as Hint[]
|
||||
return {
|
||||
list: hints,
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
}
|
||||
}
|
||||
|
||||
export const EmojiHinter: Hinter = {
|
||||
wordRegExp: emojiWordRegex,
|
||||
hint: generateEmojiHints
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor, generateHintListByPrefix } from './index'
|
||||
|
||||
const wordRegExp = /^(\s{0,3})(#{1,6})$/
|
||||
const allSupportedHeaders = ['# h1', '## h2', '### h3', '#### h4', '##### h5', '###### h6', '###### tags: `example`']
|
||||
const allSupportedHeadersTextToInsert = ['# ', '## ', '### ', '#### ', '##### ', '###### ', '###### tags: `example`']
|
||||
|
||||
const headerHint = (editor: Editor): Promise<Hints | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const term = searchResult[0]
|
||||
if (!term) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = generateHintListByPrefix(term, allSupportedHeaders)
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map(
|
||||
(suggestion): Hint => ({
|
||||
text: allSupportedHeadersTextToInsert[allSupportedHeaders.indexOf(suggestion)],
|
||||
displayText: suggestion
|
||||
})
|
||||
),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const HeaderHinter: Hinter = {
|
||||
wordRegExp,
|
||||
hint: headerHint
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor } from './index'
|
||||
|
||||
const wordRegExp = /^(!(\[.*])?)$/
|
||||
const allSupportedImages = [
|
||||
'',
|
||||
'',
|
||||
'![image alt][reference]'
|
||||
]
|
||||
|
||||
const imageHint = (editor: Editor): Promise<Hints | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = allSupportedImages
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map(
|
||||
(suggestion: string): Hint => ({
|
||||
text: suggestion
|
||||
})
|
||||
),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ImageHinter: Hinter = {
|
||||
wordRegExp,
|
||||
hint: imageHint
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hints } from 'codemirror'
|
||||
import { CodeBlockHinter } from './code-block'
|
||||
import { CollapsibleBlockHinter } from './collapsible-block'
|
||||
import { ContainerHinter } from './container'
|
||||
import { EmojiHinter } from './emoji'
|
||||
import { HeaderHinter } from './header'
|
||||
import { ImageHinter } from './image'
|
||||
import { LinkAndExtraTagHinter } from './link-and-extra-tag'
|
||||
import { PDFHinter } from './pdf'
|
||||
|
||||
interface findWordAtCursorResponse {
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface Hinter {
|
||||
wordRegExp: RegExp
|
||||
hint: (editor: Editor) => Promise<Hints | null>
|
||||
}
|
||||
|
||||
const allowedChars = /[^\s]/
|
||||
|
||||
export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
||||
const cursor = editor.getCursor()
|
||||
const line = editor.getLine(cursor.line)
|
||||
let start = cursor.ch
|
||||
let end = cursor.ch
|
||||
while (start && allowedChars.test(line.charAt(start - 1))) {
|
||||
--start
|
||||
}
|
||||
while (end < line.length && allowedChars.test(line.charAt(end))) {
|
||||
++end
|
||||
}
|
||||
|
||||
return {
|
||||
text: line.slice(start, end).toLowerCase(),
|
||||
start: start,
|
||||
end: end
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list (with max 8 entries) of hints for the autocompletion.
|
||||
*
|
||||
* @param prefix This is the case insensitive prefix that every hint must have
|
||||
* @param hintCandidates The list of hint candidates
|
||||
*/
|
||||
export const generateHintListByPrefix = (prefix: string, hintCandidates: string[]): string[] => {
|
||||
const searchTerm = prefix.toLowerCase()
|
||||
return hintCandidates.filter((item) => item.toLowerCase().startsWith(searchTerm)).slice(0, 7)
|
||||
}
|
||||
|
||||
export const allHinters: Hinter[] = [
|
||||
CodeBlockHinter,
|
||||
ContainerHinter,
|
||||
EmojiHinter,
|
||||
HeaderHinter,
|
||||
ImageHinter,
|
||||
LinkAndExtraTagHinter,
|
||||
PDFHinter,
|
||||
CollapsibleBlockHinter
|
||||
]
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import { DateTime } from 'luxon'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor } from './index'
|
||||
import { getGlobalState } from '../../../../redux'
|
||||
|
||||
const wordRegExp = /^(\[(.*])?)$/
|
||||
const allSupportedLinks = [
|
||||
'[link text](https:// "title")',
|
||||
'[reference]: https:// "title"',
|
||||
'[link text][reference]',
|
||||
'[reference]',
|
||||
'[^footnote reference]: https://',
|
||||
'[^footnote reference]',
|
||||
'^[inline footnote]',
|
||||
'[TOC]',
|
||||
'name',
|
||||
'time',
|
||||
'[color=#FFFFFF]'
|
||||
]
|
||||
|
||||
const getUserName = (): string => {
|
||||
const user = getGlobalState().user
|
||||
return user ? user.displayName : 'Anonymous'
|
||||
}
|
||||
|
||||
const linkAndExtraTagHint = (editor: Editor): Promise<Hints | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = allSupportedLinks
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion: string): Hint => {
|
||||
switch (suggestion) {
|
||||
case 'name':
|
||||
// Get the user when a completion happens, this prevents to early calls resulting in 'Anonymous'
|
||||
return {
|
||||
text: `[name=${getUserName()}]`
|
||||
}
|
||||
case 'time':
|
||||
// show the current time when the autocompletion is opened and not when the function is loaded
|
||||
return {
|
||||
text: `[time=${DateTime.local().toFormat('DDDD T')}]`
|
||||
}
|
||||
default:
|
||||
return {
|
||||
text: suggestion + ' ',
|
||||
displayText: suggestion
|
||||
}
|
||||
}
|
||||
}),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const LinkAndExtraTagHinter: Hinter = {
|
||||
wordRegExp,
|
||||
hint: linkAndExtraTagHint
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, Hint, Hints } from 'codemirror'
|
||||
import { Pos } from 'codemirror'
|
||||
import type { Hinter } from './index'
|
||||
import { findWordAtCursor } from './index'
|
||||
|
||||
const wordRegExp = /^({[%}]?)$/
|
||||
|
||||
const pdfHint = (editor: Editor): Promise<Hints | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = ['{%pdf https:// %}']
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map(
|
||||
(suggestion: string): Hint => ({
|
||||
text: suggestion
|
||||
})
|
||||
),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const PDFHinter: Hinter = {
|
||||
wordRegExp,
|
||||
hint: pdfHint
|
||||
}
|
|
@ -1,89 +1,112 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, EditorChange } from 'codemirror'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import type { ScrollProps } from '../synced-scroll/scroll-props'
|
||||
import { StatusBar } from './status-bar/status-bar'
|
||||
import { ToolBar } from './tool-bar/tool-bar'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { setNoteContent } from '../../../redux/note-details/methods'
|
||||
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
||||
import { useCodeMirrorOptions } from './hooks/use-code-mirror-options'
|
||||
import { useOnEditorPasteCallback } from './hooks/use-on-editor-paste-callback'
|
||||
import { useOnEditorFileDrop } from './hooks/use-on-editor-file-drop'
|
||||
import { useOnEditorScroll } from './hooks/use-on-editor-scroll'
|
||||
import { useApplyScrollState } from './hooks/use-apply-scroll-state'
|
||||
import { MaxLengthWarning } from './max-length-warning/max-length-warning'
|
||||
import { useOnImageUploadFromRenderer } from './hooks/use-on-image-upload-from-renderer'
|
||||
import { ExtendedCodemirror } from './extended-codemirror/extended-codemirror'
|
||||
import type { ReactCodeMirrorRef } from '@uiw/react-codemirror'
|
||||
import ReactCodeMirror from '@uiw/react-codemirror'
|
||||
import { useCursorActivityCallback } from './hooks/use-cursor-activity-callback'
|
||||
import { useApplyScrollState } from './hooks/use-apply-scroll-state'
|
||||
import styles from './extended-codemirror/codemirror.module.scss'
|
||||
import { oneDark } from '@codemirror/theme-one-dark'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import { useCodeMirrorScrollWatchExtension } from './hooks/code-mirror-extensions/use-code-mirror-scroll-watch-extension'
|
||||
import { useCodeMirrorPasteExtension } from './hooks/code-mirror-extensions/use-code-mirror-paste-extension'
|
||||
import { useCodeMirrorFileDropExtension } from './hooks/code-mirror-extensions/use-code-mirror-file-drop-extension'
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
||||
import { languages } from '@codemirror/language-data'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { autocompletion } from '@codemirror/autocomplete'
|
||||
import { useCodeMirrorFocusReference } from './hooks/use-code-mirror-focus-reference'
|
||||
import { useOffScreenScrollProtection } from './hooks/use-off-screen-scroll-protection'
|
||||
|
||||
const logger = new Logger('EditorPane')
|
||||
|
||||
export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
|
||||
const editor = useRef<Editor>()
|
||||
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
|
||||
const codeMirrorRef = useRef<ReactCodeMirrorRef | null>(null)
|
||||
|
||||
const onPaste = useOnEditorPasteCallback()
|
||||
const onEditorScroll = useOnEditorScroll(onScroll)
|
||||
useApplyScrollState(editor, scrollState)
|
||||
useApplyScrollState(codeMirrorRef, scrollState)
|
||||
|
||||
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
||||
setNoteContent(value)
|
||||
}, [])
|
||||
const editorScrollExtension = useCodeMirrorScrollWatchExtension(onScroll)
|
||||
const editorPasteExtension = useCodeMirrorPasteExtension()
|
||||
const dropExtension = useCodeMirrorFileDropExtension()
|
||||
const [focusExtension, editorFocused] = useCodeMirrorFocusReference()
|
||||
const saveOffFocusScrollStateExtensions = useOffScreenScrollProtection()
|
||||
const cursorActivityExtension = useCursorActivityCallback(editorFocused)
|
||||
|
||||
const onBeforeChange = useCallback(
|
||||
(value: string): void => {
|
||||
if (!editorFocused.current) {
|
||||
logger.debug("Don't post content change because editor isn't focused")
|
||||
} else {
|
||||
setNoteContent(value)
|
||||
}
|
||||
},
|
||||
[editorFocused]
|
||||
)
|
||||
|
||||
const extensions = useMemo(
|
||||
() => [
|
||||
markdown({ base: markdownLanguage, codeLanguages: languages }),
|
||||
...saveOffFocusScrollStateExtensions,
|
||||
focusExtension,
|
||||
EditorView.lineWrapping,
|
||||
editorScrollExtension,
|
||||
editorPasteExtension,
|
||||
dropExtension,
|
||||
autocompletion(),
|
||||
cursorActivityExtension
|
||||
],
|
||||
[
|
||||
cursorActivityExtension,
|
||||
dropExtension,
|
||||
editorPasteExtension,
|
||||
editorScrollExtension,
|
||||
focusExtension,
|
||||
saveOffFocusScrollStateExtensions
|
||||
]
|
||||
)
|
||||
|
||||
useOnImageUploadFromRenderer()
|
||||
|
||||
const onEditorDidMount = useCallback((mountedEditor: Editor) => {
|
||||
editor.current = mountedEditor
|
||||
}, [])
|
||||
|
||||
const onCursorActivity = useCursorActivityCallback()
|
||||
const onDrop = useOnEditorFileDrop()
|
||||
const codeMirrorOptions = useCodeMirrorOptions()
|
||||
|
||||
const editorFocus = useRef<boolean>(false)
|
||||
const onFocus = useCallback(() => {
|
||||
editorFocus.current = true
|
||||
if (editor.current) {
|
||||
onCursorActivity(editor.current)
|
||||
}
|
||||
}, [editor, onCursorActivity])
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
editorFocus.current = false
|
||||
}, [])
|
||||
|
||||
const cursorActivity = useCallback(
|
||||
(editor: Editor) => {
|
||||
if (editorFocus.current) {
|
||||
onCursorActivity(editor)
|
||||
}
|
||||
},
|
||||
[onCursorActivity]
|
||||
const codeMirrorClassName = useMemo(
|
||||
() => `overflow-hidden ${styles.extendedCodemirror} h-100 ${ligaturesEnabled ? '' : styles['no-ligatures']}`,
|
||||
[ligaturesEnabled]
|
||||
)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={`d-flex flex-column h-100 position-relative`} onMouseEnter={onMakeScrollSource}>
|
||||
<MaxLengthWarning />
|
||||
<ToolBar />
|
||||
<ExtendedCodemirror
|
||||
className={`overflow-hidden w-100 flex-fill`}
|
||||
<ReactCodeMirror
|
||||
placeholder={t('editor.placeholder')}
|
||||
extensions={extensions}
|
||||
width={'100%'}
|
||||
height={'100%'}
|
||||
maxHeight={'100%'}
|
||||
maxWidth={'100%'}
|
||||
basicSetup={true}
|
||||
className={codeMirrorClassName}
|
||||
theme={oneDark}
|
||||
value={markdownContent}
|
||||
options={codeMirrorOptions}
|
||||
onPaste={onPaste}
|
||||
onDrop={onDrop}
|
||||
onCursorActivity={cursorActivity}
|
||||
editorDidMount={onEditorDidMount}
|
||||
onBeforeChange={onBeforeChange}
|
||||
onScroll={onEditorScroll}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
ligatures={ligaturesEnabled}
|
||||
onChange={onBeforeChange}
|
||||
ref={codeMirrorRef}
|
||||
/>
|
||||
<StatusBar />
|
||||
</div>
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
// codemirror addons
|
||||
import 'codemirror/addon/comment/comment'
|
||||
import 'codemirror/addon/dialog/dialog'
|
||||
import 'codemirror/addon/display/autorefresh'
|
||||
import 'codemirror/addon/display/fullscreen'
|
||||
import 'codemirror/addon/display/placeholder'
|
||||
import 'codemirror/addon/edit/closebrackets'
|
||||
import 'codemirror/addon/edit/closetag'
|
||||
import 'codemirror/addon/edit/continuelist'
|
||||
import 'codemirror/addon/edit/matchbrackets'
|
||||
import 'codemirror/addon/edit/matchtags'
|
||||
import 'codemirror/addon/fold/foldcode'
|
||||
import 'codemirror/addon/fold/foldgutter'
|
||||
import 'codemirror/addon/fold/markdown-fold'
|
||||
import 'codemirror/addon/hint/show-hint'
|
||||
import 'codemirror/addon/search/jump-to-line'
|
||||
import 'codemirror/addon/search/match-highlighter'
|
||||
import 'codemirror/addon/search/search'
|
||||
import 'codemirror/addon/selection/active-line'
|
||||
// codemirror keymaps
|
||||
import 'codemirror/keymap/emacs'
|
||||
import 'codemirror/keymap/sublime'
|
||||
import 'codemirror/keymap/vim'
|
||||
// codemirror syntax highlighting modes
|
||||
import 'codemirror/mode/apl/apl'
|
||||
import 'codemirror/mode/asciiarmor/asciiarmor'
|
||||
import 'codemirror/mode/asn.1/asn.1'
|
||||
import 'codemirror/mode/asterisk/asterisk'
|
||||
import 'codemirror/mode/brainfuck/brainfuck'
|
||||
import 'codemirror/mode/clike/clike'
|
||||
import 'codemirror/mode/clojure/clojure'
|
||||
import 'codemirror/mode/cmake/cmake'
|
||||
import 'codemirror/mode/cobol/cobol'
|
||||
import 'codemirror/mode/coffeescript/coffeescript'
|
||||
import 'codemirror/mode/commonlisp/commonlisp'
|
||||
import 'codemirror/mode/crystal/crystal'
|
||||
import 'codemirror/mode/css/css'
|
||||
import 'codemirror/mode/cypher/cypher'
|
||||
import 'codemirror/mode/d/d'
|
||||
import 'codemirror/mode/dart/dart'
|
||||
import 'codemirror/mode/diff/diff'
|
||||
import 'codemirror/mode/django/django'
|
||||
import 'codemirror/mode/dockerfile/dockerfile'
|
||||
import 'codemirror/mode/dtd/dtd'
|
||||
import 'codemirror/mode/dylan/dylan'
|
||||
import 'codemirror/mode/ebnf/ebnf'
|
||||
import 'codemirror/mode/ecl/ecl'
|
||||
import 'codemirror/mode/eiffel/eiffel'
|
||||
import 'codemirror/mode/elm/elm'
|
||||
import 'codemirror/mode/erlang/erlang'
|
||||
import 'codemirror/mode/factor/factor'
|
||||
import 'codemirror/mode/fcl/fcl'
|
||||
import 'codemirror/mode/forth/forth'
|
||||
import 'codemirror/mode/fortran/fortran'
|
||||
import 'codemirror/mode/gas/gas'
|
||||
import 'codemirror/mode/gfm/gfm'
|
||||
import 'codemirror/mode/gherkin/gherkin'
|
||||
import 'codemirror/mode/go/go'
|
||||
import 'codemirror/mode/groovy/groovy'
|
||||
import 'codemirror/mode/haml/haml'
|
||||
import 'codemirror/mode/handlebars/handlebars'
|
||||
import 'codemirror/mode/haskell/haskell'
|
||||
import 'codemirror/mode/haskell-literate/haskell-literate'
|
||||
import 'codemirror/mode/haxe/haxe'
|
||||
import 'codemirror/mode/htmlembedded/htmlembedded'
|
||||
import 'codemirror/mode/htmlmixed/htmlmixed'
|
||||
import 'codemirror/mode/http/http'
|
||||
import 'codemirror/mode/idl/idl'
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/mode/jinja2/jinja2'
|
||||
import 'codemirror/mode/jsx/jsx'
|
||||
import 'codemirror/mode/julia/julia'
|
||||
import 'codemirror/mode/livescript/livescript'
|
||||
import 'codemirror/mode/lua/lua'
|
||||
import 'codemirror/mode/markdown/markdown'
|
||||
import 'codemirror/mode/mathematica/mathematica'
|
||||
import 'codemirror/mode/mbox/mbox'
|
||||
import 'codemirror/mode/mirc/mirc'
|
||||
import 'codemirror/mode/mllike/mllike'
|
||||
import 'codemirror/mode/modelica/modelica'
|
||||
import 'codemirror/mode/mscgen/mscgen'
|
||||
import 'codemirror/mode/mumps/mumps'
|
||||
import 'codemirror/mode/nginx/nginx'
|
||||
import 'codemirror/mode/nsis/nsis'
|
||||
import 'codemirror/mode/ntriples/ntriples'
|
||||
import 'codemirror/mode/octave/octave'
|
||||
import 'codemirror/mode/oz/oz'
|
||||
import 'codemirror/mode/pascal/pascal'
|
||||
import 'codemirror/mode/pegjs/pegjs'
|
||||
import 'codemirror/mode/perl/perl'
|
||||
import 'codemirror/mode/php/php'
|
||||
import 'codemirror/mode/pig/pig'
|
||||
import 'codemirror/mode/powershell/powershell'
|
||||
import 'codemirror/mode/properties/properties'
|
||||
import 'codemirror/mode/protobuf/protobuf'
|
||||
import 'codemirror/mode/pug/pug'
|
||||
import 'codemirror/mode/puppet/puppet'
|
||||
import 'codemirror/mode/python/python'
|
||||
import 'codemirror/mode/q/q'
|
||||
import 'codemirror/mode/r/r'
|
||||
import 'codemirror/mode/rpm/rpm'
|
||||
import 'codemirror/mode/rst/rst'
|
||||
import 'codemirror/mode/ruby/ruby'
|
||||
import 'codemirror/mode/rust/rust'
|
||||
import 'codemirror/mode/sas/sas'
|
||||
import 'codemirror/mode/sass/sass'
|
||||
import 'codemirror/mode/scheme/scheme'
|
||||
import 'codemirror/mode/shell/shell'
|
||||
import 'codemirror/mode/sieve/sieve'
|
||||
import 'codemirror/mode/slim/slim'
|
||||
import 'codemirror/mode/smalltalk/smalltalk'
|
||||
import 'codemirror/mode/smarty/smarty'
|
||||
import 'codemirror/mode/solr/solr'
|
||||
import 'codemirror/mode/soy/soy'
|
||||
import 'codemirror/mode/sparql/sparql'
|
||||
import 'codemirror/mode/spreadsheet/spreadsheet'
|
||||
import 'codemirror/mode/sql/sql'
|
||||
import 'codemirror/mode/stex/stex'
|
||||
import 'codemirror/mode/stylus/stylus'
|
||||
import 'codemirror/mode/swift/swift'
|
||||
import 'codemirror/mode/tcl/tcl'
|
||||
import 'codemirror/mode/textile/textile'
|
||||
import 'codemirror/mode/tiddlywiki/tiddlywiki'
|
||||
import 'codemirror/mode/tiki/tiki'
|
||||
import 'codemirror/mode/toml/toml'
|
||||
import 'codemirror/mode/tornado/tornado'
|
||||
import 'codemirror/mode/troff/troff'
|
||||
import 'codemirror/mode/ttcn/ttcn'
|
||||
import 'codemirror/mode/ttcn-cfg/ttcn-cfg'
|
||||
import 'codemirror/mode/turtle/turtle'
|
||||
import 'codemirror/mode/twig/twig'
|
||||
import 'codemirror/mode/vb/vb'
|
||||
import 'codemirror/mode/vbscript/vbscript'
|
||||
import 'codemirror/mode/velocity/velocity'
|
||||
import 'codemirror/mode/verilog/verilog'
|
||||
import 'codemirror/mode/vhdl/vhdl'
|
||||
import 'codemirror/mode/vue/vue'
|
||||
import 'codemirror/mode/wast/wast'
|
||||
import 'codemirror/mode/webidl/webidl'
|
||||
import 'codemirror/mode/xml/xml'
|
||||
import 'codemirror/mode/xquery/xquery'
|
||||
import 'codemirror/mode/yacas/yacas'
|
||||
import 'codemirror/mode/yaml/yaml'
|
||||
import 'codemirror/mode/yaml-frontmatter/yaml-frontmatter'
|
||||
import 'codemirror/mode/z80/z80'
|
|
@ -4,44 +4,15 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.extended-codemirror {
|
||||
:global {
|
||||
@import '~codemirror/lib/codemirror';
|
||||
@import '~codemirror/addon/display/fullscreen';
|
||||
@import '~codemirror/addon/fold/foldgutter';
|
||||
@import '~codemirror/addon/dialog/dialog';
|
||||
@import '~codemirror/theme/neat';
|
||||
@import './one-dark';
|
||||
@import './hints';
|
||||
|
||||
.CodeMirror {
|
||||
& {
|
||||
@import '../../../../../global-styles/variables.module';
|
||||
font-family: "Fira Code", $font-family-emojis, Consolas, monaco, monospace;
|
||||
}
|
||||
letter-spacing: 0.025em;
|
||||
line-height: 1.25;
|
||||
font-size: 18px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-drag .CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
.extendedCodemirror {
|
||||
:global(.cm-editor .cm-line) {
|
||||
@import '../../../../../global-styles/variables.module';
|
||||
font-family: "Fira Code", $font-family-emojis, Consolas, monaco, monospace;
|
||||
}
|
||||
|
||||
&.no-ligatures {
|
||||
:global {
|
||||
.CodeMirror {
|
||||
|
||||
.CodeMirror-line, .CodeMirror-line-like {
|
||||
font-feature-settings: inherit;
|
||||
}
|
||||
|
||||
.CodeMirror-line, .CodeMirror-line-like {
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
}
|
||||
:global(.cm-editor .cm-line) {
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import type { IControlledCodeMirror } from 'react-codemirror2'
|
||||
import { Controlled } from 'react-codemirror2'
|
||||
import './codemirror-imports'
|
||||
import styles from './codemirror.module.scss'
|
||||
import { allHinters, findWordAtCursor } from '../autocompletion'
|
||||
import type { Editor } from 'codemirror'
|
||||
|
||||
export interface ExtendedCodemirrorProps extends Omit<IControlledCodeMirror, 'onChange'> {
|
||||
ligatures?: boolean
|
||||
}
|
||||
|
||||
const onChange = (editor: Editor) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
for (const hinter of allHinters) {
|
||||
if (hinter.wordRegExp.test(searchTerm.text)) {
|
||||
editor.showHint({
|
||||
container: editor.getWrapperElement(),
|
||||
hint: hinter.hint,
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: false,
|
||||
alignWithWord: true
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Controlled controlled code mirror} but with several addons, different font, ligatures and other improvements.
|
||||
*
|
||||
* @param className Additional css class names that should be added to the component
|
||||
* @param ligatures Renders text ligatures if {@code true}
|
||||
* @param props Other code mirror props that will be forwarded to the editor
|
||||
*/
|
||||
export const ExtendedCodemirror: React.FC<ExtendedCodemirrorProps> = ({ className, ligatures, ...props }) => {
|
||||
return (
|
||||
<Controlled
|
||||
className={`${className ?? ''} ${ligatures ? '' : styles['no-ligatures']} ${styles['extended-codemirror']}`}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { handleUpload } from '../../upload-handler'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
import Optional from 'optional-js'
|
||||
|
||||
/**
|
||||
* Creates a callback that is used to process file drops on the code mirror editor
|
||||
*
|
||||
* @return the code mirror callback
|
||||
*/
|
||||
export const useCodeMirrorFileDropExtension = (): Extension => {
|
||||
const onDrop = useCallback((event: DragEvent, view: EditorView): void => {
|
||||
if (!event.pageX || !event.pageY) {
|
||||
return
|
||||
}
|
||||
Optional.ofNullable(event.dataTransfer?.files)
|
||||
.filter((files) => files.length > 0)
|
||||
.ifPresent((files) => {
|
||||
event.preventDefault()
|
||||
const newCursor = view.posAtCoords({ y: event.pageY, x: event.pageX })
|
||||
if (newCursor === null) {
|
||||
return
|
||||
}
|
||||
handleUpload(files[0], { from: newCursor })
|
||||
})
|
||||
}, [])
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
drop: onDrop
|
||||
}),
|
||||
[onDrop]
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { handleFilePaste, handleTablePaste } from '../../tool-bar/utils/pasteHandlers'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
/**
|
||||
* Creates a {@link Extension code mirror extension} that handles the table or file paste action.
|
||||
*
|
||||
* @return the created {@link Extension code mirror extension}
|
||||
*/
|
||||
export const useCodeMirrorPasteExtension = (): Extension => {
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
paste: (event: ClipboardEvent) => {
|
||||
const clipboardData = event.clipboardData
|
||||
if (!clipboardData) {
|
||||
return
|
||||
}
|
||||
if (handleTablePaste(clipboardData) || handleFilePaste(clipboardData)) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { ScrollState } from '../../../synced-scroll/scroll-props'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
export type OnScrollCallback = ((scrollState: ScrollState) => void) | undefined
|
||||
|
||||
/**
|
||||
* Extracts the {@link ScrollState scroll state} from the given {@link EditorView editor view}.
|
||||
*
|
||||
* @param view The {@link EditorView editor view} whose scroll state should be extracted.
|
||||
*/
|
||||
export const extractScrollState = (view: EditorView): ScrollState => {
|
||||
const state = view.state
|
||||
const scrollTop = view.scrollDOM.scrollTop
|
||||
const lineBlockAtHeight = view.lineBlockAtHeight(scrollTop)
|
||||
const line = state.doc.lineAt(lineBlockAtHeight.from)
|
||||
const percentageRaw = (scrollTop - lineBlockAtHeight.top) / lineBlockAtHeight.height
|
||||
const scrolledPercentage = Math.floor(percentageRaw * 100)
|
||||
return {
|
||||
firstLineInView: line.number,
|
||||
scrolledPercentage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a code mirror extension for the scroll binding.
|
||||
* It calculates a {@link ScrollState} and posts it on change.
|
||||
*
|
||||
* @param onScroll The callback that is used to post {@link ScrollState scroll states} when the editor view is scrolling.
|
||||
* @return The extensions that watches the scrolling in the editor.
|
||||
*/
|
||||
export const useCodeMirrorScrollWatchExtension = (onScroll: OnScrollCallback): Extension => {
|
||||
const onEditorScroll = useCallback(
|
||||
(view: EditorView) => {
|
||||
if (!onScroll || !view) {
|
||||
return undefined
|
||||
}
|
||||
onScroll(extractScrollState(view))
|
||||
},
|
||||
[onScroll]
|
||||
)
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
scroll: (event, view) => onEditorScroll(view)
|
||||
}),
|
||||
[onEditorScroll]
|
||||
)
|
||||
}
|
|
@ -6,8 +6,24 @@
|
|||
|
||||
import type { MutableRefObject } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { Editor } from 'codemirror'
|
||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
||||
import type { ReactCodeMirrorRef } from '@uiw/react-codemirror'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import equal from 'fast-deep-equal'
|
||||
|
||||
/**
|
||||
* Applies the given {@link ScrollState scroll state} to the given {@link EditorView code mirror editor view}.
|
||||
*
|
||||
* @param view The {@link EditorView view} that should be scrolled
|
||||
* @param scrollState The {@link ScrollState scroll state} that should be applied
|
||||
*/
|
||||
export const applyScrollState = (view: EditorView, scrollState: ScrollState): void => {
|
||||
const line = view.state.doc.line(scrollState.firstLineInView)
|
||||
const lineBlock = view.lineBlockAt(line.from)
|
||||
const margin = Math.floor(lineBlock.height * scrollState.scrolledPercentage) / 100
|
||||
const stateEffect = EditorView.scrollIntoView(line.from, { y: 'start', yMargin: -margin })
|
||||
view.dispatch({ effects: [stateEffect] })
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the given scroll state and scrolls the editor to the state if changed.
|
||||
|
@ -16,22 +32,21 @@ import type { ScrollState } from '../../synced-scroll/scroll-props'
|
|||
* @param scrollState The scroll state that should be monitored
|
||||
*/
|
||||
export const useApplyScrollState = (
|
||||
editorRef: MutableRefObject<Editor | undefined>,
|
||||
editorRef: MutableRefObject<ReactCodeMirrorRef | null>,
|
||||
scrollState?: ScrollState
|
||||
): void => {
|
||||
const lastScrollPosition = useRef<number>()
|
||||
const lastScrollPosition = useRef<ScrollState>()
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current
|
||||
if (!editor || !scrollState) {
|
||||
const view = editorRef.current?.view
|
||||
if (!view || !scrollState) {
|
||||
return
|
||||
}
|
||||
const startYOfLine = editor.heightAtLine(scrollState.firstLineInView - 1, 'local')
|
||||
const heightOfLine = (editor.lineInfo(scrollState.firstLineInView - 1).handle as { height: number }).height
|
||||
const newPositionRaw = startYOfLine + (heightOfLine * scrollState.scrolledPercentage) / 100
|
||||
const newPosition = Math.floor(newPositionRaw)
|
||||
if (newPosition !== lastScrollPosition.current) {
|
||||
lastScrollPosition.current = newPosition
|
||||
editor.scrollTo(0, newPosition)
|
||||
|
||||
if (equal(scrollState, lastScrollPosition.current)) {
|
||||
return
|
||||
}
|
||||
applyScrollState(view, scrollState)
|
||||
lastScrollPosition.current = scrollState
|
||||
}, [editorRef, scrollState])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
/**
|
||||
* Creates a {@link RefObject<boolean> reference} that contains the information if the editor is currently focused or not.
|
||||
*
|
||||
* @returns The reference and the necessary {@link Extension code mirror extension} that receives the focus and blur events
|
||||
*/
|
||||
export const useCodeMirrorFocusReference = (): [Extension, RefObject<boolean>] => {
|
||||
const focusReference = useRef<boolean>(false)
|
||||
const codeMirrorExtension = useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
blur: () => {
|
||||
focusReference.current = false
|
||||
},
|
||||
focus: () => {
|
||||
focusReference.current = true
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return [codeMirrorExtension, focusReference]
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { EditorConfiguration } from 'codemirror'
|
||||
import { useMemo } from 'react'
|
||||
import { createDefaultKeyMap } from '../key-map'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Generates the configuration for a CodeMirror instance.
|
||||
*/
|
||||
export const useCodeMirrorOptions = (): EditorConfiguration => {
|
||||
const editorPreferences = useApplicationState((state) => state.editorConfig.preferences)
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo<EditorConfiguration>(
|
||||
() => ({
|
||||
...editorPreferences,
|
||||
mode: 'gfm',
|
||||
viewportMargin: 20,
|
||||
styleActiveLine: true,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
showCursorWhenSelecting: true,
|
||||
highlightSelectionMatches: true,
|
||||
inputStyle: 'textarea',
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true,
|
||||
matchTags: {
|
||||
bothTags: true
|
||||
},
|
||||
autoCloseTags: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'],
|
||||
extraKeys: createDefaultKeyMap(),
|
||||
flattenSpans: true,
|
||||
addModeClass: true,
|
||||
autoRefresh: true,
|
||||
// otherCursors: true,
|
||||
placeholder: t('editor.placeholder')
|
||||
}),
|
||||
[t, editorPreferences]
|
||||
)
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { StatusBarInfo } from '../status-bar/status-bar'
|
||||
import { useMemo } from 'react'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* Provides a {@link StatusBarInfo} object and a function that can update this object using a {@link CodeMirror code mirror instance}.
|
||||
*/
|
||||
export const useCreateStatusBarInfo = (): StatusBarInfo => {
|
||||
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
const markdownContent = useApplicationState((state) => state.noteDetails.markdownContent)
|
||||
const markdownContentLines = useApplicationState((state) => state.noteDetails.markdownContentLines)
|
||||
|
||||
return useMemo(() => {
|
||||
const startCharacter = selection.from.character
|
||||
const endCharacter = selection.to?.character ?? 0
|
||||
const startLine = selection.from.line
|
||||
const endLine = selection.to?.line ?? 0
|
||||
|
||||
return {
|
||||
position: { line: startLine, character: startCharacter },
|
||||
charactersInDocument: markdownContent.length,
|
||||
remainingCharacters: maxDocumentLength - markdownContent.length,
|
||||
linesInDocument: markdownContentLines.length,
|
||||
selectedColumns: endCharacter - startCharacter,
|
||||
selectedLines: endLine - startLine
|
||||
}
|
||||
}, [markdownContent.length, markdownContentLines.length, maxDocumentLength, selection])
|
||||
}
|
|
@ -4,27 +4,36 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor } from 'codemirror'
|
||||
import { useCallback } from 'react'
|
||||
import type { CursorPosition } from '../../../../redux/editor/types'
|
||||
import type { RefObject } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { updateCursorPositions } from '../../../../redux/note-details/methods'
|
||||
import type { ViewUpdate } from '@codemirror/view'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
const logger = new Logger('useCursorActivityCallback')
|
||||
|
||||
/**
|
||||
* Provides a callback for codemirror that handles cursor changes
|
||||
*
|
||||
* @return the generated callback
|
||||
*/
|
||||
export const useCursorActivityCallback = (): ((editor: Editor) => void) => {
|
||||
return useCallback((editor) => {
|
||||
const firstSelection = editor.listSelections()[0]
|
||||
if (firstSelection === undefined) {
|
||||
return
|
||||
}
|
||||
const start: CursorPosition = { line: firstSelection.from().line, character: firstSelection.from().ch }
|
||||
const end: CursorPosition = { line: firstSelection.to().line, character: firstSelection.to().ch }
|
||||
updateCursorPositions({
|
||||
from: start,
|
||||
to: start.line === end.line && start.character === end.character ? undefined : end
|
||||
})
|
||||
}, [])
|
||||
export const useCursorActivityCallback = (editorFocused: RefObject<boolean>): Extension => {
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate): void => {
|
||||
if (!editorFocused.current) {
|
||||
logger.debug("Don't post updated cursor because editor isn't focused")
|
||||
return
|
||||
}
|
||||
const firstSelection = viewUpdate.state.selection.main
|
||||
const newCursorPos = {
|
||||
from: firstSelection.from,
|
||||
to: firstSelection.to === firstSelection.from ? undefined : firstSelection.to
|
||||
}
|
||||
updateCursorPositions(newCursorPos)
|
||||
}),
|
||||
[editorFocused]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
||||
export interface LineBasedPosition {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
|
||||
const calculateLineBasedPosition = (absolutePosition: number, lineStartIndexes: number[]): LineBasedPosition => {
|
||||
const foundLineIndex = lineStartIndexes.findIndex((startIndex) => absolutePosition < startIndex)
|
||||
const line = foundLineIndex === -1 ? lineStartIndexes.length - 1 : foundLineIndex - 1
|
||||
return {
|
||||
line: line,
|
||||
character: absolutePosition - lineStartIndexes[line]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the line+character based position of the to cursor, if available.
|
||||
*/
|
||||
export const useLineBasedToPosition = (): LineBasedPosition | undefined => {
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
|
||||
return useMemo(() => {
|
||||
const to = selection.to
|
||||
if (to === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return calculateLineBasedPosition(to, lineStartIndexes)
|
||||
}, [selection.to, lineStartIndexes])
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the line+character based position of the from cursor.
|
||||
*/
|
||||
export const useLineBasedFromPosition = (): LineBasedPosition => {
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
|
||||
return useMemo(() => {
|
||||
return calculateLineBasedPosition(selection.from, lineStartIndexes)
|
||||
}, [selection.from, lineStartIndexes])
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo, useRef } from 'react'
|
||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
||||
import { extractScrollState } from './code-mirror-extensions/use-code-mirror-scroll-watch-extension'
|
||||
import { applyScrollState } from './use-apply-scroll-state'
|
||||
import { store } from '../../../../redux'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
|
||||
const logger = new Logger('useOffScreenScrollProtection')
|
||||
|
||||
/**
|
||||
* If the editor content changes while the editor isn't focused then the editor starts jumping around.
|
||||
* This extension fixes this behaviour by saving the scroll state when the editor looses focus and applies it on content changes.
|
||||
*
|
||||
* @returns necessary {@link Extension code mirror extensions} to provide the functionality
|
||||
*/
|
||||
export const useOffScreenScrollProtection = (): Extension[] => {
|
||||
const offFocusScrollState = useRef<ScrollState>()
|
||||
|
||||
return useMemo(() => {
|
||||
const saveOffFocusScrollStateExtension = EditorView.domEventHandlers({
|
||||
blur: (event, view) => {
|
||||
offFocusScrollState.current = extractScrollState(view)
|
||||
logger.debug('Save off-focus scroll state', offFocusScrollState.current)
|
||||
},
|
||||
focus: () => {
|
||||
offFocusScrollState.current = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const changeExtension = EditorView.updateListener.of((update) => {
|
||||
const view = update.view
|
||||
const scrollState = offFocusScrollState.current
|
||||
if (!scrollState || !update.docChanged) {
|
||||
return
|
||||
}
|
||||
logger.debug('Apply off-focus scroll state', scrollState)
|
||||
applyScrollState(view, scrollState)
|
||||
const selection = store.getState().noteDetails.selection
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
selection: {
|
||||
anchor: selection.from,
|
||||
head: selection.to
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return [saveOffFocusScrollStateExtension, changeExtension]
|
||||
}, [])
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import type { Editor } from 'codemirror'
|
||||
import { handleUpload } from '../upload-handler'
|
||||
import type { DomEvent } from 'react-codemirror2'
|
||||
|
||||
interface DropEvent {
|
||||
pageX: number
|
||||
pageY: number
|
||||
dataTransfer: {
|
||||
files: FileList
|
||||
effectAllowed: string
|
||||
} | null
|
||||
preventDefault: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callback that is used to process file drops on the code mirror editor
|
||||
*
|
||||
* @return the code mirror callback
|
||||
*/
|
||||
export const useOnEditorFileDrop = (): DomEvent => {
|
||||
return useCallback((dropEditor: Editor, event: DropEvent) => {
|
||||
if (
|
||||
event &&
|
||||
dropEditor &&
|
||||
event.pageX &&
|
||||
event.pageY &&
|
||||
event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files.length >= 1
|
||||
) {
|
||||
event.preventDefault()
|
||||
const newCursor = dropEditor.coordsChar({ top: event.pageY, left: event.pageX }, 'page')
|
||||
dropEditor.setCursor(newCursor)
|
||||
const files: FileList = event.dataTransfer.files
|
||||
handleUpload(files[0])
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import type { Editor } from 'codemirror'
|
||||
import type { PasteEvent } from '../tool-bar/utils/pasteHandlers'
|
||||
import { handleFilePaste, handleTablePaste } from '../tool-bar/utils/pasteHandlers'
|
||||
import type { DomEvent } from 'react-codemirror2'
|
||||
|
||||
/**
|
||||
* Creates a callback that handles the table or file paste action in code mirror.
|
||||
*
|
||||
* @return the created callback
|
||||
*/
|
||||
export const useOnEditorPasteCallback = (): DomEvent => {
|
||||
return useCallback((pasteEditor: Editor, event: PasteEvent) => {
|
||||
if (!event || !event.clipboardData) {
|
||||
return
|
||||
}
|
||||
if (handleTablePaste(event) || handleFilePaste(event)) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { DomEvent } from 'react-codemirror2'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { Editor, ScrollInfo } from 'codemirror'
|
||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
||||
|
||||
/**
|
||||
* Creates a callback for the scroll binding of the code mirror editor.
|
||||
* It calculates a {@link ScrollState} and posts it on change.
|
||||
*
|
||||
* @param onScroll The callback that is used to post the {@link ScrolLState}.
|
||||
* @return The callback for the code mirror scroll binding.
|
||||
*/
|
||||
export const useOnEditorScroll = (onScroll?: (scrollState: ScrollState) => void): DomEvent => {
|
||||
const [editorScrollState, setEditorScrollState] = useState<ScrollState>()
|
||||
|
||||
useEffect(() => {
|
||||
if (onScroll && editorScrollState) {
|
||||
onScroll(editorScrollState)
|
||||
}
|
||||
}, [editorScrollState, onScroll])
|
||||
|
||||
return useCallback(
|
||||
(editor: Editor, scrollInfo: ScrollInfo) => {
|
||||
if (!editor || !onScroll || !scrollInfo) {
|
||||
return
|
||||
}
|
||||
const line = editor.lineAtHeight(scrollInfo.top, 'local')
|
||||
const startYOfLine = editor.heightAtLine(line, 'local')
|
||||
const lineInfo = editor.lineInfo(line)
|
||||
if (lineInfo === null) {
|
||||
return
|
||||
}
|
||||
const heightOfLine = (lineInfo.handle as { height: number }).height
|
||||
const percentageRaw = Math.max(scrollInfo.top - startYOfLine, 0) / heightOfLine
|
||||
const percentage = Math.floor(percentageRaw * 100)
|
||||
|
||||
setEditorScrollState({ firstLineInView: line + 1, scrolledPercentage: percentage })
|
||||
},
|
||||
[onScroll]
|
||||
)
|
||||
}
|
|
@ -36,7 +36,7 @@ export const useOnImageUploadFromRenderer = (): void => {
|
|||
.then((blob) => {
|
||||
const file = new File([blob], fileName, { type: blob.type })
|
||||
const { cursorSelection, alt, title } = Optional.ofNullable(lineIndex)
|
||||
.map((actualLineIndex) => findPlaceholderInMarkdownContent(actualLineIndex, placeholderIndexInLine))
|
||||
.flatMap((actualLineIndex) => findPlaceholderInMarkdownContent(actualLineIndex, placeholderIndexInLine))
|
||||
.orElseGet(() => ({}))
|
||||
handleUpload(file, cursorSelection, alt, title)
|
||||
})
|
||||
|
@ -58,26 +58,25 @@ export interface ExtractResult {
|
|||
* @param replacementIndexInLine If multiple image placeholders are present in the target line then this number describes the index of the wanted placeholder.
|
||||
* @return the calculated start and end position or undefined if no position could be determined
|
||||
*/
|
||||
const findPlaceholderInMarkdownContent = (lineIndex: number, replacementIndexInLine = 0): ExtractResult | undefined => {
|
||||
const currentMarkdownContentLines = getGlobalState().noteDetails.markdownContent.split('\n')
|
||||
const lineAtIndex = currentMarkdownContentLines[lineIndex]
|
||||
if (lineAtIndex === undefined) {
|
||||
return
|
||||
}
|
||||
return findImagePlaceholderInLine(currentMarkdownContentLines[lineIndex], lineIndex, replacementIndexInLine)
|
||||
const findPlaceholderInMarkdownContent = (lineIndex: number, replacementIndexInLine = 0): Optional<ExtractResult> => {
|
||||
const noteDetails = getGlobalState().noteDetails
|
||||
const currentMarkdownContentLines = noteDetails.markdownContent.lines
|
||||
return Optional.ofNullable(noteDetails.markdownContent.lineStartIndexes[lineIndex]).map((startIndexOfLine) =>
|
||||
findImagePlaceholderInLine(currentMarkdownContentLines[lineIndex], startIndexOfLine, replacementIndexInLine)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the right image placeholder in the given line.
|
||||
*
|
||||
* @param line The line that should be inspected
|
||||
* @param lineIndex The index of the line in the document
|
||||
* @param startIndexOfLine The absolute start index of the line in the document
|
||||
* @param replacementIndexInLine If multiple image placeholders are present in the target line then this number describes the index of the wanted placeholder.
|
||||
* @return the calculated start and end position or undefined if no position could be determined
|
||||
*/
|
||||
const findImagePlaceholderInLine = (
|
||||
line: string,
|
||||
lineIndex: number,
|
||||
startIndexOfLine: number,
|
||||
replacementIndexInLine = 0
|
||||
): ExtractResult | undefined => {
|
||||
const startOfImageTag = findRegexMatchInText(line, imageWithPlaceholderLinkRegex, replacementIndexInLine)
|
||||
|
@ -85,16 +84,12 @@ const findImagePlaceholderInLine = (
|
|||
return
|
||||
}
|
||||
|
||||
const from = startIndexOfLine + startOfImageTag.index
|
||||
const to = from + startOfImageTag[0].length
|
||||
return {
|
||||
cursorSelection: {
|
||||
from: {
|
||||
character: startOfImageTag.index,
|
||||
line: lineIndex
|
||||
},
|
||||
to: {
|
||||
character: startOfImageTag.index + startOfImageTag[0].length,
|
||||
line: lineIndex
|
||||
}
|
||||
from,
|
||||
to
|
||||
},
|
||||
alt: startOfImageTag[1],
|
||||
title: startOfImageTag[2]
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor, KeyMap, Pass } from 'codemirror'
|
||||
import CodeMirror from 'codemirror'
|
||||
import { isMac } from '../utils'
|
||||
import { formatSelection } from '../../../redux/note-details/methods'
|
||||
import { FormatType } from '../../../redux/note-details/types'
|
||||
|
||||
const isVim = (keyMapName?: string) => keyMapName?.substr(0, 3) === 'vim'
|
||||
|
||||
const f10 = (editor: Editor): void | typeof Pass => editor.setOption('fullScreen', !editor.getOption('fullScreen'))
|
||||
const esc = (editor: Editor): void | typeof Pass => {
|
||||
if (editor.getOption('fullScreen') && !isVim(editor.getOption('keyMap'))) {
|
||||
editor.setOption('fullScreen', false)
|
||||
} else {
|
||||
return CodeMirror.Pass
|
||||
}
|
||||
}
|
||||
const suppressKey = (): void => {
|
||||
/*no op*/
|
||||
}
|
||||
const tab = (editor: Editor) => {
|
||||
const tab = '\t'
|
||||
|
||||
// contruct x length spaces
|
||||
const spaces = Array((editor.getOption('indentUnit') ?? 0) + 1).join(' ')
|
||||
|
||||
// auto indent whole line when in list or blockquote
|
||||
const cursor = editor.getCursor()
|
||||
const line = editor.getLine(cursor.line)
|
||||
|
||||
// this regex match the following patterns
|
||||
// 1. blockquote starts with "> " or ">>"
|
||||
// 2. unorder list starts with *+-parseInt
|
||||
// 3. order list starts with "1." or "1)"
|
||||
const regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/
|
||||
|
||||
let match
|
||||
const multiple = editor.getSelection().split('\n').length > 1 || editor.getSelections().length > 1
|
||||
|
||||
if (multiple) {
|
||||
editor.execCommand('defaultTab')
|
||||
} else if ((match = regex.exec(line)) !== null) {
|
||||
const ch = match[1].length
|
||||
const pos = {
|
||||
line: cursor.line,
|
||||
ch: ch
|
||||
}
|
||||
if (editor.getOption('indentWithTabs')) {
|
||||
editor.replaceRange(tab, pos, pos, '+input')
|
||||
} else {
|
||||
editor.replaceRange(spaces, pos, pos, '+input')
|
||||
}
|
||||
} else {
|
||||
if (editor.getOption('indentWithTabs')) {
|
||||
editor.execCommand('defaultTab')
|
||||
} else {
|
||||
editor.replaceSelection(spaces)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createDefaultKeyMap: () => KeyMap = () => {
|
||||
if (isMac()) {
|
||||
return {
|
||||
F9: suppressKey,
|
||||
F10: f10,
|
||||
Esc: esc,
|
||||
'Cmd-S': suppressKey,
|
||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||
Tab: tab,
|
||||
'Cmd-Left': 'goLineLeftSmart',
|
||||
'Cmd-Right': 'goLineRight',
|
||||
Home: 'goLineLeftSmart',
|
||||
End: 'goLineRight',
|
||||
'Cmd-I': () => formatSelection(FormatType.ITALIC),
|
||||
'Cmd-B': () => formatSelection(FormatType.BOLD),
|
||||
'Cmd-U': () => formatSelection(FormatType.UNDERLINE),
|
||||
'Cmd-D': () => formatSelection(FormatType.STRIKETHROUGH),
|
||||
'Cmd-M': () => formatSelection(FormatType.HIGHLIGHT)
|
||||
} as KeyMap
|
||||
} else {
|
||||
return {
|
||||
F9: suppressKey,
|
||||
F10: f10,
|
||||
Esc: esc,
|
||||
'Ctrl-S': suppressKey,
|
||||
Enter: 'newlineAndIndentContinueMarkdownList',
|
||||
Tab: tab,
|
||||
Home: 'goLineLeftSmart',
|
||||
End: 'goLineRight',
|
||||
'Ctrl-I': () => formatSelection(FormatType.ITALIC),
|
||||
'Ctrl-B': () => formatSelection(FormatType.BOLD),
|
||||
'Ctrl-U': () => formatSelection(FormatType.UNDERLINE),
|
||||
'Ctrl-D': () => formatSelection(FormatType.STRIKETHROUGH),
|
||||
'Ctrl-M': () => formatSelection(FormatType.HIGHLIGHT),
|
||||
'Ctrl-K': () => formatSelection(FormatType.LINK)
|
||||
} as KeyMap
|
||||
}
|
||||
}
|
|
@ -6,27 +6,23 @@
|
|||
|
||||
import React, { useMemo } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import type { CursorPosition } from '../../../../redux/editor/types'
|
||||
|
||||
export interface CursorPositionInfoProps {
|
||||
cursorPosition: CursorPosition
|
||||
}
|
||||
import { useLineBasedFromPosition } from '../hooks/use-line-based-position'
|
||||
|
||||
/**
|
||||
* Renders a translated text that shows the given cursor position.
|
||||
*
|
||||
* @param cursorPosition The cursor position that should be included
|
||||
*/
|
||||
export const CursorPositionInfo: React.FC<CursorPositionInfoProps> = ({ cursorPosition }) => {
|
||||
export const CursorPositionInfo: React.FC = () => {
|
||||
const lineBasedPosition = useLineBasedFromPosition()
|
||||
|
||||
const translationOptions = useMemo(
|
||||
() => ({
|
||||
line: cursorPosition.line + 1,
|
||||
columns: cursorPosition.character + 1
|
||||
line: lineBasedPosition.line + 1,
|
||||
columns: lineBasedPosition.character + 1
|
||||
}),
|
||||
[cursorPosition.character, cursorPosition.line]
|
||||
[lineBasedPosition]
|
||||
)
|
||||
|
||||
return (
|
||||
return translationOptions === undefined ? null : (
|
||||
<span>
|
||||
<Trans i18nKey={'editor.statusBar.cursor'} values={translationOptions} />
|
||||
</span>
|
||||
|
|
|
@ -6,20 +6,18 @@
|
|||
|
||||
import React, { useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
export interface LinesInDocumentInfoProps {
|
||||
numberOfLinesInDocument: number
|
||||
}
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* Renders a translated text that shows the number of lines in the document.
|
||||
*
|
||||
* @param linesInDocument The number of lines in the document
|
||||
*/
|
||||
export const NumberOfLinesInDocumentInfo: React.FC<LinesInDocumentInfoProps> = ({ numberOfLinesInDocument }) => {
|
||||
export const NumberOfLinesInDocumentInfo: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const translationOptions = useMemo(() => ({ lines: numberOfLinesInDocument }), [numberOfLinesInDocument])
|
||||
const linesInDocument = useApplicationState((state) => state.noteDetails.markdownContent.lines.length)
|
||||
const translationOptions = useMemo(() => ({ lines: linesInDocument }), [linesInDocument])
|
||||
|
||||
return (
|
||||
<span>
|
||||
|
|
|
@ -7,11 +7,7 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
export interface LengthInfoProps {
|
||||
remainingCharacters: number
|
||||
charactersInDocument: number
|
||||
}
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* Renders a translated text that shows the number of remaining characters.
|
||||
|
@ -19,9 +15,13 @@ export interface LengthInfoProps {
|
|||
* @param remainingCharacters The number of characters that are still available in this document
|
||||
* @param charactersInDocument The total number of characters in the document
|
||||
*/
|
||||
export const RemainingCharactersInfo: React.FC<LengthInfoProps> = ({ remainingCharacters, charactersInDocument }) => {
|
||||
export const RemainingCharactersInfo: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
|
||||
const contentLength = useApplicationState((state) => state.noteDetails.markdownContent.plain.length)
|
||||
const remainingCharacters = useMemo(() => maxDocumentLength - contentLength, [contentLength, maxDocumentLength])
|
||||
|
||||
const remainingCharactersClass = useMemo(() => {
|
||||
if (remainingCharacters <= 0) {
|
||||
return 'text-danger'
|
||||
|
@ -42,7 +42,7 @@ export const RemainingCharactersInfo: React.FC<LengthInfoProps> = ({ remainingCh
|
|||
}
|
||||
}, [remainingCharacters, t])
|
||||
|
||||
const translationOptions = useMemo(() => ({ length: charactersInDocument }), [charactersInDocument])
|
||||
const translationOptions = useMemo(() => ({ length: contentLength }), [contentLength])
|
||||
|
||||
return (
|
||||
<span {...cypressId('remainingCharacters')} title={lengthTooltip} className={remainingCharactersClass}>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import { SeparatorDash } from './separator-dash'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Shows the total number of selected characters.
|
||||
*/
|
||||
export const SelectedCharacters: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
const count = useMemo(
|
||||
() => (selection.to === undefined ? undefined : selection.to - selection.from),
|
||||
[selection.from, selection.to]
|
||||
)
|
||||
const countTranslationOptions = useMemo(() => ({ count }), [count])
|
||||
|
||||
return count === undefined ? null : (
|
||||
<Fragment>
|
||||
<SeparatorDash />
|
||||
<span>
|
||||
<Trans i18nKey={`editor.statusBar.selection.characters`} values={countTranslationOptions} />
|
||||
</span>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import { SeparatorDash } from './separator-dash'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useLineBasedFromPosition, useLineBasedToPosition } from '../hooks/use-line-based-position'
|
||||
|
||||
/**
|
||||
* Shows the total number of selected lines.
|
||||
*/
|
||||
export const SelectedLines: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const from = useLineBasedFromPosition()
|
||||
const to = useLineBasedToPosition()
|
||||
|
||||
const count = useMemo(() => (to ? to?.line - from.line + 1 : 0), [from.line, to])
|
||||
|
||||
const countTranslationOptions = useMemo(() => ({ count }), [count])
|
||||
|
||||
return count <= 1 ? null : (
|
||||
<Fragment>
|
||||
<SeparatorDash />
|
||||
<span>
|
||||
<Trans i18nKey={`editor.statusBar.selection.lines`} values={countTranslationOptions} />
|
||||
</span>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -9,56 +9,25 @@ import styles from './status-bar.module.scss'
|
|||
import { RemainingCharactersInfo } from './remaining-characters-info'
|
||||
import { NumberOfLinesInDocumentInfo } from './number-of-lines-in-document-info'
|
||||
import { CursorPositionInfo } from './cursor-position-info'
|
||||
import { SelectionInfo } from './selection-info'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { SeparatorDash } from './separator-dash'
|
||||
import type { CursorPosition } from '../../../../redux/editor/types'
|
||||
import { useCreateStatusBarInfo } from '../hooks/use-create-status-bar-info'
|
||||
|
||||
export interface StatusBarInfo {
|
||||
position: CursorPosition
|
||||
selectedColumns: number
|
||||
selectedLines: number
|
||||
linesInDocument: number
|
||||
charactersInDocument: number
|
||||
remainingCharacters: number
|
||||
}
|
||||
|
||||
export const defaultState: StatusBarInfo = {
|
||||
position: { line: 0, character: 0 },
|
||||
selectedColumns: 0,
|
||||
selectedLines: 0,
|
||||
linesInDocument: 0,
|
||||
charactersInDocument: 0,
|
||||
remainingCharacters: 0
|
||||
}
|
||||
import { SelectedCharacters } from './selected-characters'
|
||||
import { SelectedLines } from './selected-lines'
|
||||
|
||||
/**
|
||||
* Shows additional information about the document length and the current selection.
|
||||
*/
|
||||
export const StatusBar: React.FC = () => {
|
||||
const statusBarInfo = useCreateStatusBarInfo()
|
||||
|
||||
return (
|
||||
<div className={`d-flex flex-row ${styles['status-bar']} px-2`}>
|
||||
<div>
|
||||
<CursorPositionInfo cursorPosition={statusBarInfo.position} />
|
||||
<ShowIf condition={statusBarInfo.selectedLines === 1 && statusBarInfo.selectedColumns > 0}>
|
||||
<SeparatorDash />
|
||||
<SelectionInfo count={statusBarInfo.selectedColumns} translationKey={'column'} />
|
||||
</ShowIf>
|
||||
<ShowIf condition={statusBarInfo.selectedLines > 1}>
|
||||
<SeparatorDash />
|
||||
<SelectionInfo count={statusBarInfo.selectedLines} translationKey={'line'} />
|
||||
</ShowIf>
|
||||
<CursorPositionInfo />
|
||||
<SelectedCharacters />
|
||||
<SelectedLines />
|
||||
</div>
|
||||
<div className='ml-auto'>
|
||||
<NumberOfLinesInDocumentInfo numberOfLinesInDocument={statusBarInfo.linesInDocument} />
|
||||
<NumberOfLinesInDocumentInfo />
|
||||
<SeparatorDash />
|
||||
<RemainingCharactersInfo
|
||||
remainingCharacters={statusBarInfo.remainingCharacters}
|
||||
charactersInDocument={statusBarInfo.charactersInDocument}
|
||||
/>
|
||||
<RemainingCharactersInfo />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { EditorConfiguration } from 'codemirror'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
|
||||
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||
import type { EditorPreferenceProperty } from './editor-preference-property'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
|
||||
export interface EditorPreferenceBooleanProps {
|
||||
property: EditorPreferenceProperty
|
||||
}
|
||||
|
||||
export const EditorPreferenceBooleanProperty: React.FC<EditorPreferenceBooleanProps> = ({ property }) => {
|
||||
const preference = useApplicationState((state) => state.editorConfig.preferences[property]?.toString() ?? '')
|
||||
|
||||
const { t } = useTranslation()
|
||||
const selectItem = useCallback(
|
||||
(event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedItem: boolean = event.target.value === 'true'
|
||||
|
||||
mergeEditorPreferences({
|
||||
[property]: selectedItem
|
||||
} as EditorConfiguration)
|
||||
},
|
||||
[property]
|
||||
)
|
||||
|
||||
const i18nPrefix = `editor.modal.preferences.${property}`
|
||||
|
||||
return (
|
||||
<EditorPreferenceInput
|
||||
onChange={selectItem}
|
||||
property={property}
|
||||
type={EditorPreferenceInputType.SELECT}
|
||||
value={preference}>
|
||||
<option value={'true'}>{t(`${i18nPrefix}.on`)}</option>
|
||||
<option value={'false'}>{t(`${i18nPrefix}.off`)}</option>
|
||||
</EditorPreferenceInput>
|
||||
)
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
export enum EditorPreferenceInputType {
|
||||
SELECT,
|
||||
BOOLEAN,
|
||||
NUMBER
|
||||
}
|
||||
|
||||
export interface EditorPreferenceInputProps {
|
||||
property: string
|
||||
type: EditorPreferenceInputType
|
||||
onChange: React.ChangeEventHandler<HTMLSelectElement>
|
||||
value?: string | number | string[]
|
||||
}
|
||||
|
||||
export const EditorPreferenceInput: React.FC<EditorPreferenceInputProps> = ({
|
||||
property,
|
||||
type,
|
||||
onChange,
|
||||
value,
|
||||
children
|
||||
}) => {
|
||||
useTranslation()
|
||||
return (
|
||||
<Form.Group controlId={`editor-pref-${property}`}>
|
||||
<Form.Label>
|
||||
<Trans
|
||||
i18nKey={`editor.modal.preferences.${property}${type === EditorPreferenceInputType.NUMBER ? '' : '.label'}`}
|
||||
/>
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as={type === EditorPreferenceInputType.NUMBER ? 'input' : 'select'}
|
||||
size='sm'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
type={type === EditorPreferenceInputType.NUMBER ? 'number' : ''}>
|
||||
{children}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ChangeEvent } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { setEditorLigatures } from '../../../../../redux/editor/methods'
|
||||
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
|
||||
export const EditorPreferenceLigaturesSelect: React.FC = () => {
|
||||
const ligaturesEnabled = useApplicationState((state) => Boolean(state.editorConfig.ligatures).toString())
|
||||
const saveLigatures = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const ligaturesActivated: boolean = event.target.value === 'true'
|
||||
setEditorLigatures(ligaturesActivated)
|
||||
}, [])
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<EditorPreferenceInput
|
||||
onChange={saveLigatures}
|
||||
value={ligaturesEnabled}
|
||||
property={'ligatures'}
|
||||
type={EditorPreferenceInputType.BOOLEAN}>
|
||||
<option value='true'>{t(`common.yes`)}</option>
|
||||
<option value='false'>{t(`common.no`)}</option>
|
||||
</EditorPreferenceInput>
|
||||
)
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { EditorConfiguration } from 'codemirror'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
|
||||
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||
import type { EditorPreferenceProperty } from './editor-preference-property'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
|
||||
export interface EditorPreferenceNumberProps {
|
||||
property: EditorPreferenceProperty
|
||||
}
|
||||
|
||||
export const EditorPreferenceNumberProperty: React.FC<EditorPreferenceNumberProps> = ({ property }) => {
|
||||
const preference = useApplicationState((state) => state.editorConfig.preferences[property]?.toString() ?? '')
|
||||
|
||||
const selectItem = useCallback(
|
||||
(event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedItem: number = Number.parseInt(event.target.value)
|
||||
|
||||
mergeEditorPreferences({
|
||||
[property]: selectedItem
|
||||
} as EditorConfiguration)
|
||||
},
|
||||
[property]
|
||||
)
|
||||
|
||||
return (
|
||||
<EditorPreferenceInput
|
||||
onChange={selectItem}
|
||||
property={property}
|
||||
type={EditorPreferenceInputType.NUMBER}
|
||||
value={preference}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export enum EditorPreferenceProperty {
|
||||
KEYMAP = 'keyMap',
|
||||
THEME = 'theme',
|
||||
INDENT_WITH_TABS = 'indentWithTabs',
|
||||
INDENT_UNIT = 'indentUnit',
|
||||
SPELL_CHECK = 'spellcheck'
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { EditorConfiguration } from 'codemirror'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
|
||||
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||
import type { EditorPreferenceProperty } from './editor-preference-property'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
|
||||
export interface EditorPreferenceSelectPropertyProps {
|
||||
property: EditorPreferenceProperty
|
||||
selections: string[]
|
||||
}
|
||||
|
||||
export const EditorPreferenceSelectProperty: React.FC<EditorPreferenceSelectPropertyProps> = ({
|
||||
property,
|
||||
selections
|
||||
}) => {
|
||||
const preference = useApplicationState((state) => state.editorConfig.preferences[property]?.toString() ?? '')
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const selectItem = useCallback(
|
||||
(event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedItem: string = event.target.value
|
||||
|
||||
mergeEditorPreferences({
|
||||
[property]: selectedItem
|
||||
} as EditorConfiguration)
|
||||
},
|
||||
[property]
|
||||
)
|
||||
|
||||
const i18nPrefix = `editor.modal.preferences.${property}`
|
||||
|
||||
return (
|
||||
<EditorPreferenceInput
|
||||
onChange={selectItem}
|
||||
property={property}
|
||||
type={EditorPreferenceInputType.SELECT}
|
||||
value={preference}>
|
||||
{selections.map((selection) => (
|
||||
<option key={selection} value={selection}>
|
||||
{t(`${i18nPrefix}.${selection}`)}
|
||||
</option>
|
||||
))}
|
||||
</EditorPreferenceInput>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ChangeEvent } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
import { setEditorSmartPaste } from '../../../../../redux/editor/methods'
|
||||
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||
|
||||
export const EditorPreferenceSmartPasteSelect: React.FC = () => {
|
||||
const smartPasteEnabled = useApplicationState((state) => Boolean(state.editorConfig.smartPaste).toString())
|
||||
const saveSmartPaste = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const smartPasteActivated: boolean = event.target.value === 'true'
|
||||
setEditorSmartPaste(smartPasteActivated)
|
||||
}, [])
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<EditorPreferenceInput
|
||||
onChange={saveSmartPaste}
|
||||
value={smartPasteEnabled}
|
||||
property={'smartPaste'}
|
||||
type={EditorPreferenceInputType.BOOLEAN}>
|
||||
<option value='true'>{t(`common.yes`)}</option>
|
||||
<option value='false'>{t(`common.no`)}</option>
|
||||
</EditorPreferenceInput>
|
||||
)
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Button, Form, ListGroup } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { CommonModal } from '../../../../common/modals/common-modal'
|
||||
import { ShowIf } from '../../../../common/show-if/show-if'
|
||||
import { EditorPreferenceBooleanProperty } from './editor-preference-boolean-property'
|
||||
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||
import { EditorPreferenceLigaturesSelect } from './editor-preference-ligatures-select'
|
||||
import { EditorPreferenceNumberProperty } from './editor-preference-number-property'
|
||||
import { EditorPreferenceProperty } from './editor-preference-property'
|
||||
import { EditorPreferenceSelectProperty } from './editor-preference-select-property'
|
||||
import { EditorPreferenceSmartPasteSelect } from './editor-preference-smart-paste-select'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
|
||||
export const EditorPreferences: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const indentWithTabs = useApplicationState((state) => state.editorConfig.preferences.indentWithTabs ?? false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Button variant='light' onClick={() => setShowModal(true)} title={t('editor.editorToolbar.preferences')}>
|
||||
<ForkAwesomeIcon icon='wrench' />
|
||||
</Button>
|
||||
<CommonModal
|
||||
show={showModal}
|
||||
onHide={() => setShowModal(false)}
|
||||
title={'editor.modal.preferences.title'}
|
||||
showCloseButton={true}
|
||||
titleIcon={'wrench'}>
|
||||
<Form>
|
||||
<ListGroup>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceSelectProperty
|
||||
property={EditorPreferenceProperty.THEME}
|
||||
selections={['one-dark', 'neat']}
|
||||
/>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceSelectProperty
|
||||
property={EditorPreferenceProperty.KEYMAP}
|
||||
selections={['sublime', 'emacs', 'vim']}
|
||||
/>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceBooleanProperty property={EditorPreferenceProperty.INDENT_WITH_TABS} />
|
||||
</ListGroup.Item>
|
||||
<ShowIf condition={!indentWithTabs}>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceNumberProperty property={EditorPreferenceProperty.INDENT_UNIT} />
|
||||
</ListGroup.Item>
|
||||
</ShowIf>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceLigaturesSelect />
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceSmartPasteSelect />
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceInput
|
||||
onChange={() => alert('This feature is not yet implemented.')}
|
||||
property={EditorPreferenceProperty.SPELL_CHECK}
|
||||
type={EditorPreferenceInputType.SELECT}>
|
||||
<option value='off'>Off</option>
|
||||
<option value='en'>English</option>
|
||||
</EditorPreferenceInput>
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
</Form>
|
||||
</CommonModal>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import React from 'react'
|
||||
import { ButtonGroup, ButtonToolbar } from 'react-bootstrap'
|
||||
import { EditorPreferences } from './editor-preferences/editor-preferences'
|
||||
import { EmojiPickerButton } from './emoji-picker/emoji-picker-button'
|
||||
import { TablePickerButton } from './table-picker/table-picker-button'
|
||||
import styles from './tool-bar.module.scss'
|
||||
|
@ -46,9 +45,6 @@ export const ToolBar: React.FC = () => {
|
|||
<ToolbarButton icon={'comment'} formatType={FormatType.COMMENT} />
|
||||
<EmojiPickerButton />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className={'mx-1 flex-wrap'}>
|
||||
<EditorPreferences />
|
||||
</ButtonGroup>
|
||||
</ButtonToolbar>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,20 +13,19 @@ import { Mock } from 'ts-mockery'
|
|||
describe('Check whether cursor is in codefence', () => {
|
||||
const getGlobalStateMocked = jest.spyOn(storeModule, 'getGlobalState')
|
||||
|
||||
const mockRedux = (content: string, line: number): void => {
|
||||
const mockRedux = (content: string, from: number): void => {
|
||||
const contentLines = content.split('\n')
|
||||
getGlobalStateMocked.mockImplementation(() =>
|
||||
Mock.from<ApplicationState>({
|
||||
noteDetails: {
|
||||
...initialState,
|
||||
selection: {
|
||||
from: {
|
||||
line: line,
|
||||
character: 0
|
||||
}
|
||||
from
|
||||
},
|
||||
markdownContentLines: contentLines,
|
||||
markdownContent: content
|
||||
markdownContent: {
|
||||
plain: content,
|
||||
lines: contentLines
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -46,22 +45,22 @@ describe('Check whether cursor is in codefence', () => {
|
|||
})
|
||||
|
||||
it('returns true with one open codefence directly above', () => {
|
||||
mockRedux('```\n', 1)
|
||||
mockRedux('```\n', 4)
|
||||
expect(isCursorInCodeFence()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true with one open codefence and empty lines above', () => {
|
||||
mockRedux('```\n\n\n', 3)
|
||||
mockRedux('```\n\n\n', 5)
|
||||
expect(isCursorInCodeFence()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false with one completed codefence above', () => {
|
||||
mockRedux('```\n\n```\n', 3)
|
||||
mockRedux('```\n\n```\n', 8)
|
||||
expect(isCursorInCodeFence()).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true with one completed and one open codefence above', () => {
|
||||
mockRedux('```\n\n```\n\n```\n\n', 6)
|
||||
mockRedux('```\n\n```\n\n```\n\n', 13)
|
||||
expect(isCursorInCodeFence()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,10 +10,8 @@ import { getGlobalState } from '../../../../../redux'
|
|||
* Checks if the start of the current {@link CursorSelection cursor selection} is in a code fence.
|
||||
*/
|
||||
export const isCursorInCodeFence = (): boolean => {
|
||||
const lines = getGlobalState().noteDetails.markdownContentLines.slice(
|
||||
0,
|
||||
getGlobalState().noteDetails.selection.from.line
|
||||
)
|
||||
const noteDetails = getGlobalState().noteDetails
|
||||
const lines = noteDetails.markdownContent.plain.slice(0, noteDetails.selection.from).split('\n')
|
||||
return countCodeFenceLinesUntilIndex(lines) % 2 === 1
|
||||
}
|
||||
|
||||
|
|
|
@ -22,21 +22,20 @@ export interface PasteEvent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if the given {@link PasteEvent paste event} contains a text formatted table
|
||||
* and inserts it into the markdown content.
|
||||
* This happens only if smart paste was activated.
|
||||
* Checks if the given {@link DataTransfer clipboard data} contains a text formatted table
|
||||
* and inserts it into the markdown content. This happens only if smart paste is activated.
|
||||
*
|
||||
* @param event The {@link PasteEvent} from the browser
|
||||
* @param clipboardData The {@link DataTransfer} from the paste event
|
||||
* @return {@code true} if the event was processed. {@code false} otherwise
|
||||
*/
|
||||
export const handleTablePaste = (event: PasteEvent): boolean => {
|
||||
export const handleTablePaste = (clipboardData: DataTransfer): boolean => {
|
||||
if (!getGlobalState().editorConfig.smartPaste || isCursorInCodeFence()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return Optional.ofNullable(event.clipboardData.getData('text'))
|
||||
.filter((pasteText) => !!pasteText && isTable(pasteText))
|
||||
.map((pasteText) => convertClipboardTableToMarkdown(pasteText))
|
||||
return Optional.ofNullable(clipboardData.getData('text'))
|
||||
.filter(isTable)
|
||||
.map(convertClipboardTableToMarkdown)
|
||||
.map((markdownTable) => {
|
||||
replaceSelection(markdownTable)
|
||||
return true
|
||||
|
@ -47,12 +46,12 @@ export const handleTablePaste = (event: PasteEvent): boolean => {
|
|||
/**
|
||||
* Checks if the given {@link PasteEvent paste event} contains files and uploads them.
|
||||
*
|
||||
* @param event The {@link PasteEvent} from the browser
|
||||
* @param clipboardData The {@link DataTransfer} from the paste event
|
||||
* @return {@code true} if the event was processed. {@code false} otherwise
|
||||
*/
|
||||
export const handleFilePaste = (event: PasteEvent): boolean => {
|
||||
return Optional.ofNullable(event.clipboardData.files)
|
||||
.filter((files) => !!files && files.length > 0)
|
||||
export const handleFilePaste = (clipboardData: DataTransfer): boolean => {
|
||||
return Optional.of(clipboardData.files)
|
||||
.filter((files) => files.length > 0)
|
||||
.map((files) => {
|
||||
handleUpload(files[0])
|
||||
return true
|
||||
|
|
|
@ -86,7 +86,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
)
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER,
|
||||
CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE,
|
||||
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
|
||||
)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import type { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
||||
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
|
||||
|
@ -27,26 +27,50 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
|
||||
const communicator = useRendererToEditorCommunicator()
|
||||
|
||||
const countWordsInRenderedDocument = useCallback(() => {
|
||||
const documentContainer = document.querySelector('[data-word-count-target]')
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED,
|
||||
words: documentContainer ? countWords(documentContainer) : 0
|
||||
})
|
||||
}, [communicator])
|
||||
const sendScrolling = useRef<boolean>(false)
|
||||
|
||||
useRendererReceiveHandler(CommunicationMessageType.SET_BASE_CONFIGURATION, (values) =>
|
||||
setBaseConfiguration(values.baseConfiguration)
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE,
|
||||
useCallback(() => {
|
||||
sendScrolling.current = false
|
||||
}, [])
|
||||
)
|
||||
useRendererReceiveHandler(CommunicationMessageType.SET_MARKDOWN_CONTENT, (values) =>
|
||||
setMarkdownContentLines(values.content)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_BASE_CONFIGURATION,
|
||||
useCallback((values) => setBaseConfiguration(values.baseConfiguration), [])
|
||||
)
|
||||
useRendererReceiveHandler(CommunicationMessageType.SET_DARKMODE, (values) => setDarkMode(values.activated))
|
||||
useRendererReceiveHandler(CommunicationMessageType.SET_SCROLL_STATE, (values) => setScrollState(values.scrollState))
|
||||
useRendererReceiveHandler(CommunicationMessageType.SET_FRONTMATTER_INFO, (values) =>
|
||||
setFrontmatterInfo(values.frontmatterInfo)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_MARKDOWN_CONTENT,
|
||||
useCallback((values) => setMarkdownContentLines(values.content), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_DARKMODE,
|
||||
useCallback((values) => setDarkMode(values.activated), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_SCROLL_STATE,
|
||||
useCallback((values) => setScrollState(values.scrollState), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_FRONTMATTER_INFO,
|
||||
useCallback((values) => setFrontmatterInfo(values.frontmatterInfo), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.GET_WORD_COUNT,
|
||||
useCallback(() => {
|
||||
const documentContainer = document.querySelector('[data-word-count-target]')
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED,
|
||||
words: documentContainer ? countWords(documentContainer) : 0
|
||||
})
|
||||
}, [communicator])
|
||||
)
|
||||
useRendererReceiveHandler(CommunicationMessageType.GET_WORD_COUNT, () => countWordsInRenderedDocument())
|
||||
|
||||
const onTaskCheckedChange = useCallback(
|
||||
(lineInMarkdown: number, checked: boolean) => {
|
||||
|
@ -70,13 +94,17 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
)
|
||||
|
||||
const onMakeScrollSource = useCallback(() => {
|
||||
sendScrolling.current = true
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
|
||||
type: CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
||||
})
|
||||
}, [communicator])
|
||||
|
||||
const onScroll = useCallback(
|
||||
(scrollState: ScrollState) => {
|
||||
if (!sendScrolling.current) {
|
||||
return
|
||||
}
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.SET_SCROLL_STATE,
|
||||
scrollState
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { useEffect } from 'react'
|
||||
import type { CommunicationMessages, RendererToEditorMessageType } from '../rendering-message'
|
||||
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import type { Handler } from '../window-post-message-communicator'
|
||||
import type { MaybeHandler } from '../window-post-message-communicator'
|
||||
|
||||
/**
|
||||
* Sets the handler for the given message type in the current editor to renderer communicator.
|
||||
|
@ -17,7 +17,7 @@ import type { Handler } from '../window-post-message-communicator'
|
|||
*/
|
||||
export const useEditorReceiveHandler = <R extends RendererToEditorMessageType>(
|
||||
messageType: R,
|
||||
handler: Handler<CommunicationMessages, R>
|
||||
handler: MaybeHandler<CommunicationMessages, R>
|
||||
): void => {
|
||||
const editorToRendererCommunicator = useEditorToRendererCommunicator()
|
||||
useEffect(() => {
|
||||
|
|
|
@ -9,6 +9,11 @@ import type { CommunicationMessages, EditorToRendererMessageType } from '../rend
|
|||
import type { Handler } from '../window-post-message-communicator'
|
||||
import { useRendererToEditorCommunicator } from '../../../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
||||
|
||||
export type CommunicationMessageHandler<MESSAGE_TYPE extends EditorToRendererMessageType> = Handler<
|
||||
CommunicationMessages,
|
||||
MESSAGE_TYPE
|
||||
>
|
||||
|
||||
/**
|
||||
* Sets the handler for the given message type in the current renderer to editor communicator.
|
||||
*
|
||||
|
@ -17,7 +22,7 @@ import { useRendererToEditorCommunicator } from '../../../editor-page/render-con
|
|||
*/
|
||||
export const useRendererReceiveHandler = <MESSAGE_TYPE extends EditorToRendererMessageType>(
|
||||
messageType: MESSAGE_TYPE,
|
||||
handler: Handler<CommunicationMessages, MESSAGE_TYPE>
|
||||
handler: CommunicationMessageHandler<MESSAGE_TYPE>
|
||||
): void => {
|
||||
const editorToRendererCommunicator = useRendererToEditorCommunicator()
|
||||
useEffect(() => {
|
||||
|
|
|
@ -12,7 +12,8 @@ export enum CommunicationMessageType {
|
|||
SET_DARKMODE = 'SET_DARKMODE',
|
||||
ON_TASK_CHECKBOX_CHANGE = 'ON_TASK_CHECKBOX_CHANGE',
|
||||
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
||||
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
|
||||
ENABLE_RENDERER_SCROLL_SOURCE = 'ENABLE_RENDERER_SCROLL_SOURCE',
|
||||
DISABLE_RENDERER_SCROLL_SOURCE = 'DISABLE_RENDERER_SCROLL_SOURCE',
|
||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
||||
|
@ -23,8 +24,8 @@ export enum CommunicationMessageType {
|
|||
IMAGE_UPLOAD = 'IMAGE_UPLOAD'
|
||||
}
|
||||
|
||||
export interface NoPayloadMessage {
|
||||
type: CommunicationMessageType.RENDERER_READY | CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
|
||||
export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
|
||||
type: TYPE
|
||||
}
|
||||
|
||||
export interface SetDarkModeMessage {
|
||||
|
@ -97,7 +98,9 @@ export interface OnWordCountCalculatedMessage {
|
|||
}
|
||||
|
||||
export type CommunicationMessages =
|
||||
| NoPayloadMessage
|
||||
| NoPayloadMessage<CommunicationMessageType.RENDERER_READY>
|
||||
| NoPayloadMessage<CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE>
|
||||
| NoPayloadMessage<CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE>
|
||||
| SetDarkModeMessage
|
||||
| SetBaseUrlMessage
|
||||
| GetWordCountMessage
|
||||
|
@ -118,10 +121,11 @@ export type EditorToRendererMessageType =
|
|||
| CommunicationMessageType.SET_BASE_CONFIGURATION
|
||||
| CommunicationMessageType.GET_WORD_COUNT
|
||||
| CommunicationMessageType.SET_FRONTMATTER_INFO
|
||||
| CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE
|
||||
|
||||
export type RendererToEditorMessageType =
|
||||
| CommunicationMessageType.RENDERER_READY
|
||||
| CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
|
||||
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
||||
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
||||
| CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
|
||||
| CommunicationMessageType.SET_SCROLL_STATE
|
||||
|
|
|
@ -11,12 +11,14 @@ import type { Logger } from '../../../utils/logger'
|
|||
*/
|
||||
export class IframeCommunicatorSendingError extends Error {}
|
||||
|
||||
export type Handler<MESSAGES, MESSAGE_TYPE extends string> =
|
||||
| ((values: Extract<MESSAGES, PostMessage<MESSAGE_TYPE>>) => void)
|
||||
| undefined
|
||||
export type Handler<MESSAGES, MESSAGE_TYPE extends string> = (
|
||||
values: Extract<MESSAGES, PostMessage<MESSAGE_TYPE>>
|
||||
) => void
|
||||
|
||||
export type MaybeHandler<MESSAGES, MESSAGE_TYPE extends string> = Handler<MESSAGES, MESSAGE_TYPE> | undefined
|
||||
|
||||
export type HandlerMap<MESSAGES, MESSAGE_TYPE extends string> = Partial<{
|
||||
[key in MESSAGE_TYPE]: Handler<MESSAGES, MESSAGE_TYPE>
|
||||
[key in MESSAGE_TYPE]: MaybeHandler<MESSAGES, MESSAGE_TYPE>
|
||||
}>
|
||||
|
||||
export interface PostMessage<MESSAGE_TYPE extends string> {
|
||||
|
@ -108,8 +110,9 @@ export abstract class WindowPostMessageCommunicator<
|
|||
* @param messageType The message type for which the handler should be called
|
||||
* @param handler The handler that processes messages with the given message type.
|
||||
*/
|
||||
public setHandler<R extends RECEIVE_TYPE>(messageType: R, handler: Handler<MESSAGES, R>): void {
|
||||
this.handlers[messageType] = handler as Handler<MESSAGES, RECEIVE_TYPE>
|
||||
public setHandler<R extends RECEIVE_TYPE>(messageType: R, handler: MaybeHandler<MESSAGES, R>): void {
|
||||
this.log.debug('Set handler for', messageType)
|
||||
this.handlers[messageType] = handler as MaybeHandler<MESSAGES, RECEIVE_TYPE>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue