mirror of
https://github.com/mattermost/focalboard.git
synced 2025-02-01 19:14:35 +02:00
* Fix #1928. Use draft-js-live-markdown-plugin. * Ported markdown plugin to TS and checked in directly * Fix type * Fix types
This commit is contained in:
parent
2e4d015586
commit
a8b7a6a556
@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {ContentBlock, ContentState, Modifier, SelectionState} from 'draft-js'
|
||||
|
||||
import {BlockStrategy} from '../pluginStrategy'
|
||||
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createCodeBlockStrategy = (): BlockStrategy => {
|
||||
const blockType = 'code-block'
|
||||
const CODE_BLOCK_REGEX = /^```/g
|
||||
|
||||
return {
|
||||
type: blockType,
|
||||
className: 'code-block',
|
||||
mapBlockType: (contentState) => {
|
||||
// Takes a ContentState and returns a ContentState with code block content
|
||||
// block type applied
|
||||
const blockMap = contentState.getBlockMap()
|
||||
let newContentState = contentState
|
||||
let codeBlockKeys: string[] = []
|
||||
let notCodeBlockKeys: string[] = []
|
||||
let tempKeys: string[] = []
|
||||
let language: string
|
||||
|
||||
// Find all code blocks
|
||||
blockMap.forEach((block, blockKey) => {
|
||||
if (!block || !blockKey) {
|
||||
return
|
||||
}
|
||||
const text = block.getText()
|
||||
const codeBlockDelimiterRanges = findRangesWithRegex(
|
||||
text,
|
||||
CODE_BLOCK_REGEX,
|
||||
)
|
||||
const precededByDelimiter = tempKeys.length > 0
|
||||
|
||||
// Parse out the language specified after the delimiter for use with the
|
||||
// draft-js-prism-plugin for syntax highlighting
|
||||
if (codeBlockDelimiterRanges.length > 0 && !precededByDelimiter) {
|
||||
language = (text.match(/\w+/g) || [])[0] || 'javascript'
|
||||
}
|
||||
|
||||
// If we find the opening code block delimiter we must maintain an array
|
||||
// of all keys for content blocks that might need to be code blocks if we
|
||||
// later find a closing code block delimiter
|
||||
if (codeBlockDelimiterRanges.length > 0 || precededByDelimiter) {
|
||||
tempKeys.push(blockKey)
|
||||
} else {
|
||||
notCodeBlockKeys.push(blockKey)
|
||||
}
|
||||
|
||||
// If we find the closing code block delimiter ``` then store the keys for
|
||||
// the sandwiched content blocks
|
||||
if (codeBlockDelimiterRanges.length > 0 && precededByDelimiter) {
|
||||
codeBlockKeys = codeBlockKeys.concat(tempKeys)
|
||||
tempKeys = []
|
||||
}
|
||||
})
|
||||
|
||||
// Loop through keys for blocks that should not have code block type and remove
|
||||
// code block type if necessary
|
||||
notCodeBlockKeys = notCodeBlockKeys.concat(tempKeys)
|
||||
notCodeBlockKeys.forEach((blockKey) => {
|
||||
if (newContentState.getBlockForKey(blockKey).getType() === blockType) {
|
||||
newContentState = Modifier.setBlockType(
|
||||
newContentState,
|
||||
SelectionState.createEmpty(blockKey),
|
||||
'unstyled',
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Loop through found code block keys and apply the block style and language
|
||||
// metadata to the block
|
||||
codeBlockKeys.forEach((blockKey, i) => {
|
||||
// Apply language metadata to block (ignore delimiter blocks)
|
||||
const isDelimiterBlock = i === 0 || i === codeBlockKeys.length - 1
|
||||
const block = newContentState.getBlockForKey(blockKey)
|
||||
const newBlockMap = newContentState.getBlockMap()
|
||||
const data = block.
|
||||
getData().
|
||||
merge({language: isDelimiterBlock ? undefined : language})
|
||||
const newBlock = block.merge({data}) as ContentBlock
|
||||
newContentState = newContentState.merge({
|
||||
blockMap: newBlockMap.set(blockKey, newBlock),
|
||||
}) as ContentState
|
||||
|
||||
// Apply block type to block
|
||||
newContentState = Modifier.setBlockType(
|
||||
newContentState,
|
||||
SelectionState.createEmpty(blockKey),
|
||||
blockType,
|
||||
)
|
||||
})
|
||||
|
||||
return newContentState
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createCodeBlockStrategy
|
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Modifier, SelectionState} from 'draft-js'
|
||||
|
||||
import {BlockStrategy} from '../pluginStrategy'
|
||||
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createHeadingBlockStrategy = (): BlockStrategy => {
|
||||
const HEADING_REGEX = /(^#{1,6})\s(.*)/gm
|
||||
const HEADING_LEVELS = [
|
||||
'header-one',
|
||||
'header-two',
|
||||
'header-three',
|
||||
'header-four',
|
||||
'header-five',
|
||||
'header-six',
|
||||
]
|
||||
|
||||
return {
|
||||
type: 'heading',
|
||||
className: 'heading-block',
|
||||
mapBlockType: (contentState) => {
|
||||
// Takes a ContentState and returns a ContentState with heading content block
|
||||
// type applied
|
||||
const blockMap = contentState.getBlockMap()
|
||||
let newContentState = contentState
|
||||
|
||||
// Find all heading blocks
|
||||
blockMap.forEach((block, blockKey) => {
|
||||
if (!block || !blockKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = block.getText()
|
||||
const headingBlockDelimiterRanges = findRangesWithRegex(
|
||||
text,
|
||||
HEADING_REGEX,
|
||||
)
|
||||
let headingLevel = 1
|
||||
|
||||
// Determine what heading level it should be
|
||||
if (headingBlockDelimiterRanges.length > 0) {
|
||||
headingLevel = (text.match(/#/g) || []).length
|
||||
}
|
||||
|
||||
// Apply the corresponding heading block type
|
||||
if (headingBlockDelimiterRanges.length > 0) {
|
||||
newContentState = Modifier.setBlockType(
|
||||
newContentState,
|
||||
SelectionState.createEmpty(blockKey),
|
||||
HEADING_LEVELS[headingLevel - 1],
|
||||
)
|
||||
} else if (HEADING_LEVELS.includes(newContentState.getBlockForKey(blockKey).getType())) {
|
||||
// Remove any existing heading block type if there shouldn't be one
|
||||
newContentState = Modifier.setBlockType(
|
||||
newContentState,
|
||||
SelectionState.createEmpty(blockKey),
|
||||
'unstyled',
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return newContentState
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createHeadingBlockStrategy
|
@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
// Bold can be delimited by: **, __, ***, and ___
|
||||
const createBoldStyleStrategy = (): InlineStrategy => {
|
||||
const asteriskDelimitedRegex =
|
||||
'(\\*\\*\\*)(.+?)(\\*\\*\\*)|(\\*\\*)(.+?)(\\*\\*)(?!\\*)'
|
||||
const underscoreDelimitedRegex = '(___)(.+?)(___)|(__)(.+?)(__)(?!_)'
|
||||
const boldRegex = new RegExp(
|
||||
`${asteriskDelimitedRegex}|${underscoreDelimitedRegex}`,
|
||||
'g',
|
||||
)
|
||||
const boldDelimiterRegex = /^(\*\*\*|\*\*|___|__)|(\*\*\*|\*\*|___|__)$/g
|
||||
|
||||
return {
|
||||
style: 'BOLD',
|
||||
delimiterStyle: 'BOLD-DELIMITER',
|
||||
findStyleRanges: (block) => {
|
||||
// Return an array of arrays containing start and end indices for ranges of
|
||||
// text that should be bolded
|
||||
// e.g. [[0,6], [10,20]]
|
||||
const text = block.getText()
|
||||
const boldRanges = findRangesWithRegex(text, boldRegex)
|
||||
return boldRanges
|
||||
},
|
||||
findDelimiterRanges: (block, styleRanges) => {
|
||||
// Find ranges for delimiters at the beginning/end of styled text ranges
|
||||
// Returns an array of arrays containing start and end indices for delimiters
|
||||
const text = block.getText()
|
||||
let boldDelimiterRanges: number[][] = []
|
||||
styleRanges.forEach((styleRange) => {
|
||||
const delimiterRange = findRangesWithRegex(
|
||||
text.substring(styleRange[0], styleRange[1] + 1),
|
||||
boldDelimiterRegex,
|
||||
).map((indices) => indices.map((x) => x + styleRange[0]))
|
||||
boldDelimiterRanges = boldDelimiterRanges.concat(delimiterRange)
|
||||
})
|
||||
return boldDelimiterRanges
|
||||
},
|
||||
delimiterStyles: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createBoldStyleStrategy
|
@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createHeadingDelimiterStyleStrategy = (): InlineStrategy => {
|
||||
const headingDelimiterRegex = /(^#{1,6})\s/g
|
||||
|
||||
return {
|
||||
style: 'HEADING-DELIMITER',
|
||||
findStyleRanges: (block) => {
|
||||
// Skip the text search if the block isn't a header block
|
||||
if (block.getType().indexOf('header') < 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const text = block.getText()
|
||||
const headingDelimiterRanges = findRangesWithRegex(
|
||||
text,
|
||||
headingDelimiterRegex,
|
||||
)
|
||||
return headingDelimiterRanges
|
||||
},
|
||||
styles: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createHeadingDelimiterStyleStrategy
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createInlineCodeStyleStrategy = (): InlineStrategy => {
|
||||
const codeRegex = /(`)([^\n\r`]+?)(`)/g
|
||||
|
||||
return {
|
||||
style: 'INLINE-CODE',
|
||||
findStyleRanges: (block) => {
|
||||
// Don't allow inline code inside of code blocks
|
||||
if (block.getType() === 'code-block') {
|
||||
return []
|
||||
}
|
||||
|
||||
const text = block.getText()
|
||||
const codeRanges = findRangesWithRegex(text, codeRegex)
|
||||
return codeRanges
|
||||
},
|
||||
styles: {
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createInlineCodeStyleStrategy
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createItalicStyleStrategy = (): InlineStrategy => {
|
||||
const asteriskDelimitedRegex = '(?<!\\*)(\\*)(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)' // *italic*
|
||||
const underscoreDelimitedRegex = '(?<!_)(_)(?!_)(.+?)(?<!_)_(?!_)' // _italic_
|
||||
const strongEmphasisRegex = '(\\*\\*\\*|___)(.+?)(\\*\\*\\*|___)' // ***bolditalic*** ___bolditalic___
|
||||
const boldWrappedAsteriskRegex =
|
||||
'(?<=\\*\\*)(\\*)(?!\\*)(.*?[^\\*]+)(?<!\\*)\\*(?![^\\*]\\*)|(?<!\\*)(\\*)(?!\\*)(.*?[^\\*]+)(?<!\\*)\\*(?=\\*\\*)' // ***italic* and bold** **bold and *italic***
|
||||
const boldWrappedUnderscoreRegex =
|
||||
'(?<=__)(_)(?!_)(.*?[^_]+)(?<!_)_(?![^_]_)|(?<!_)(_)(?!_)(.*?[^_]+)(?<!_)_(?=__)' // ___italic_ and bold__ __bold and _italic___
|
||||
const italicRegex = new RegExp(
|
||||
`${asteriskDelimitedRegex}|${underscoreDelimitedRegex}|${strongEmphasisRegex}|${boldWrappedAsteriskRegex}|${boldWrappedUnderscoreRegex}`,
|
||||
'g',
|
||||
)
|
||||
|
||||
const italicDelimiterRegex = /^(\*\*\*|\*|___|_)|(\*\*\*|\*|___|_)$/g
|
||||
|
||||
return {
|
||||
style: 'ITALIC',
|
||||
delimiterStyle: 'ITALIC-DELIMITER',
|
||||
findStyleRanges: (block) => {
|
||||
// Return an array of arrays containing start and end indices for ranges of
|
||||
// text that should be italicized
|
||||
// e.g. [[0,6], [10,20]]
|
||||
const text = block.getText()
|
||||
const italicRanges = findRangesWithRegex(text, italicRegex)
|
||||
return italicRanges
|
||||
},
|
||||
findDelimiterRanges: (block, styleRanges) => {
|
||||
// Find ranges for delimiters at the beginning/end of styled text ranges
|
||||
// Returns an array of arrays containing start and end indices for delimiters
|
||||
const text = block.getText()
|
||||
let italicDelimiterRanges: number[][] = []
|
||||
styleRanges.forEach((styleRange) => {
|
||||
const delimiterRange = findRangesWithRegex(
|
||||
text.substring(styleRange[0], styleRange[1] + 1),
|
||||
italicDelimiterRegex,
|
||||
).map((indices) => indices.map((x) => x + styleRange[0]))
|
||||
italicDelimiterRanges = italicDelimiterRanges.concat(delimiterRange)
|
||||
})
|
||||
return italicDelimiterRanges
|
||||
},
|
||||
delimiterStyles: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createItalicStyleStrategy
|
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createOLDelimiterStyleStrategy = (): InlineStrategy => {
|
||||
const olDelimiterRegex = /^\d{1,3}\. /g
|
||||
|
||||
return {
|
||||
style: 'OL-DELIMITER',
|
||||
findStyleRanges: (block) => {
|
||||
const text = block.getText()
|
||||
const olDelimiterRanges = findRangesWithRegex(text, olDelimiterRegex)
|
||||
return olDelimiterRanges
|
||||
},
|
||||
styles: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createOLDelimiterStyleStrategy
|
@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createQuoteStyleStrategy = (): InlineStrategy => {
|
||||
const quoteRegex = /^> (.*)/g
|
||||
const quoteDelimiterRegex = /^> /g
|
||||
|
||||
return {
|
||||
style: 'QUOTE',
|
||||
delimiterStyle: 'QUOTE-DELIMITER',
|
||||
findStyleRanges: (block) => {
|
||||
const text = block.getText()
|
||||
const quoteRanges = findRangesWithRegex(text, quoteRegex)
|
||||
return quoteRanges
|
||||
},
|
||||
findDelimiterRanges: (block, styleRanges) => {
|
||||
const text = block.getText()
|
||||
let quoteDelimiterRanges: number[][] = []
|
||||
styleRanges.forEach((styleRange) => {
|
||||
const delimiterRange = findRangesWithRegex(
|
||||
text.substring(styleRange[0], styleRange[1] + 1),
|
||||
quoteDelimiterRegex,
|
||||
).map((indices) => indices.map((x) => x + styleRange[0]))
|
||||
quoteDelimiterRanges = quoteDelimiterRanges.concat(delimiterRange)
|
||||
})
|
||||
return quoteDelimiterRanges
|
||||
},
|
||||
styles: {
|
||||
opacity: 0.75,
|
||||
},
|
||||
delimiterStyles: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createQuoteStyleStrategy
|
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createStrikethroughStyleStrategy = (): InlineStrategy => {
|
||||
const strikethroughRegex = /(~~)(.+?)(~~)/g
|
||||
const strikethroughDelimiterRegex = /^(~~)|(~~)$/g
|
||||
|
||||
return {
|
||||
style: 'STRIKETHROUGH',
|
||||
delimiterStyle: 'STRIKETHROUGH-DELIMITER',
|
||||
findStyleRanges: (block) => {
|
||||
// Return an array of arrays containing start and end indices for ranges of
|
||||
// text that should be crossed out
|
||||
// e.g. [[0,6], [10,20]]
|
||||
const text = block.getText()
|
||||
const strikethroughRanges = findRangesWithRegex(text, strikethroughRegex)
|
||||
return strikethroughRanges
|
||||
},
|
||||
findDelimiterRanges: (block, styleRanges) => {
|
||||
// Find ranges for delimiters at the beginning/end of styled text ranges
|
||||
// Returns an array of arrays containing start and end indices for delimiters
|
||||
const text = block.getText()
|
||||
let strikethroughDelimiterRanges: number[][] = []
|
||||
styleRanges.forEach((styleRange) => {
|
||||
const delimiterRange = findRangesWithRegex(
|
||||
text.substring(styleRange[0], styleRange[1] + 1),
|
||||
strikethroughDelimiterRegex,
|
||||
).map((indices) => indices.map((x) => x + styleRange[0]))
|
||||
strikethroughDelimiterRanges = strikethroughDelimiterRanges.concat(
|
||||
delimiterRange,
|
||||
)
|
||||
})
|
||||
return strikethroughDelimiterRanges
|
||||
},
|
||||
styles: {
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
delimiterStyles: {
|
||||
opacity: 0.4,
|
||||
textDecoration: 'none',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createStrikethroughStyleStrategy
|
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {InlineStrategy} from '../pluginStrategy'
|
||||
import findRangesWithRegex from '../utils/findRangesWithRegex'
|
||||
|
||||
const createULDelimiterStyleStrategy = (): InlineStrategy => {
|
||||
const ulDelimiterRegex = /^\* /g
|
||||
|
||||
return {
|
||||
style: 'UL-DELIMITER',
|
||||
findStyleRanges: (block) => {
|
||||
const text = block.getText()
|
||||
const ulDelimiterRanges = findRangesWithRegex(text, ulDelimiterRegex)
|
||||
return ulDelimiterRanges
|
||||
},
|
||||
styles: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createULDelimiterStyleStrategy
|
263
webapp/src/components/live-markdown-plugin/liveMarkdownPlugin.ts
Normal file
263
webapp/src/components/live-markdown-plugin/liveMarkdownPlugin.ts
Normal file
@ -0,0 +1,263 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {
|
||||
EditorState,
|
||||
CharacterMetadata,
|
||||
ContentState,
|
||||
ContentBlock,
|
||||
EditorChangeType,
|
||||
DraftStyleMap,
|
||||
} from 'draft-js'
|
||||
import {EditorPlugin} from '@draft-js-plugins/editor'
|
||||
import {Repeat, List} from 'immutable'
|
||||
|
||||
// Inline style handlers
|
||||
import createBoldStyleStrategy from './inline-styles/boldStyleStrategy'
|
||||
import createItalicStyleStrategy from './inline-styles/italicStyleStrategy'
|
||||
import createStrikethroughStyleStrategy from './inline-styles/strikethroughStyleStrategy'
|
||||
import createHeadingDelimiterStyleStrategy from './inline-styles/headingDelimiterStyleStrategy'
|
||||
import createULDelimiterStyleStrategy from './inline-styles/ulDelimiterStyleStrategy'
|
||||
import createOLDelimiterStyleStrategy from './inline-styles/olDelimiterStyleStrategy'
|
||||
import createQuoteStyleStrategy from './inline-styles/quoteStyleStrategy'
|
||||
import createInlineCodeStyleStrategy from './inline-styles/inlineCodeStyleStrategy'
|
||||
|
||||
// Block type handlers
|
||||
import createCodeBlockStrategy from './block-types/codeBlockStrategy'
|
||||
import createHeadingBlockStrategy from './block-types/headingBlockStrategy'
|
||||
import {BlockStrategy, InlineStrategy} from './pluginStrategy'
|
||||
|
||||
export interface LiveMarkdownPluginConfig {
|
||||
inlineStyleStrategies?: InlineStrategy[],
|
||||
blockTypeStrategies?: BlockStrategy[],
|
||||
}
|
||||
|
||||
function createLiveMarkdownPlugin(config: LiveMarkdownPluginConfig = {}): EditorPlugin {
|
||||
const {
|
||||
inlineStyleStrategies = [
|
||||
createBoldStyleStrategy(),
|
||||
createItalicStyleStrategy(),
|
||||
createStrikethroughStyleStrategy(),
|
||||
createHeadingDelimiterStyleStrategy(),
|
||||
createULDelimiterStyleStrategy(),
|
||||
createOLDelimiterStyleStrategy(),
|
||||
createQuoteStyleStrategy(),
|
||||
createInlineCodeStyleStrategy(),
|
||||
],
|
||||
blockTypeStrategies = [
|
||||
createCodeBlockStrategy(),
|
||||
createHeadingBlockStrategy(),
|
||||
],
|
||||
} = config
|
||||
|
||||
// Construct the editor style map from our inline style strategies
|
||||
const customStyleMap: DraftStyleMap = {}
|
||||
inlineStyleStrategies.forEach((styleStrategy) => {
|
||||
if (styleStrategy.style && styleStrategy.styles) {
|
||||
customStyleMap[styleStrategy.style] = styleStrategy.styles
|
||||
}
|
||||
if (styleStrategy.delimiterStyle && styleStrategy.delimiterStyles) {
|
||||
customStyleMap[styleStrategy.delimiterStyle] =
|
||||
styleStrategy.delimiterStyles
|
||||
}
|
||||
})
|
||||
|
||||
// Construct the block style fn
|
||||
const blockStyleMap = blockTypeStrategies.reduce((map: Record<string, string>, blockStrategy) => {
|
||||
map[blockStrategy.type] = blockStrategy.className
|
||||
return map
|
||||
}, {})
|
||||
const blockStyleFn = (block: ContentBlock) => {
|
||||
const blockType = block.getType()
|
||||
return blockStyleMap[blockType]
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// We must handle the maintenance of block types and inline styles on changes.
|
||||
// To make sure the code is efficient we only perform maintenance on content
|
||||
// blocks that have been changed. We only perform maintenance for change types
|
||||
// that result in actual text changes (ignore cursing through text, etc).
|
||||
onChange: (editorState) => {
|
||||
// if (editorState.getLastChangeType() === 'insert-fragment')
|
||||
// return maintainWholeEditorState();
|
||||
return maintainEditorState(
|
||||
editorState,
|
||||
blockTypeStrategies,
|
||||
inlineStyleStrategies,
|
||||
)
|
||||
},
|
||||
customStyleMap,
|
||||
blockStyleFn,
|
||||
}
|
||||
}
|
||||
|
||||
// Takes an EditorState and returns a ContentState updated with block types and
|
||||
// inline styles according to the provided strategies
|
||||
// Takes a targeted approach that only updates the modified block/blocks
|
||||
const maintainEditorState = (
|
||||
editorState: EditorState,
|
||||
blockTypeStrategies: BlockStrategy[],
|
||||
inlineStyleStrategies: InlineStrategy[],
|
||||
) => {
|
||||
// Bypass maintenance if text was not changed
|
||||
const lastChangeType = editorState.getLastChangeType()
|
||||
const bypassOnChangeTypes = [
|
||||
'adjust-depth',
|
||||
'apply-entity',
|
||||
'change-block-data',
|
||||
'change-block-type',
|
||||
'change-inline-style',
|
||||
'maintain-markdown',
|
||||
]
|
||||
if (bypassOnChangeTypes.includes(lastChangeType)) {
|
||||
return editorState
|
||||
}
|
||||
|
||||
// Maintain block types then inline styles
|
||||
// Order is important bc we want the inline style strategies to be able to
|
||||
// look at block type to avoid unnecessary regex searching when possible
|
||||
const contentState = editorState.getCurrentContent()
|
||||
let newContentState = maintainBlockTypes(contentState, blockTypeStrategies)
|
||||
newContentState = maintainInlineStyles(
|
||||
newContentState,
|
||||
editorState,
|
||||
inlineStyleStrategies,
|
||||
)
|
||||
|
||||
// Apply the updated content state
|
||||
let newEditorState = editorState
|
||||
if (contentState !== newContentState) {
|
||||
newEditorState = EditorState.push(
|
||||
editorState,
|
||||
newContentState,
|
||||
'maintain-markdown' as EditorChangeType,
|
||||
)
|
||||
}
|
||||
newEditorState = EditorState.forceSelection(
|
||||
newEditorState,
|
||||
editorState.getSelection(),
|
||||
)
|
||||
|
||||
return newEditorState
|
||||
}
|
||||
|
||||
// Takes a ContentState and returns a ContentState with block types and inline styles
|
||||
// applied or removed as necessary
|
||||
const maintainBlockTypes = (contentState: ContentState, blockTypeStrategies: BlockStrategy[]) => {
|
||||
return blockTypeStrategies.reduce((cs, blockTypeStrategy) => {
|
||||
return blockTypeStrategy.mapBlockType(cs)
|
||||
}, contentState)
|
||||
}
|
||||
|
||||
// Takes a ContentState (and EditorState for getting the selection and change type)
|
||||
// and returns a ContentState with inline styles applied or removed as necessary
|
||||
const maintainInlineStyles = (
|
||||
contentState: ContentState,
|
||||
editorState: EditorState,
|
||||
inlineStyleStrategies: InlineStrategy[],
|
||||
): ContentState => {
|
||||
const lastChangeType = editorState.getLastChangeType()
|
||||
const selection = editorState.getSelection()
|
||||
const blockKey = selection.getStartKey()
|
||||
const block = contentState.getBlockForKey(blockKey)
|
||||
const blockMap = contentState.getBlockMap()
|
||||
let newBlockMap = blockMap
|
||||
|
||||
// If text has been pasted (potentially modifying/creating multiple blocks) or
|
||||
// the editor is new we must maintain the styles for all content blocks
|
||||
if (lastChangeType === 'insert-fragment' || !lastChangeType) {
|
||||
blockMap.forEach((b, k) => {
|
||||
if (!b || !k) {
|
||||
return
|
||||
}
|
||||
const newBlock = mapInlineStyles(b, inlineStyleStrategies) as ContentBlock
|
||||
newBlockMap = newBlockMap.set(k, newBlock)
|
||||
})
|
||||
} else {
|
||||
const newBlock = mapInlineStyles(block, inlineStyleStrategies) as ContentBlock
|
||||
newBlockMap = newBlockMap.set(blockKey, newBlock)
|
||||
}
|
||||
|
||||
// If enter was pressed (or the block was otherwise split) we must maintain
|
||||
// styles in the previous block as well
|
||||
if (lastChangeType === 'split-block') {
|
||||
const newPrevBlock = mapInlineStyles(
|
||||
contentState.getBlockBefore(blockKey)!,
|
||||
inlineStyleStrategies,
|
||||
) as ContentBlock
|
||||
newBlockMap = newBlockMap.set(
|
||||
contentState.getKeyBefore(blockKey),
|
||||
newPrevBlock,
|
||||
)
|
||||
}
|
||||
|
||||
const newContentState = contentState.merge({
|
||||
blockMap: newBlockMap,
|
||||
}) as ContentState
|
||||
|
||||
return newContentState
|
||||
}
|
||||
|
||||
// Maps inline styles to the provided ContentBlock's CharacterMetadata list based
|
||||
// on the plugin's inline style strategies
|
||||
const mapInlineStyles = (block: ContentBlock, strategies: InlineStrategy[]) => {
|
||||
// This will be called upon any change that has the potential to effect the styles
|
||||
// of a content block.
|
||||
// Find all of the ranges that should have styles applied to them (i.e. all bold,
|
||||
// italic, or strikethrough delimited ranges of the block).
|
||||
const blockText = block.getText()
|
||||
|
||||
// Create a list of empty CharacterMetadata to map styles to
|
||||
// eslint-disable-next-line new-cap
|
||||
let characterMetadataList = List(
|
||||
// eslint-disable-next-line new-cap
|
||||
Repeat(CharacterMetadata.create(), blockText.length),
|
||||
)
|
||||
|
||||
// Evaluate block text with each style strategy and apply styles to matching
|
||||
// ranges of text and delimiters
|
||||
strategies.forEach((strategy) => {
|
||||
const styleRanges = strategy.findStyleRanges(block)
|
||||
const delimiterRanges = strategy.findDelimiterRanges ? strategy.findDelimiterRanges(block, styleRanges) : []
|
||||
|
||||
characterMetadataList = applyStyleRangesToCharacterMetadata(
|
||||
strategy.style,
|
||||
styleRanges,
|
||||
characterMetadataList,
|
||||
)
|
||||
|
||||
characterMetadataList = applyStyleRangesToCharacterMetadata(
|
||||
strategy.delimiterStyle,
|
||||
delimiterRanges,
|
||||
characterMetadataList,
|
||||
)
|
||||
})
|
||||
|
||||
// Apply the list of CharacterMetadata to the content block
|
||||
return block.set('characterList', characterMetadataList)
|
||||
}
|
||||
|
||||
// Applies the provided style to the corresponding ranges of the character metadata
|
||||
const applyStyleRangesToCharacterMetadata = (
|
||||
style: string | undefined,
|
||||
ranges: number[][],
|
||||
characterMetadataList: List<CharacterMetadata>,
|
||||
) => {
|
||||
let styledCharacterMetadataList = characterMetadataList
|
||||
if (!style) {
|
||||
return styledCharacterMetadataList
|
||||
}
|
||||
|
||||
ranges.forEach((range) => {
|
||||
for (let i = range[0]; i <= range[1]; i++) {
|
||||
const styled = CharacterMetadata.applyStyle(
|
||||
characterMetadataList.get(i),
|
||||
style,
|
||||
)
|
||||
styledCharacterMetadataList = styledCharacterMetadataList.set(i, styled)
|
||||
}
|
||||
})
|
||||
return styledCharacterMetadataList
|
||||
}
|
||||
|
||||
export default createLiveMarkdownPlugin
|
20
webapp/src/components/live-markdown-plugin/pluginStrategy.ts
Normal file
20
webapp/src/components/live-markdown-plugin/pluginStrategy.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as React from 'react'
|
||||
import {ContentBlock, ContentState} from 'draft-js'
|
||||
|
||||
export interface InlineStrategy {
|
||||
style: string,
|
||||
findStyleRanges: (text: ContentBlock) => number[][],
|
||||
findDelimiterRanges?: (text: ContentBlock, styleRanges: number[][]) => number[][],
|
||||
delimiterStyle?: string,
|
||||
styles?: React.CSSProperties,
|
||||
delimiterStyles?: React.CSSProperties,
|
||||
}
|
||||
|
||||
export interface BlockStrategy {
|
||||
type: string,
|
||||
className: string,
|
||||
mapBlockType: (state: ContentState) => ContentState
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
const findRangesWithRegex = (text: string, regex: RegExp): number[][] => {
|
||||
const ranges: number[][] = []
|
||||
let matches
|
||||
|
||||
do {
|
||||
matches = regex.exec(text)
|
||||
if (matches) {
|
||||
ranges.push([matches.index, (matches.index + matches[0].length) - 1])
|
||||
}
|
||||
} while (matches)
|
||||
|
||||
return ranges
|
||||
}
|
||||
|
||||
export default findRangesWithRegex
|
@ -1,28 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {getDefaultKeyBinding, EditorState, ContentState, DraftHandleValue} from 'draft-js'
|
||||
import Editor from '@draft-js-plugins/editor'
|
||||
import createEmojiPlugin from '@draft-js-plugins/emoji'
|
||||
import '@draft-js-plugins/emoji/lib/plugin.css'
|
||||
import createMentionPlugin, {
|
||||
defaultSuggestionsFilter,
|
||||
MentionData,
|
||||
} from '@draft-js-plugins/mention'
|
||||
import '@draft-js-plugins/mention/lib/plugin.css'
|
||||
import './markdownEditorInput.scss'
|
||||
import {ContentState, DraftHandleValue, EditorState, getDefaultKeyBinding} from 'draft-js'
|
||||
import React, {
|
||||
ReactElement, useCallback, useEffect,
|
||||
useMemo, useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import createEmojiPlugin from '@draft-js-plugins/emoji'
|
||||
import '@draft-js-plugins/emoji/lib/plugin.css'
|
||||
|
||||
import {getWorkspaceUsersList} from '../../store/users'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getWorkspaceUsersList} from '../../store/users'
|
||||
import {IUser} from '../../user'
|
||||
import createLiveMarkdownPlugin from '../live-markdown-plugin/liveMarkdownPlugin'
|
||||
import './markdownEditorInput.scss'
|
||||
|
||||
import Entry from './entryComponent/entryComponent'
|
||||
|
||||
@ -60,6 +57,7 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
const {MentionSuggestions, plugins, EmojiSuggestions} = useMemo(() => {
|
||||
const mentionPlugin = createMentionPlugin({mentionPrefix: '@'})
|
||||
const emojiPlugin = createEmojiPlugin()
|
||||
const markdownPlugin = createLiveMarkdownPlugin()
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
const {EmojiSuggestions} = emojiPlugin
|
||||
@ -69,6 +67,7 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
||||
const plugins = [
|
||||
mentionPlugin,
|
||||
emojiPlugin,
|
||||
markdownPlugin,
|
||||
]
|
||||
return {plugins, MentionSuggestions, EmojiSuggestions}
|
||||
}, [])
|
||||
|
Loading…
x
Reference in New Issue
Block a user