当前位置: 首页 > news >正文

react实现markdown

参考:https://blog.csdn.net/Jack_lzx/article/details/118495763

参考:https://blog.csdn.net/m0_48474585/article/details/119742984

0. 示例

用react实现markdown编辑器

1.基本布局及样式

    <><div className='tf_editor_header'>头部:放一些编辑工具</div><div className='tf_editor'><div className='edit'>左边:编辑区域</div><div className='show'>右边:展示区域</div></div></>
.tf_editor_header{height: 60px;width: 100%;background-color: #fff;border-bottom:1px solid  rgba(0,0,0,.1);
}.tf_editor{display: flex;flex-direction: row;height: calc(100vh - 60px);width: 100%;.edit{padding: 0.8rem;flex: 1;background-color: #f5f5f5;max-width: 50vw;box-sizing: border-box;border-right: 1px solid  rgba(0,0,0,.1);}.show{padding: 0.8rem;flex: 1;background-color: #fff;max-width: 50vw;box-sizing: border-box;}
}

2.编辑区域

  import _ from 'lodash';const [content, setContent] = useState('')const onEditChange = (e) => { const curContent = e.target.valuesetContent(curContent)}<textarea style={{resize: "none"}} onScroll={(e) => handleScroll(1, e)}className='edit' ref={edit}onChange={onEditChange}></textarea>
  • 使用textarea实现编辑区域,可以通过下面的css设置去除textarea本身的样式
textarea {border: none;outline: none;padding: 0;margin: 0;-webkit-appearance: none;-moz-appearance: none;appearance: none;background-image: none;background-color: transparent;font-size: inherit;width: 100%;
}
textarea:focus {outline: none;
}
  • 添加onChange方法,监听用户的输入事件,并把用户输入的值(e.target.value)保存到content中

3.展示区域

npm i marked 
npm i highlight.js
  • 安装插件

    • marked用于将content渲染成markdown格式的内容
    • highlight.js用于实现代码高亮
// 引入import { marked } from 'marked';import hljs from 'highlight.js';import './github-dark.css';// 配置useEffect(() => {// 配置highlighthljs.configure({tabReplace: '',classPrefix: 'hljs-',languages: ['CSS', 'HTML', 'JavaScript', 'Python', 'TypeScript', 'Markdown'],});// 配置markedmarked.setOptions({renderer: new marked.Renderer(),highlight: code => hljs.highlightAuto(code).value,gfm: true, //默认为true。 允许 Git Hub标准的markdown.tables: true, //默认为true。 允许支持表格语法。该选项要求 gfm 为true。breaks: true, //默认为false。 允许回车换行。该选项要求 gfm 为true。});}, []);// 展示<div className='show'dangerouslySetInnerHTML={{__html: marked(content).replace(/<pre>/g, "<pre id='hljs'>"),}}></div>
  • 这里(原作者)对样式做了修改,我直接复制过来了(github-dark.css)
#hljs {padding: 12px;color: #c9d1d9;background: #0d1117;border-radius: 12px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}code {font-family: 'FiraCode';
}/* 代码片段 */
#hljs code {color: #c9d1d9;background: #0d1117;padding: 0;font-size: 16px;
}.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {/* prettylights-syntax-keyword */color: #ff7b72;
}.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {/* prettylights-syntax-entity */color: #d2a8ff;
}.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {/* prettylights-syntax-constant */color: #79c0ff;
}.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {/* prettylights-syntax-string */color: #a5d6ff;
}.hljs-built_in,
.hljs-symbol {/* prettylights-syntax-variable */color: #ffa657;
}.hljs-comment,
.hljs-code,
.hljs-formula {/* prettylights-syntax-comment */color: #8b949e;
}.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {/* prettylights-syntax-entity-tag */color: #7ee787;
}.hljs-subst {/* prettylights-syntax-storage-modifier-import */color: #c9d1d9;
}.hljs-section {/* prettylights-syntax-markup-heading */color: #1f6feb;font-weight: bold;
}.hljs-bullet {/* prettylights-syntax-markup-list */color: #f2cc60;
}.hljs-emphasis {/* prettylights-syntax-markup-italic */color: #c9d1d9;font-style: italic;
}.hljs-strong {/* prettylights-syntax-markup-bold */color: #c9d1d9;font-weight: bold;
}.hljs-addition {/* prettylights-syntax-markup-inserted */color: #aff5b4;background-color: #033a16;
}.hljs-deletion {/* prettylights-syntax-markup-deleted */color: #ffdcd7;background-color: #67060c;
}/* .hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {} */

4.滚动效果

