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

Electron-vite【实战】MD 编辑器 -- 大纲区(含自动生成大纲,大纲缩进,折叠大纲,滚动同步高亮大纲,点击大纲滚动等)

最终效果

在这里插入图片描述

页面

    <!-- 大纲 --><div v-if="currentFilePath && outlineList.length" class="outlinePanel"><div class="panelTitle">大纲</div><div class="searchTitleBox"><Icon class="searchTitleInputIcon" icon="material-symbols-light:search" /><inputv-model="searchTitleKeyWord"class="searchTitleInput"type="text"placeholder="请输入标题"/><Iconv-show="searchTitleKeyWord"class="clearSearchTitleInputBtn"icon="codex:cross"@click="clearSearchTitleInput"/></div><div class="outlineListBox"><template v-for="(item, index) in outlineList_filtered"><divv-if="!item.hide":key="index":title="item.text"class="outlineItem":style="{ color: currentOutLineID === item.id ? 'red' : '' }"@click="handle_clickOutLine(item)"><div v-for="i in item.level" :key="i"><Iconv-if="index < outlineList_filtered.length - 1 &&item.level < outlineList_filtered[index + 1].level &&i === item.level":icon="item.collapsed ? 'iconamoon:arrow-right-2-light' : 'iconamoon:arrow-down-2-light'"@click.stop="toggleCollapse(item, index)"/><Icon v-else icon="" /></div><div style="margin-left: 4px">{{ item.text }}</div></div></template></div></div>

不同级别标题的缩进和折叠图标的实现

            <div v-for="i in item.level" :key="i"><Iconv-if="index < outlineList_filtered.length - 1 &&item.level < outlineList_filtered[index + 1].level &&i === item.level":icon="item.collapsed ? 'iconamoon:arrow-right-2-light' : 'iconamoon:arrow-down-2-light'"@click.stop="toggleCollapse(item, index)"/><Icon v-else icon="" /></div>
  • 按 level 值,渲染空白图标, level 值越大,缩进越多。
  • 仅当下一行标题的 level 更大,即有子标题时,显示大纲折叠图标

相关样式

/* 大纲的样式 */
.outlinePanel {width: 200px;border: 1px solid gray;border-left: none;display: flex;flex-direction: column;font-size: 14px;
}
.searchTitleBox {display: flex;align-items: center;justify-content: center;padding: 10px;
}
.searchTitleInput {display: block;font-size: 12px;padding: 4px 20px;
}
.searchTitleInputIcon {position: absolute;font-size: 16px;transform: translateX(-80px);
}
.clearSearchTitleInputBtn {position: absolute;cursor: pointer;font-size: 16px;transform: translateX(77px);
}
.outlineListBox {padding: 10px;line-height: 1.5;flex: 1;overflow-y: auto;
}
.outlineItem {cursor: pointer;text-wrap: nowrap;display: flex;align-items: center;color: #787474;margin-bottom: 6px;
}

相关变量

// 大纲相关变量
const searchTitleKeyWord = ref('') // 搜索大纲的关键词
const currentOutLineID = ref('0') // 当前选中的大纲项的id
const ifClickOutLine = ref(false) // 是否点击了大纲项
// 计算属性:根据markdownContent生成大纲列表
const outlineList = computed(() => {const originalList = MarkdownParser(markdownContent.value).outline || []// 使用 reactive 让大纲项的每个属性都是响应式return originalList.map((item) => reactive({ ...item, collapsed: false }))
})
// 计算属性:根据searchTitleKeyWord过滤大纲列表
const outlineList_filtered = computed(() => {let result = outlineList.value.filter((outline) => {return outline.text.toLowerCase().includes(searchTitleKeyWord.value.toLowerCase())})return result
})

核心方法

根据 markdown 内容生成大纲

const originalList = MarkdownParser(markdownContent.value).outline || []

在 MarkdownParser 方法中,遍历每一行时,将标题行转换为目标格式,存入 outlineList 中

    // 标题if (line.startsWith('#')) {const resultTemp = parseMarkdownHeadings(line, index)if (resultTemp) {line = resultTemp.contentoutlineList.push({...resultTemp.outline,index})}}

最终返回

  return {content: result,outline: outlineList}

其中格式解析的方法 parseMarkdownHeadings 如下:

/*** 将 Markdown 标题(# 开头)转换为 HTML 标题标签* @param markdown - 输入的 Markdown 文本* @returns 转换后的 HTML 文本*/
function parseMarkdownHeadings(markdown: string,index: number
): {content: stringoutline: outlineItemType
} | void {// 正则表达式匹配 Markdown 标题const headingRegex = /^(#+)\s+(.*)$/gmconst match = headingRegex.exec(markdown)if (match) {const level = match[1].length // 标题等级由#的数量决定const text = match[2].trim()const id = index.toString() // 生成锚点IDreturn {content: `<h${level}  id=${id}  >${escapeHTML(text)}</h${level}>`,outline: {text,id,level}}}
}