目标:使得左右两边的滚动能实现联动

  .edit{padding: 0.8rem;flex: 1;background-color: #f5f5f5;max-width: 50vw;box-sizing: border-box;border-right: 1px solid  rgba(0,0,0,.1);overflow: scroll;  // 编辑区域超出区域滚动}.show{padding: 0.8rem;flex: 1;background-color: #fff;max-width: 50vw;box-sizing: border-box;overflow: scroll;  // 展示区域超出区域滚动}// 设置滚动条的样式.edit:focus-visible{outline: 0px solid transparent;}.show{padding: 0.8rem;flex: 1;background-color: #fff;max-width: 50vw;box-sizing: border-box;overflow: scroll;}// 滚动条的样式.show::-webkit-scrollbar {/*滚动条整体样式*/width : 10px;  /*高宽分别对应横竖滚动条的尺寸*/height: 5px;}.show::-webkit-scrollbar-thumb {/*滚动条里面小方块*/border-radius: 10px;background   : #ddd;}.show::-webkit-scrollbar-track {/*滚动条里面轨道*/border-radius: 10px;background   : transparent;}.edit::-webkit-scrollbar {/*滚动条整体样式*/width : 10px;  /*高宽分别对应横竖滚动条的尺寸*/height: 5px;}.edit::-webkit-scrollbar-thumb {/*滚动条里面小方块*/border-radius: 10px;background   : #ddd;}.edit::-webkit-scrollbar-track {/*滚动条里面轨道*/border-radius: 10px;background   : transparent;}
}
  • 上面的css用于设置滚动条的样式,并设置展示区和编辑区超出范围能滚动
// 左边编辑区触发事件
onScroll={(e) => handleScroll(1, e)}
// 右边编辑区触发事件
onScroll={(e) => handleScroll(2, e)}const edit = useRef()
const show = useRef()// 展示区与代码区同步滚动
const handleScroll = (block, event) => {let { scrollHeight, scrollTop, clientHeight } = event.targetlet scale = scrollTop / (scrollHeight - clientHeight)  if(block === 1) {  driveScroll(scale, show.current)  } else if(block === 2) {   driveScroll(scale, edit.current)}
}
// 驱动一个元素进行滚动
const driveScroll = (scale, el) => {let { scrollHeight, clientHeight } = elel.scrollTop = (scrollHeight - clientHeight) * scale  // scrollTop的同比例滚动
}
  • 以编辑区域为例,当前滚动的长度可以用scrollTop获取,那他滚动到底部,即scrollTop的最大值为scrollHeight-clientHeight。我们计算 当前滚动值 与 最大滚动值的比例,即

    scale = scrollTop / (scrollHeight - clientHeight)

  • 再来计算展示区域,展示区域的当前滚动值 与 最大滚动值的比例(scale)应该与编辑区域的比例相同,那么它当前的滚动长度应该设置为scrollTop = scale * (scrollHeight - clientHeight)

问题:上面的写法会有一个问题,如果你在handleScroll方法中输出block的值,就会发现block的值一直在切换 block 1, block 2, block 1, block 2,block 1, block 2 …

  const handleScroll = (block, event) => {console.log('block', block). // 输出block的值let { scrollHeight, scrollTop, clientHeight } = event.targetlet scale = scrollTop / (scrollHeight - clientHeight)  if(block === 1) {  driveScroll(scale, show.current)  } else if(block === 2) {   driveScroll(scale, edit.current)}}

原因:这是因为当你主动触发了block 1的滚动事件,他会令block 2发生滚动,也就是被动触发了block 2 的滚动事件,

解决:用一个变量记录手动触发的是那个区域,这个变量有三种状态:没触发(初始状态),触发了左边的编辑区域,触发了右边的预览区域。分为以下两个步骤

  • 1.手动触发某个区域后需要将这个变量设置为对应的值。(手动触发前的变量值必须是初始状态)
let scrolling = useRef(0) // 记录当前滚动的是哪一个区域,1为编辑区域,2为展示区域
// 展示区与代码区同步滚动
const handleScroll = (block, event) => {let { scrollHeight, scrollTop, clientHeight } = event.targetlet scale = scrollTop / (scrollHeight - clientHeight)  // 改进后的计算滚动比例的方法if(block === 1) {if(scrolling.current === 0) scrolling.current = 1;  if(scrolling.current === 2) return;    driveScroll(scale, show.current)  } else if(block === 2) {  if(scrolling.current === 0) scrolling.current = 2;if(scrolling.current === 1) return;    driveScroll(scale, edit.current)}
}
  • 2.将变量重置为初始状态。这里使用了一个定时器,设定的滚动时间是200ms,滚动结束后将变量初始化。
let scrollTimer = useRef(null) // 记录滚动定时器
// 驱动一个元素进行滚动
const driveScroll = (scale, el) => {let { scrollHeight, clientHeight } = elel.scrollTop = (scrollHeight - clientHeight) * scale  // scrollTop的同比例滚动if(scrollTimer.current) clearTimeout(scrollTimer.current);scrollTimer.current = setTimeout(() => {scrolling.current = 0   clearTimeout(scrollTimer.current)}, 200)
}

5.工具栏

样式参考csdn的编辑器

目标:实现加粗功能

实现方式:在选中文字的前后加上**

步骤一:为加粗的按钮绑定加粗方法

<div className='operation_item' onClick={() => { addMark('****')}}><BoldOutlined />加粗
</div>

步骤二:光标的位置可以从textarea上读取,textarea自带光标开始和结束的属性:selectionStart,selectionEnd

  const addMark = (mark) =>{const begin = edit.current.selectionStart  // 光标开始点const end = edit.current.selectionEnd // 光标结束let mid = mark.length / 2 // 比如mark为****,需要在选中字符串前面加上两个**,后面加上两个**const newValue = (content.slice(0, begin) + mark.slice(0, mid) +content.slice(begin, end) + mark.slice(mid) +  content.slice(end))edit.current.value = newValue // 设置textarea中的值edit.current.setSelectionRange(begin + mid, end + mid) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值}
  • 选中的字符串为content.slice(begin, end),在选中的字符串左右拼接上mark.slice(0, mid)和mark.slice(mid)
  • 光标的位置设置为 edit.current.setSelectionRange(begin + mid, end + mid) // 设置光标的位置,可以举个例子:1,(光标开始),2,3,(光标结束),4,5,然后在2,3左右分别加上两个字符,那就变成了1,字符,字符,(光标开始),2,3,(光标结束),字符,字符,4,5。可以看到光标开始的位置变为begin + mid,光标结束的位置也是end + mid

步骤三:还需要实现按键加粗的功能,在textarea上绑定onKeyDown事件,当监听到按下command+b或者contrl+b时候,调用加粗方法

<textarea style={{resize: "none"}} onScroll={(e) => handleScroll(1, e)}className='edit' ref={edit}onChange={onEditChange}onKeyDown={onKeyDown}></textarea>const onKeyDown = (e) => {if ((e.ctrlKey || e.metaKey) && e.key === 'b') {addMark('****')}
}

步骤四:找到这种规律之后可以把类似的功能都做了,代码上需要做一些细节上的修改

  • 像加粗,斜体,删除线这种是在选中字符的左右加上标记,像标题,无序,有序这种是在选中字符的左边加上标记,我这里通过OPERATIONTYPE变量进行了区分
  • 鼠标按键需要阻止默认事件
<div className='operation_item' onClick={() => { addMark('****')}}><BoldOutlined />加粗
</div>
<div className='operation_item' onClick={() => { addMark('**')}}><ItalicOutlined />斜体
</div>
<div className='operation_item' onClick={() => { addMark('# ', OPERATIONTYPE.LEFT)}}><BoldOutlined />标题
</div>
<div className='operation_item' onClick={() => { addMark('~~')}}><StrikethroughOutlined />删除线
</div>
<div className='operation_item' onClick={() => { addMark('- ', OPERATIONTYPE.LEFT)}}><UnorderedListOutlined />无序
</div>
<div className='operation_item'onClick={() => { addMark('1. ', OPERATIONTYPE.LEFT)}}><OrderedListOutlined />有序
</div>const OPERATIONTYPE = {BETWEEN: 'between',LEFT: 'left',
}const addMark = (mark, type = OPERATIONTYPE.BETWEEN) =>{const begin = edit.current.selectionStart  // 光标开始点const end = edit.current.selectionEnd // 光标结束let mid = 0let newValue = ''switch(type){case OPERATIONTYPE.BETWEEN:mid = mark.length / 2 // 比如mark为****,需要在选中字符串前面加上两个**,后面加上newValue = (content.slice(0, begin) + mark.slice(0, mid) +content.slice(begin, end) + mark.slice(mid) +  content.slice(end));breakcase OPERATIONTYPE.LEFT:mid = mark.lengthnewValue = (content.slice(0, begin) + mark.slice(0, mid) +content.slice(begin, end) +  content.slice(end));break}edit.current.value = newValue // 设置textarea中的值edit.current.setSelectionRange(begin + mid, end + mid) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值
}const onKeyDown = (e) => {if ((e.ctrlKey || e.metaKey)) {e.preventDefault()switch(e.key){case 'b':addMark('****')breakcase 'i':addMark('**')breakcase '1':addMark('# ', OPERATIONTYPE.LEFT)breakcase '2':addMark('## ', OPERATIONTYPE.LEFT)breakcase '3':addMark('### ', OPERATIONTYPE.LEFT)breakcase '4':addMark('#### ', OPERATIONTYPE.LEFT)breakcase '5':addMark('##### ', OPERATIONTYPE.LEFT)breakcase '6':addMark('###### ', OPERATIONTYPE.LEFT)break}}
}

步骤五:选中文字再次点击应该去除之前添加的字符,这里我对addMark方法进行了改造,当需要添加的字符和本身选中字符串左右两边的字符相同时,去除这些字符

  • 这里有个小细节点:当去除字符时的mid值应该取反,因为去除字符后光标会左移
  // 传入增加的元素,操作类型const addMark = (mark, type = OPERATIONTYPE.BETWEEN) =>{const begin = edit.current.selectionStart  // 光标开始点const end = edit.current.selectionEnd // 光标结束let mid = 0let newValue = ''switch(type){case OPERATIONTYPE.BETWEEN:mid = mark.length / 2 // 比如mark为****,需要在选中字符串前面加上两个**,后面加上两个**// 增加取消的功能if(content.slice(begin - mid, begin) === mark.slice(0, mid) && content.slice(end , end + mid) === mark.slice(mid)){newValue = (content.slice(0, begin - mid) +content.slice(begin, end)  +  content.slice(end + mid));mid = - mid}else{mid = mark.length / 2 // 比如mark为****,需要在选中字符串前面加上两个**,后面加上两个**newValue = (content.slice(0, begin) + mark.slice(0, mid) +content.slice(begin, end) + mark.slice(mid) +  content.slice(end));}breakcase OPERATIONTYPE.LEFT:mid = mark.lengthif(content.slice(begin - mid, begin) === mark){newValue = (content.slice(0, begin - mid) +content.slice(begin, end) +  content.slice(end));mid = - mid}else{newValue = (content.slice(0, begin) + mark.slice(0, mid) +content.slice(begin, end) +  content.slice(end));}break}edit.current.value = newValue // 设置textarea中的值edit.current.setSelectionRange(begin + mid, end + mid) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值}

步骤六:需要处理一下Tab键的缩进

  • 这里我设置了一个常量TABINDENT,用于设置Tab缩进的空格数
  • 并定义了一个获取textarea相关信息的方法getTextareaInfo
  • 当监听到用户按下tab键时,在当前行前面加上相应的空格数
  const TABINDENT = 2 // 缩进个数const getTextareaInfo = (textarea, textContent) => {let cursorPositionStart = textarea.selectionStart // 光标开始的位置let cursorPositionEnd = textarea.selectionEnd// 光标开始的位置let cursorLineIndex = textContent.substring(0, cursorPositionStart).split('\n').length - 1 // 光标所在行的indexlet textLineArray = textContent.split('\n') // 将每行切割成数组let cursorLineContent = textLineArray[cursorLineIndex] // 光标所在行的内容return {cursorPositionStart,cursorPositionEnd,cursorLineIndex,textLineArray,cursorLineContent}}const onKeyDown = (e) => {// tab键if(e.key === 'Tab'){e.preventDefault()// 需要将光标所在行的前面添加上空格const {cursorLineIndex, textLineArray, cursorPositionStart, cursorPositionEnd} = getTextareaInfo(edit.current, content)// 缩进for(let i = 0; i < TABINDENT;i ++){textLineArray[cursorLineIndex] = ' ' + textLineArray[cursorLineIndex]}const newValue = textLineArray.join('\n')edit.current.value = newValue // 设置textarea中的值let len = 0for(let i = 0; i < cursorLineIndex; i++){len += textLineArray[i].length}edit.current.setSelectionRange(cursorPositionStart + TABINDENT, cursorPositionEnd + TABINDENT) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值}// 快捷键if ((e.ctrlKey || e.metaKey)) {e.preventDefault()switch(e.key){case 'b':addMark('****')breakcase 'i':addMark('**')breakcase '1':addMark('# ', OPERATIONTYPE.LEFT)breakcase '2':addMark('## ', OPERATIONTYPE.LEFT)breakcase '3':addMark('### ', OPERATIONTYPE.LEFT)breakcase '4':addMark('#### ', OPERATIONTYPE.LEFT)breakcase '5':addMark('##### ', OPERATIONTYPE.LEFT)breakcase '6':addMark('###### ', OPERATIONTYPE.LEFT)break}}
}

优化了一下,如果选中的是多行,应该多行都缩进

  • 修改了getTextareaInfo方法,把结束行的信息也返回了
  • 把之前写在e.key === 'Tab’条件下的内容抽到了一个单独的方法,按下tab时给选中项的每一行都加上了缩进。(关键代码是这个循环:let line = cursorLineIndexStart; line <= cursorLineIndexEnd; line++ )
const getTextareaInfo = (textarea, textContent) => {let cursorPositionStart = textarea.selectionStart // 光标开始的位置let cursorPositionEnd = textarea.selectionEnd// 光标开始的位置let cursorLineIndexStart = textContent.substring(0, cursorPositionStart).split('\n').length - 1 // 光标开始行的indexlet cursorLineIndexEnd = textContent.substring(0, cursorPositionEnd).split('\n').length - 1 // 光标开始行的indexlet textLineArray = textContent.split('\n') // 将每行切割成数组return {cursorPositionStart,cursorPositionEnd,cursorLineIndexStart,cursorLineIndexEnd,textLineArray,}
}const handleTab = () => {// 需要将光标所在行的前面添加上空格const {cursorLineIndexStart, cursorLineIndexEnd, textLineArray, cursorPositionStart, cursorPositionEnd} = getTextareaInfo(edit.current, content)// 缩进for(let line = cursorLineIndexStart; line <= cursorLineIndexEnd; line++ ){for(let i = 0; i < TABINDENT;i ++){textLineArray[line] = ' ' + textLineArray[line]}}const newValue = textLineArray.join('\n')edit.current.value = newValue // 设置textarea中的值let len = 0for(let i = 0; i < cursorLineIndexStart; i++){len += textLineArray[i].length}edit.current.setSelectionRange(cursorPositionStart + TABINDENT, cursorPositionEnd + (cursorLineIndexEnd - cursorLineIndexStart + 1) * TABINDENT) // 设置光标edit.current.focus() setContent(newValue) // 更新content的值
}

步骤七:突然想起来一个功能:按下command/contrl+x时应该删除这一行,我在写代码的时候经常用

  • 编写handleShear方法,把选中的行都删除
const handleShear = () => {// 需要将光标所在行的前面添加上空格const {cursorLineIndexStart, cursorLineIndexEnd, textLineArray, cursorPositionStart} = getTextareaInfo(edit.current, content)const newTextLineArray = textLineArray.slice(0, cursorLineIndexStart).concat(textLineArray.slice(cursorLineIndexEnd + 1))const newValue = newTextLineArray.join('\n')edit.current.value = newValue // 设置textarea中的值let len = 0for(let i = 0; i < cursorLineIndexStart; i++){len += textLineArray[i].length}edit.current.setSelectionRange(cursorPositionStart, cursorPositionStart) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值
}

既然实现了剪切,那肯定要粘贴(在handleShear方法复制即可)

// 安装:
npm i --save copy-to-clipboard// 使用:
copy(textLineArray.slice(cursorLineIndexStart, cursorLineIndexEnd).join('\n'));

那顺便实现一下复制粘贴的功能吧

// 复制方法command/contrl+cconst handleCopy = () => {// 需要将光标所在行的前面添加上空格const {cursorLineIndexStart, cursorLineIndexEnd, textLineArray} = getTextareaInfo(edit.current, content)copy(textLineArray.slice(cursorLineIndexStart, cursorLineIndexEnd).join('\n'));}// 粘贴方法command/contrl+vconst handlePaste = async () => {const coptText = await navigator.clipboard.readText()// 需要将光标所在行的前面添加上空格const {cursorPositionStart, cursorPositionEnd} = getTextareaInfo(edit.current, content)const newValue = content.slice(0, cursorPositionStart) + coptText + content.slice(cursorPositionEnd)edit.current.value = newValue // 设置textarea中的值edit.current.setSelectionRange(cursorPositionStart, cursorPositionStart + coptText.length) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值}

对了,还有回车,如果上面是列表,回车的时候也需要是列表

  const handleEnter = (e) => {// 其实只要上一行有没有-开头或者1.开头就好了// 需要将光标所在行的前面添加上空格const {cursorLineIndexStart, cursorPositionEnd, textLineArray, cursorPositionStart} = getTextareaInfo(edit.current, content)const preLine = textLineArray[cursorLineIndexStart]console.log(preLine)if(preLine.indexOf('- ') === 0){e.preventDefault()const newValue = edit.current.value + '\n- 'edit.current.value = newValue// 设置textarea中的值edit.current.setSelectionRange(cursorPositionEnd + 3, cursorPositionEnd + 3) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值}else if(/^\d+\. /.test(preLine)){const num = preLine.match(/^\d+/)[0]e.preventDefault()const newValue = edit.current.value +   `\n${Number(num)+1}. ` edit.current.value = newValue// 设置textarea中的值edit.current.setSelectionRange(cursorPositionEnd + 3 + num.length, cursorPositionEnd + 3 + num.length) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值}}

忘了还有全选!(command/contrl+a)

  const handleSelectAll = () => {// 需要将光标所在行的前面添加上空格edit.current.setSelectionRange(0, content.length) // 设置光标的位置edit.current.focus() }

6.添加图片

图片上传部分我参考csdn,做了一个弹窗,分上传图片tab和添加链接tab,弹窗部分自由实现呀~

弹窗的功能主要是点击确定后把上传图片的链接 或 添加的链接抛出来,然后拼接到内容上就行

const [addImgModalVisible, setAddImgModalVisible] = useState(false)<AddImageModal visible={addImgModalVisible} setVisible={setAddImgModalVisible} saveUrl={saveUrl}/>const saveUrl = (url, callback) => {const {cursorPositionStart, cursorPositionEnd} = getTextareaInfo(edit.current, content)const newValue = content.slice(0, cursorPositionStart) + `![在这里插入图片描述](${url})` + content.slice(cursorPositionEnd)edit.current.value = newValue// 设置textarea中的值edit.current.setSelectionRange(cursorPositionEnd + 13, cursorPositionEnd + 13 + url.length) // 设置光标的位置edit.current.focus() setContent(newValue) // 更新content的值// 把url的地址拼接到开始光标的位置即可callback && callback()
}11

7.其他问题

一:文章内容刷新之后会消失!我使用的是localstroage存储,有更好的方法可以告知我哦

  1. 添加一个暂存按钮
  2. 初始化的时候如果localStorage有存储,则直接把值设置上去
  3. 在发布文章之后记得把localStorage的值清空哈(window.localStorage.removeItem(STAGINGPOST))
const STAGINGPOST = "stagingpost"useEffect(() => {let stagingpost = window.localStorage.getItem(STAGINGPOST)if(stagingpost){edit.current.value = stagingpost// 设置textarea中的值setContent(stagingpost) // 更新content的值}},[])<Button onClick={() => {window.localStorage.setItem(STAGINGPOST, content)message.success('暂存成功')
}}>暂存</Button>

二:把编辑器组件抽取成单独的方法,实现内容发布的功能

  • 主要使用了useImperativeHandle和forwardRef
  • 使用forwardRef包裹你写的这个组件,然后把相关的内容暴露出去
import {useImperativeHandle, forwardRef } from 'react';const Editor = forwardRef((props, ref) => {
...useImperativeHandle(ref, () => ({edit,show,content,setContent}))
...
}
  • 父组件在调用时的使用方法如下:
const EditorRef = useRef()// 调用的话使用这种写法:EditorRef.current.edit/EditorRef.current.show/EditorRef.current.content/EditorRef.current.setContent<Editor ref={EditorRef} extraOperation={extraOperation}/>

对了,得把添加图片弹框的逻辑也抽取出来哦~

还会持续优化的哦,后面的内容还在开发学习中…

相关文章:

react实现markdown

参考&#xff1a;https://blog.csdn.net/Jack_lzx/article/details/118495763 参考&#xff1a;https://blog.csdn.net/m0_48474585/article/details/119742984 0. 示例 用react实现markdown编辑器 1.基本布局及样式 <><div classNametf_editor_header>头部&…...

HTTP请求走私漏洞简单分析

文章目录 HTTP请求走私漏洞的产生HTTP请求走私漏洞的分类HTTP请求走私攻击的危害确认HTTP请求走私漏洞通过时间延迟技术确认CL漏洞通过时间延迟技术寻找TE.CL漏洞 使用差异响应内容确认漏洞通过差异响应确认CL.TE漏洞通过差异响应确认TE.CL漏洞 请求走私漏洞的利用通过请求漏洞…...

BI-SQL丨两表差异比较

BOSS&#xff1a;哎&#xff0c;白茶&#xff0c;我们最近新上了一个系统&#xff0c;后续有一些数据要进行源切换&#xff0c;这个能整么&#xff1f; 白茶&#xff1a;没问题&#xff0c;可以整&#xff01; BOSS&#xff1a;哦&#xff0c;对了&#xff0c;差点忘记告诉你了…...

ZooKeeper 选举的过半机制防止脑裂

结论&#xff1a; Zookeeper采用过半选举机制&#xff0c;防止了脑裂。 原因&#xff1a; 如果有5台节点&#xff0c;leader联系不上了&#xff0c;其他4个节点由于超过半数&#xff0c;所以又选出了一个leader&#xff0c;当失联的leader恢复网络时&#xff0c;发现集群中已…...

【图论】树上差分(边差分)

一.简介 其实点差分和边差分区别不大。 点差分中&#xff0c;d数组存储的是树上的节点 边差分中&#xff0c;d数组存储的是当前节点到父节点的那条边的差分值。 指定注意的是&#xff1a;边差分中因为根连的父节点是虚点&#xff0c;所以遍历结果时应当忽略&#xff01; 二…...

RT1052的定时器

文章目录 1 通用定时器1.1 定时器框图1.2 实现周期性中断 2 相关寄存器3 定时器配置3.1 时钟使能3.2 初始化GPT1定时器3.2.1 base3.2.2 initConfig3.2.2.1 clockSorce3.2.2.2 divider3.2.2.3 enablexxxxx 3.3 设置 GPT1 比较值3.3.1 base3.3.2 channel3.3.3 value 3.4 设置 GPT…...

opencv python 训练自己的分类器

源码下载 一、分类器制作 1.样本准备 收集好你所需的正样本&#xff0c;和负样本&#xff0c;分别保存在不同文件夹 在pycharm新建项目&#xff0c;项目结构如下&#xff1a;has_mask文件夹放置正样本&#xff0c;no_mask文件夹放置负样本 安装opencv&#xff0c;把opencv包…...

详解Mybatis之分页插件【PageHelper】

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 操作系统&#xff1a;win10 x64 位 家庭版 Maven版本&#xff1a;apache-maven-3.6.3 Mybatis版本&#xff1a;3.5.6 文章目录 一. 什么是分页&#xff1f;二. 为什么使用分页&#xff1f;三. 如何设计一个Page类&#xff08;分…...

【基于矢量射线的衍射积分 (VRBDI)】基于矢量射线的衍射积分 (VRBDI) 和仿真工具(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

基于jackson对bean的序列号和反序列化

通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致&#xff0c;这是怎么回事呢? 分页查询时服务端响应给页面的数据中id的值为19位数字&#xff0c;类型为long 页面中js处理long型数字只能精确到前16位&#xff0c;所以最终通过ajax请求提交给服务…...

排队理论简介

排队理论简介 1. 理论背景2. 研究的数学方法3. 拒绝型排队系统与等候型排队系统4. 拒绝型排队系统 本文参考文献为Вентцель Е. С.的《Исследование операций》。 1. 理论背景 排队理论又称大众服务理论&#xff0c;顾名思义指的是在有限的服务条…...

极速查找(3)-算法分析

篇前小言 本篇文章是对查找&#xff08;2&#xff09;的续讲二叉排序树 二叉排序树&#xff08;Binary Search Tree&#xff0c;BST&#xff09;&#xff0c;又称为二叉查找树&#xff0c;是一种特殊的二叉树。性质&#xff1a; 左子树的节点值小于根节点的值&#xff0c;右…...

http 常见的响应状态码 ?

100——客户必须继续发出请求101——客户要求服务器根据请求转换HTTP协议版本200——交易成功201——提示知道新文件的URL202——接受和处理、但处理未完成203——返回信息不确定或不完整204——请求收到&#xff0c;但返回信息为空205——服务器完成了请求&#xff0c;用户代理…...

机器学习笔记之优化算法(四)线搜索方法(步长角度;非精确搜索)

机器学习笔记之优化算法——线搜索方法[步长角度&#xff0c;非精确搜索] 引言回顾&#xff1a;精确搜索步长及其弊端非精确搜索近似求解最优步长的条件反例论述 引言 上一节介绍了从精确搜索的步长角度观察了线搜索方法&#xff0c;本节将从非精确搜索的步长角度重新观察线搜…...

Redis 哨兵 (sentinel)

是什么 官网理论&#xff1a;https://redis.io/docs/management/sentinel/ 吹哨人巡查监控后台 master 主机是否故障&#xff0c;如果故障了根据投票数自动将某一个从库转换为新主库&#xff0c;继续对外服务。 作用&#xff1a;无人值守运维 哨兵的作用&#xff1a; 1…...

统计2021年10月每个退货率不大于0.5的商品各项指标

统计2021年10月每个退货率不大于0.5的商品各项指标_牛客题霸_牛客网s mysql&#xff08;ifnull&#xff09;&#xff1a; select product_id, format(ifnull(sum(if_click)/nullif(count(*),0),0),3) as ctr, format(ifnull(sum(if_cart)/nullif(sum(if_click),0),0),3) as c…...

【小波尺度谱】从分段离散小波变换计算小波尺度谱研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

UE5、CesiumForUnreal加载无高度地形

文章目录 1.实现目标2.实现过程3.参考资料1.实现目标 在UE5中,CesiumForUnreal插件默认的地形都是带高度的,这里加载没有高度的地形,即大地高程为0,GIF动图如下: 2.实现过程 参考官方的教程,下载无高度的DEM,再切片加载到UE中。 (1)下载无高度地形DEM0。 在官方帖子…...

关于Spring中的@Configuration中的proxyBeanMethods属性

Configuration的proxyBeanMethods属性 在Configuration注解中&#xff0c;有两个属性&#xff1a; value配置Bean名称proxyBeanMethos&#xff0c;默认是true 这个proxyBeanMethods的默认属性是true。 直接说&#xff1a;当Configuration注解的proxyBeanMeathods属性是true…...

dp1,ACM暑期培训

D - 摆花 P1077 [NOIP2012 普及组] 摆花 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) Description 小明的花店新开张&#xff0c;为了吸引顾客&#xff0c;他想在花店的门口摆上一排花&#xff0c;共 m 盆。通过调查顾客的喜好&#xff0c;小明列出了顾客最喜欢的 n 种花&…...

大厂程序员的水平比非大厂高很多嘛?

最近一个月&#xff0c;筛选了一百多份简历&#xff0c;前前后后面试了二三十人&#xff0c;基本上都是有大厂经历的人。同时&#xff0c;也录用了几个有大厂经历的。但整体而言&#xff0c;打破了对大厂出来的都是优质人才的幻觉。看到的实际情况与想象中的落差还是比较大的。…...

Java开发工具MyEclipse发布v2023.1.2,今年第二个修复版!

MyEclipse一次性提供了巨量的Eclipse插件库&#xff0c;无需学习任何新的开发语言和工具&#xff0c;便可在一体化的IDE下进行Java EE、Web和PhoneGap移动应用的开发&#xff1b;强大的智能代码补齐功能&#xff0c;让企业开发化繁为简。 MyEclipse v2023.1.2官方正式版下载 …...

基于正交滤波器组的语音DPCM编解码算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ...........................................................g0zeros(1,lenH); g1zeros(1,l…...

VS2022和QT混合编程打包发布程序

1.在开始菜单输入 CMD 找到 Qt5.15.2(MSVC 64-bit) 2.输入windeployqt exe所在路径 3.运行完毕后&#xff0c;双击打开exe文件&#xff0c;可能会报错&#xff0c;缺少相关的dll,找到缺少的dll拷贝到运行文件夹下即可。...

Filebeat学习笔记

Filebeat基本概念 简介 Filebeat是一种轻量级日志采集器&#xff0c;内置有多种模块&#xff08;auditd、Apache、Nginx、System、MySQL等&#xff09;&#xff0c;针对常见格式的日志大大简化收集、解析和可视化过程&#xff0c;只需一条命令即可。之所以能实现这一点&#…...

【实战】 九、深入React 状态管理与Redux机制(一) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十六)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理七、Hook&…...

第九十五回 如何使用dio的转换器

文章目录 概念介绍使用方法使用默认的转换器自定义转换器 示例代码经验分享 我们在上一章回中介绍了"如何打造一个网络框架"相关的内容&#xff0c;本章回中将介绍 如何使用dio的转换器.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 转换器主要用来转…...

Python深度学习“四大名著”之一【赠书活动|第二期《Python机器学习:基于PyTorch和Scikit-Learn》】

近年来&#xff0c;机器学习方法凭借其理解海量数据和自主决策的能力&#xff0c;已在医疗保健、 机器人、生物学、物理学、大众消费和互联网服务等行业得到了广泛的应用。自从AlexNet模型在2012年ImageNet大赛被提出以来&#xff0c;机器学习和深度学习迅猛发展&#xff0c;取…...

RAID相关知识

简介 RAID &#xff08; Redundant Array of Independent Disks &#xff09;即独立磁盘冗余阵列&#xff0c;通常简称为磁盘阵列。RAID技术将多个单独的物理硬盘以不同的方式组合成一个逻辑磁盘&#xff0c;从而提高硬盘的读写性能和数据安全性。 数据组织形式 分块&#x…...

DataStructure--Basic

程序设计数据结构算法 只谈数据结构不谈算法就跟去话剧院看梁山伯与祝英台结果只有梁山伯在演&#xff0c;祝英台生病了没来一样。 本文的所有内容都出自《大话数据结构》这本书中的代码实现部分&#xff0c;建议看书&#xff0c;书中比我本文写的全。 数据结构&#xff0c;直…...

政法门户网站建设情况/软文营销经典案例200字

三联虹普(SZ300384) - 低于120日均线&#xff1a;-50.46% - 滚动市盈率PE&#xff1a;38.09 - 动态年化股息收益率&#xff1a;1.97% - 最近的减持消息&#xff1a;332天前 - 其他不利消息&#xff1a;577天前 日机密封(SZ300470) - 低于120日均线&#xff1a;-38.39% - 滚动市…...

wordpress主页显示分类/短视频seo公司

为了弄清楚 LinkedList 与ArrayList的性能差异&#xff0c;特作了以下几组测试&#xff1a; 一 各插入一百万条数据&#xff1a; 以下是测试LinkedList&#xff1a; importjava.util.LinkedList;importutils.system;publicclassCollection ...{ /** *//** * TODO 描述…...

山东省个人网站备案/河源市企业网站seo价格

1、flush&#xff0c;从memstore刷到hdfs的时候&#xff0c;只会把内存中同一cell最新的一个版数据刷到hdfs中。 2、compact(major),合并文件时&#xff0c;并且是major合并&#xff0c;把所有hfile文件都合并为一个&#xff0c;多个hfile文件中相同cell只会把最新的一个版本的…...

做期货在哪个网站看消息/百色seo快速排名

链表08--删除链表中重复节点-jz56题目概述解析&参考答案注意事项说明题目概述 算法说明 在一个排序的链表中&#xff0c;存在重复的结点&#xff0c;请删除该链表中重复的结点&#xff0c;重复的结点不保留&#xff0c;返回链表头指针。 例如&#xff0c;链表1->2->…...

专业做家政网站/域名解析ip

项目启动出现这个异常&#xff1a; Mapped Statements collection already contains value for 我这边的原因是在 Mapper 中出现了重载方法。后来想了一下&#xff0c;也很好理解&#xff0c;MyBatis 将方法名当作 id 进行映射&#xff0c;所以就会出现这个异常。...

云服务器做视频网站/营销推广文案

1.一把斧 https://blog.csdn.net/qq_32040767/article/details/77096680 二把斧 https://blog.csdn.net/lesaqiu/article/details/54846960 3.三把斧 点击菜单中的 “File” -> “Invalidate Caches / Restart”&#xff0c;然后点击对话框中的 “Invalidate and Resta…...