切换大纲项的折叠状态

// 切换大纲项的折叠状态
const toggleCollapse = (item: outlineItemType, index: number): void => {item.collapsed = !item.collapsedfor (let i = index + 1, len = outlineList_filtered.value.length; i < len; i++) {const outline = outlineList_filtered.value[i]if (outline.level > item.level) {outline.hide = item.collapsed} else {break}}
}

点击大纲项滚动编辑区和预览区

在这里插入图片描述

 @click="handle_clickOutLine(item)"
// 点击大纲项
const handle_clickOutLine = (item): void => {currentOutLineID.value = item.idconst editor = editorRef.valueif (editor) {ifClickOutLine.value = true// 编辑区滚动滚动到指定行const lineHeight = parseInt(getComputedStyle(editor).lineHeight)const paddingTop = parseInt(getComputedStyle(editor).paddingTop)editor.scrollTo({top: paddingTop + item.index * lineHeight,behavior: 'smooth'})}const preview = previewRef.valueif (preview) {const targetDom = document.getElementById(item.id)if (targetDom) {targetDom.scrollIntoView({behavior: 'smooth',block: 'start'})setTimeout(() => {// 延迟1s后,将 ifClickOutLine 置为 false,防止点击大纲项时,在滚动编辑区时,同步滚动预览区ifClickOutLine.value = false}, 1000)}}
}

点击大纲项时,禁止编辑区的滚动导致预览区同步滚动

// 同步预览区滚动
const syncPreviewScroll = (): void => {// 点击大纲项时,不触发同步预览区滚动if (editorRef.value && previewRef.value && !ifClickOutLine.value) {const editor = editorRef.valueconst preview = previewRef.valueconst editorScrollRatio = editor.scrollTop / (editor.scrollHeight - editor.clientHeight)const previewScrollTop = editorScrollRatio * (preview.scrollHeight - preview.clientHeight)preview.scrollTop = previewScrollTop}
}

滚动预览区时,当前高亮大纲同步变化

onMounted(() => {if (previewRef.value) {// 监听滚动事件以更新当前大纲项IDpreviewRef.value.addEventListener('scroll', update_currentOutlineId)}
// 更新当前高亮大纲
const update_currentOutlineId = (): void => {if (ifClickOutLine.value) {return}if (!previewRef.value) returnconst headings = previewRef.value.querySelectorAll('h1, h2, h3, h4, h5, h6')let lastId = ''for (const heading of headings) {// 当标题即将超出预览区域时,停止查找更新if ((heading as HTMLElement).offsetTop - 60 >= previewRef.value.scrollTop) {break}lastId = heading.id}currentOutLineID.value = lastId
}

onBeforeUnmount 中

  if (previewRef.value) {previewRef.value.removeEventListener('click', handle_preview_click)previewRef.value.removeEventListener('scroll', update_currentOutlineId)}
http://www.xdnf.cn/news/13420.html

相关文章:

  • 【读论文】Closed-loop Diffusion Control of Complex Physical Systems 闭环扩散控制系统
  • 汽车制造通信革新:网关模块让EtherCAT成功对接CCLINK
  • 神经网络全景图:五大核心架构详解与本质区别
  • CUDA 与 cuDNN 免登录下载政策详解(基于官方权威信息)
  • docker和docker-compose的版本对应关系怎么看?
  • CVE-2017-12615源码分析与漏洞复现(Tomcat 任意文件上传)
  • DAY 46 超大力王爱学Python
  • 矩阵批量剪辑源码开发,OEM贴牌
  • SQL进阶之旅 Day 25:高并发环境下的SQL优化
  • 04__C++特殊的函数语法
  • 摄影入门:相机基本参数解析
  • MES生产工单管理系统,Java+Vue,含源码与文档,高效统筹生产流程,精准管控工单执行与进度
  • 为 Nginx 配置 HTTPS(以 n8n 为例)完整教程【CentOS 7】
  • 【编译工具】(调试)Chrome DevTools + Postman:调试组合如何让我的开发效率提升400%?
  • 蛋糕烘焙小程序源码介绍
  • ubuntuserver24.04版本:redis编译安装时出现工具、依赖库问题解决方法
  • 「Java基本语法」运算符与表达式
  • Java多线程实现之线程调度详解
  • 35. 搜索插入位置
  • OpenLayers 可视化之热力图
  • 滑动窗口最大值和最小值
  • 理解 PostgreSQL 中的 Virtual Transaction ID(VXID)
  • 123123
  • 【Java】动态加载数据库驱动:灵活连接
  • IMX6ULL--EPIT 定时器理论
  • 打卡第41天:训练和测试的规范写法
  • C/C++八股文
  • 面试题 - 日志(Java)
  • 网络爬虫学习心得
  • 智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql