提交 4665e8e3 编写于 作者: 郭维嘉

feat:添加目录

上级 e52f5f9d
无法预览此类型文件
......@@ -28,7 +28,10 @@
<div id="app"></div>
<!-- <textarea class="text" name="" id="" cols="30" rows="10"></textarea> -->
<!-- <div contenteditable="true" class="text"></div> -->
<a href="https://codechina.csdn.net/xiongjiamu/jupyter-101/-/blob/master/002-demo.ipynb" class="jupyterEl"></a>
<a
href="https://codechina.csdn.net/xiongjiamu/jupyter-101/-/blob/master/002-demo.ipynb"
class="jupyterEl"
></a>
<button id="a">111111</button>
<button id="b">2222222</button>
<script src="./markdown-editor.js"></script>
......@@ -261,7 +264,27 @@
return ee.getVideoList();
}
document.querySelector("#a").onclick = async function() {
console.log(await test());
ee.setValue(`
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
#### 四级标题2四级标题2四级标题2四级标题2四级标题2四级标题2四级标题2四级标题2
#### 四级标题3
#### 四级标题4
### 三级标题2
### 三级标题3
## 二级标题2
## 二级标题3
## 二级标题4
## 二级标题5
### 三级标题4
[toc]
`);
// ee.disable();
// ee.getVideoList(list => {
......
此差异已折叠。
......@@ -22,7 +22,7 @@
:registerTools="registerTools"
@upload="handleUpload"
@getFormatType="formatType = $event"
@updateShowHelp="showHelp = $event"
@updateShowDoc="showHelp = $event"
@renderLinks="$emit('renderLinks', $event)"
/>
<input
......@@ -41,6 +41,9 @@
:height="textareaHeight"
:html.sync="html"
:htmlMinHeight="htmlMinHeight"
:show-help="showHelp"
:dirTags="dirTags"
@updateShowDoc="showHelp = $event"
v-if="showPreview"
/>
......@@ -69,8 +72,9 @@
@submit="submit"
@enter="handleEnter"
@getFilteredTags="filteredTags = $event"
@updateShowHelp="showHelp = $event"
@updateShowDoc="showHelp = $event"
@renderLinksHtml="renderLinksHtml"
@getDirTags="getDirTags"
@queryUserList="queryUserList"
@callUserList="callUserList = $event"
v-else
......@@ -304,6 +308,7 @@ export default {
uploadFlePercent: 0,
uploadVideoPercent: 0,
textLength: "",
dirTags: [],
userList: false,
callUserList: [],
linkList: [],
......@@ -475,12 +480,36 @@ export default {
}
});
},
getDirTags({ vDom, dirTags }) {
this.dirTags = dirTags;
const tocEls = Array.from(vDom.querySelectorAll(".tocEl"));
if (!tocEls.length) return;
tocEls.forEach(el => {
el.innerHTML = `<ul class="md_toc_list">${dirTags
.map(tag => {
return `<li class="md_toc_item" data-type="${tag.tag}" data-id="${tag.id}">${tag.text}</li>`;
})
.join("")}</ul>`;
});
this.html = vDom.innerHTML;
// console.log(document.querySelectorAll(".md_toc_list"));
document.querySelector("body").addEventListener("click", function(e) {
if (!e.target?.className.includes("md_toc_item")) return;
console.log(e.target.dataset.id);
const targetEl = document.getElementById(e.target?.dataset?.id);
if (!targetEl) return;
targetEl.scrollIntoView({
behavior: "smooth"
});
});
},
renderLinksHtml({ vDom, links }) {
// 缓存里没有的链接,就发送请求获取信息
const emitList = links.filter(
item => !this.linkList.find(link => link && link.url === item.url)
);
console.log("emit", emitList);
this.$emit("renderLinks", {
links: emitList,
callback: list => {
......
......@@ -183,6 +183,13 @@ export function isNotFalse(val) {
return val !== false;
}
export function pick(list, ...arg) {
if (!list.length || !arg?.length) return;
return list.filter(item => {
return arg.find(key => key === item.name);
});
}
export function checktUrl(val, rule) {
if (!val || !rule) return;
const hideEl = document.createElement("div");
......@@ -360,7 +367,7 @@ export function formatElements(html) {
}
});
Array.from(virtualDom.querySelectorAll("img")).forEach(item => {
item.className = 'md_img'
item.className = "md_img";
});
const list = Array.from(new Set(userList)); // 去重
return { callUserList: list, userHtml: virtualDom.innerHTML };
......@@ -368,6 +375,7 @@ export function formatElements(html) {
export function getLinkTags(id, html) {
const virtualDom = document.createElement("div");
virtualDom.innerHTML = html;
// 获取所有a标签
const links = Array.from(
virtualDom.querySelectorAll("a:not([download])")
).map((item, index) => {
......@@ -378,7 +386,18 @@ export function getLinkTags(id, html) {
url: item.href
};
});
return { vDom: virtualDom, links };
// 获取所有标题
const dirTags = Array.from(
virtualDom.querySelectorAll("h1:not(.toc_title),h2,h3,h4,h5,h6")
).map((item, index) => {
const dirItem = {
tag: item.tagName.toLowerCase(),
id: item.id,
text: item.innerText
};
return dirItem;
});
return { vDom: virtualDom, links, dirTags };
}
export function getLinkTitle(linkEl, item) {
......
......@@ -86,6 +86,46 @@ textarea {
}
}
.md_toc_list {
li.md_toc_item {
list-style-type: none;
cursor: pointer;
color: var(--md-editor-border-color-active);
position: relative;
margin: 12px 8px;
&::before {
content: "\·";
font-size: 20px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
&[data-type="h1"] {
padding-left: 20px;
}
&[data-type="h2"] {
padding-left: 30px;
}
&[data-type="h3"] {
padding-left: 40px;
}
&[data-type="h4"] {
padding-left: 50px;
}
&[data-type="h5"] {
padding-left: 60px;
}
&[data-type="h6"] {
padding-left: 70px;
}
}
}
.relative {
position: relative;
}
a.md_call_user {
color: var(--md-editor-text-color-active) !important;
&[href] {
......
......@@ -15,6 +15,10 @@
content: "\e63a";
}
.icon-dir:before {
content: "\e67f";
}
.icon-renwu:before {
content: "\e63f";
}
......@@ -84,7 +88,7 @@
}
.icon-aite:before {
content: "\e634";
content: "\e60b";
}
.icon-fengexian:before {
content: "\e60a";
......
<template>
<div class="doc_frame">
<div class="doc_container">
<h2>
<slot name="title" />
<span
@click="$emit('updateShowDoc', false)"
:class="['icon iconfont', `icon-guanbi`]"
></span>
</h2>
<slot name="content" />
<!-- <ul class="list">
<li v-for="(item, index) in list" :key="index">
<span :class="['icon iconfont', `icon-${item.icon}`]"></span>
<span>{{ item.title }}</span>
<span class="doc">{{ item.doc }}</span>
</li>
</ul> -->
</div>
<div class="before"></div>
<div class="after"></div>
</div>
</template>
<script>
export default {
props: {
showHelp: {
type: [Boolean, String],
default: false
}
},
data() {
return {
list: [
{
title: "一级标题",
doc: "# 标题",
icon: "yijibiaoti"
},
{
title: "二级标题",
doc: "## 标题",
icon: "erjibiaoti"
},
{
title: "三级标题",
doc: "### 标题",
icon: "sanjibiaoti"
},
{
title: "粗体",
doc: "**内容**",
icon: "bold"
},
{
title: "斜体",
doc: "_内容_",
icon: "italic"
},
{
title: "引用",
doc: "> 引用内容",
icon: "yinyong"
},
{
title: "链接",
doc: "[链接标题](url)",
icon: "lianjie"
},
{
title: "图片",
doc: "![alt](url)",
icon: "img"
},
{
title: "图片大小",
doc: '![alt](url "=300x200")',
icon: "img"
},
{
title: "图片位置",
doc: '![alt](url "#left")',
icon: "img"
},
{
title: "图片名称",
doc: '![alt](url "%title")',
icon: "img"
},
{
title: "代码",
doc: "`代码`",
icon: "daimakuai"
},
{
title: "代码块",
doc: "```编程语言↵代码```",
icon: "code"
},
{
title: "无序列表",
doc: "- 内容",
icon: "unorderedList"
},
{
title: "有序列表",
doc: "1. 内容",
icon: "youxuliebiao"
},
{
title: "任务列表",
doc: "- [ ] 待办事项",
icon: "renwu"
},
{
title: "分割线",
doc: "---",
icon: "fengexian"
},
{
title: "删除线",
doc: "~~内容~~",
icon: "shanchuxian"
}
]
};
}
};
</script>
<style lang="less" scoped>
.doc_frame {
position: absolute;
// background: #fff;
background: var(--md-editor-content-bg-color);
height: calc(100% + 10px);
width: 260px;
top: 0;
right: -16px;
box-sizing: border-box;
border-left: 1px solid var(--md-editor-border-color);
padding: 14px;
padding-right: 0;
// border-radius: 4px;
z-index: 9;
.before {
position: absolute;
top: 4px;
left: 0;
width: 100%;
height: 24px;
pointer-events: none;
background: linear-gradient(
to bottom,
var(--md-editor-content-bg-color),
rgba(255, 255, 255, 0)
);
}
.after {
pointer-events: none;
position: absolute;
bottom: 12px;
left: 0;
width: 100%;
height: 24px;
background: linear-gradient(
to top,
var(--md-editor-content-bg-color),
rgba(255, 255, 255, 0)
);
}
.doc_container {
overflow-y: auto;
padding-right: 14px;
height: 100%;
scrollbar-color: transparent transparent;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
h2 {
font-size: 16px;
color: var(--md-editor-text-color-active);
.icon {
float: right;
margin-top: 3px;
cursor: pointer;
font-weight: 400;
}
}
/deep/ul {
margin-top: 14px;
li {
font-size: 14px;
color: var(--md-editor-helpdoc-color);
margin-bottom: 10px;
.icon {
display: inline-block;
vertical-align: middle;
}
.doc {
float: right;
}
}
}
}
}
</style>
<template>
<div class="help_doc">
<div class="doc_container">
<h2>
Markdown 语法
<span
@click="$emit('updateShowHelp', false)"
:class="['icon iconfont', `icon-guanbi`]"
></span>
</h2>
<ul class="list">
<docFrame @updateShowDoc="$emit('updateShowDoc', $event)">
<template #title> Markdown 语法 </template>
<template #content>
<ul>
<li v-for="(item, index) in list" :key="index">
<span :class="['icon iconfont', `icon-${item.icon}`]"></span>
<span>{{ item.title }}</span>
<span class="doc">{{ item.doc }}</span>
</li>
</ul>
</div>
<div class="before"></div>
<div class="after"></div>
</div>
</template>
</docFrame>
</template>
<script>
import docFrame from "./doc-frame.vue";
export default {
components: { docFrame },
props: {
showHelp: {
type: Boolean,
type: [Boolean, String],
default: false
}
},
......@@ -126,81 +120,3 @@ export default {
}
};
</script>
<style lang="less" scoped>
.help_doc {
position: absolute;
// background: #fff;
background: var(--md-editor-content-bg-color);
height: calc(100% + 10px);
width: 260px;
top: 0;
right: -16px;
box-sizing: border-box;
border-left: 1px solid var(--md-editor-border-color);
padding: 14px;
padding-right: 0;
// border-radius: 4px;
z-index: 9;
.before {
position: absolute;
top: 4px;
left: 0;
width: 100%;
height: 24px;
pointer-events: none;
background: linear-gradient(
to bottom,
var(--md-editor-content-bg-color),
rgba(255, 255, 255, 0)
);
}
.after {
pointer-events: none;
position: absolute;
bottom: 12px;
left: 0;
width: 100%;
height: 24px;
background: linear-gradient(
to top,
var(--md-editor-content-bg-color),
rgba(255, 255, 255, 0)
);
}
.doc_container {
overflow-y: auto;
padding-right: 14px;
height: 100%;
scrollbar-color: transparent transparent;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
h2 {
font-size: 16px;
color: var(--md-editor-text-color-active);
.icon {
float: right;
margin-top: 3px;
cursor: pointer;
font-weight: 400;
}
}
ul.list {
margin-top: 14px;
li {
font-size: 14px;
color: var(--md-editor-helpdoc-color);
margin-bottom: 10px;
.icon {
display: inline-block;
vertical-align: middle;
}
.doc {
float: right;
}
}
}
}
}
</style>
<template>
<docFrame @updateShowDoc="$emit('updateShowDoc', $event)">
<template #title>目录</template>
<template #content>
<ul>
<li
:data-type="item.tag"
@click.prevent.stop="scrollToTitle(item)"
:class="{ active: dirItemActive(item) }"
v-for="item in dirTags"
:key="item.id"
>
<a href="javascript:viod(0)">
{{ item.text }}
</a>
</li>
</ul>
</template>
</docFrame>
</template>
<script>
import docFrame from "./doc-frame.vue";
export default {
components: { docFrame },
props: {
showHelp: {
type: [Boolean, String],
default: false
},
dirTags: {
type: Array,
default: () => []
},
scrollBarTop: {
type: Number,
default: 0
}
},
data() {
return {
list: [
{
title: "一级标题",
doc: "# 标题",
icon: "yijibiaoti"
},
{
title: "二级标题",
doc: "## 标题",
icon: "erjibiaoti"
},
{
title: "三级标题",
doc: "### 标题",
icon: "sanjibiaoti"
}
]
};
},
computed: {
topList() {
return this.dirTags.map(item => Math.abs(item?.top));
}
},
created() {
this.$emit(
"update:scrollBarTop",
document.querySelector(".md_preview_scroll_container").scrollTop
);
},
methods: {
scrollToTitle(item) {
const targetEl = document.getElementById(item.id);
if (!targetEl) return;
const targetOffsetTop = targetEl.offsetTop;
document.querySelector(
".md_preview .md_preview_scroll_container"
).scrollTop = targetOffsetTop;
},
dirItemActive(item) {
const itemScrollTop = document.getElementById(item.id)?.offsetTop;
const top = this.scrollBarTop - itemScrollTop;
this.$set(item, "top", top);
return Math.abs(top) === Math.min(...this.topList);
}
}
};
</script>
<style lang="less" scoped>
/deep/li {
position: relative;
cursor: pointer;
a {
text-decoration: none;
color: inherit;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
&:hover,
&.active {
a {
color: var(--md-editor-border-color-active);
}
&::before {
color: var(--md-editor-border-color-active);
}
}
&::before {
content: "\·";
position: absolute;
left: 0;
top: 50%;
font-size: 20px;
transform: translateY(-50%);
}
&[data-type="h1"] {
padding-left: 20px;
}
&[data-type="h2"] {
padding-left: 30px;
}
&[data-type="h3"] {
padding-left: 40px;
}
&[data-type="h4"] {
padding-left: 50px;
}
&[data-type="h5"] {
padding-left: 60px;
}
&[data-type="h6"] {
padding-left: 70px;
}
}
</style>
<template>
<div :class="['md_preview', { fullScreen }]">
<div
v-html="html"
:style="{
height: height > 0 ? height + 'px' : 'auto',
'min-height': htmlMinHeight + 'px'
}"
></div>
<div class="relative">
<div :class="['md_preview', { fullScreen }]">
<div
class="md_preview_scroll_container"
v-html="html"
@scroll="getScrollBarTop"
:style="{
height: height > 0 ? height + 'px' : 'auto',
'min-height': htmlMinHeight + 'px'
}"
></div>
</div>
<transition name="slide-fade">
<helpDoc
v-if="showHelp === 'help'"
@updateShowDoc="$emit('updateShowDoc', $event)"
:showHelp.sync="showHelp"
/>
<dirDoc
v-if="showHelp === 'dir'"
@updateShowDoc="$emit('updateShowDoc', $event)"
:showHelp.sync="showHelp"
:dirTags="dirTags"
:scrollBarTop.sync="scrollBarTop"
/>
</transition>
</div>
</template>
<script>
import helpDoc from "./components/help-doc.vue";
import dirDoc from "./components/toc-doc.vue";
export default {
components: { helpDoc, dirDoc },
data() {
return {};
return {
scrollBarTop: 0
};
},
props: {
id: {
......@@ -34,6 +57,14 @@ export default {
type: [String, Promise],
default: ""
},
showHelp: {
type: [Boolean, String],
default: false
},
dirTags: {
type: Array,
default: () => []
},
fullScreen: {
type: Boolean,
default: false
......@@ -65,6 +96,12 @@ export default {
item.setAttribute("preload", "auto");
});
}, 0);
},
getScrollBarTop() {
if (this.showHelp === "dir")
this.scrollBarTop = document.querySelector(
".md_preview_scroll_container"
).scrollTop;
}
}
};
......@@ -77,12 +114,17 @@ export default {
color: var(--md-editor-text-color);
word-break: break-all;
overflow-y: auto;
& > div {
.md_preview_scroll_container {
overflow-y: auto;
position: relative;
scroll-behavior: smooth;
}
&.fullScreen {
max-height: calc(100% - 42px);
overflow-y: auto;
}
.help_doc {
height: 100%;
}
}
</style>
......@@ -39,8 +39,13 @@
</textarea>
<transition name="slide-fade">
<helpDoc
v-if="showHelp"
@updateShowHelp="$emit('updateShowHelp', $event)"
v-if="showHelp === 'help'"
@updateShowDoc="$emit('updateShowDoc', $event)"
:showHelp.sync="showHelp"
/>
<dirDoc
v-if="showHelp === 'dir'"
@updateShowDoc="$emit('updateShowDoc', $event)"
:showHelp.sync="showHelp"
/>
</transition>
......@@ -73,11 +78,12 @@ import {
import selectUser from "./components/user-select.vue";
import selectLinkType from "./components/link-type-select.vue";
import helpDoc from "./components/help-doc.vue";
import dirDoc from "./components/toc-doc.vue";
import renderMix from "./mixins/render-mixins";
import selectUserMix from "./mixins/select-user-mixins";
import selectLinkTypeMix from "./mixins/select-link-type-mixins";
export default {
components: { helpDoc, selectUser, selectLinkType },
components: { helpDoc, dirDoc, selectUser, selectLinkType },
mixins: [renderMix, selectUserMix, selectLinkTypeMix],
props: {
id: {
......@@ -139,7 +145,7 @@ export default {
default: ""
},
showHelp: {
type: Boolean,
type: [Boolean, String],
default: false
},
userList: {
......
......@@ -37,13 +37,15 @@ export default {
}); // 去除标签
const filteredTags = getFilteredTags(html, cleanHtml); // 计算是否有标签被过滤
// 链接转换为卡片
const { vDom, links } = getLinkTags(this.id, cleanHtml);
// 获取标题列表
const { vDom, links, dirTags } = getLinkTags(this.id, cleanHtml);
const { callUserList, userHtml } = formatElements(cleanHtml);
// const videoHtml = await renderVideo(this.id, userHtml);
this.$emit("callUserList", callUserList);
this.$emit("getFilteredTags", filteredTags);
this.$emit("update:html", userHtml);
if (links.length) this.$emit("renderLinksHtml", { vDom, links });
if (dirTags.length) this.$emit("getDirTags", { vDom, dirTags });
},
rerender() {
const _this = this;
......@@ -79,6 +81,8 @@ export default {
src="${href}"
></video></p>`;
}
console.log("imgimgimg");
// ![img](...)渲染图片
let out =
'<p class="md_img_container"><img src="' +
......@@ -116,7 +120,37 @@ export default {
out += "/></p>";
return out;
},
heading(text, level, raw, slugger) {
if (this.options.headerIds) {
return (
"<h" +
level +
' id="' +
"h" +
level +
"_" +
this.options.headerPrefix +
slugger.slug(raw) +
"_" +
new Date().getTime() +
'">' +
text +
"</h" +
level +
">\n"
);
}
// ignore IDs
return "<h" + level + ">" + text + "</h" + level + ">\n";
},
link(href, title, text) {
console.log("linklink");
if (text?.toLowerCase() === "toc") {
return `
<h1 class="toc_title">${href}</h1>
<div class="tocEl"></div>`;
}
if (!href && !title) return "";
if (href === null) {
return text;
......@@ -155,9 +189,15 @@ export default {
return `<a type="user" download data-user="${user &&
user.username}" href="${(user && user.url) || "javascript:void(0)"}" class="md_call_user">${val}</a>`;
})
// tab缩进
.replace(/^\s{2,3}(.+)/, function(val) {
return `<span style="display:inline-block;text-indent:2em;">${val}</span>`;
});
})
.replace(
/\[TOC\]/i,
`<h1 class="toc_title">目录</h1><div class="tocEl"></div>`
);
return newText;
}
};
......
......@@ -265,7 +265,10 @@ export default {
this.$emit("setFullScreen", false);
break;
case "help":
this.$emit("updateShowHelp", true);
this.$emit("updateShowDoc", "help");
break;
case "dir":
this.$emit("updateShowDoc", "dir");
break;
default:
break;
......
......@@ -17,7 +17,7 @@
<span>预览</span>
</div>
</div>
<div :class="['header_tools', { disabled }]" v-if="!showPreview">
<div :class="['header_tools', { disabled }]">
<tool-button
:ref="item.name"
:ulNum.sync="ulNum"
......@@ -27,7 +27,7 @@
@updateText="handleUpdateText"
@upload="$emit('upload', $event)"
@setFormatType="setFormatType"
@updateShowHelp="$emit('updateShowHelp', $event)"
@updateShowDoc="$emit('updateShowDoc', $event)"
:class="{ active: item.name === 'format' && formatType }"
v-for="(item, index) in toolsShow"
:key="index"
......@@ -47,7 +47,8 @@ import {
getPosition,
removeBlankLine,
copyFormatRules,
checkBoswer
checkBoswer,
pick
} from "@/assets/js/utils";
import toolButton from "./components/tool-button";
export default {
......@@ -107,10 +108,18 @@ export default {
}
},
computed: {
previewTools() {
const list = this.toolButtonList;
return [
this.dirBtn,
...pick(list, "help", "fullScreen", "cancelFullScreen")
];
},
toolsShow() {
const toolsList = this.toolButtonList;
const toolsOptions = this.toolsOptions;
if (!toolsOptions) return toolsList;
if (this.showPreview) return this.previewTools;
return toolsList.filter(item => {
return isNotFalse(toolsOptions[item.name]);
});
......@@ -179,6 +188,11 @@ export default {
icon: "fullScreen",
tip: "全屏模式"
},
dirBtn: {
name: "dir",
icon: "dir",
tip: "目录"
},
toolButtonList: [
{
name: "call",
......@@ -298,6 +312,11 @@ export default {
"\n\n| 表头 | 表头 |\n| ------ | ------ |\n| 单元格 | 单元格 |\n| 单元格 | 单元格 |\n\n",
endStr: ""
},
// {
// name: "dir",
// icon: "dir",
// tip: "目录"
// },
{
name: "help",
icon: "help",
......@@ -408,7 +427,7 @@ export default {
this.updateText(newText, len);
if (startStr === "@") {
setTimeout(() => {
this.$parent.$refs["md_" + this.id].createSelectUserDialog('android');
this.$parent.$refs["md_" + this.id].createSelectUserDialog("android");
}, 200);
}
},
......
......@@ -4,8 +4,8 @@ import Vtip from "vtip";
import "vtip/lib/index.min.css";
import { initStyle, setzIndex, isNotEmpty } from "@/assets/js/utils";
import "@/assets/style/global.less";
import "@/assets/jupyter-render/dist/index.js";
import "@/assets/jupyter-render/dist/assets/index.css";
// import "@/assets/jupyter-render/dist/index.js";
// import "@/assets/jupyter-render/dist/assets/index.css";
Vue.use(Vtip.directive);
function initMdEditor(obj) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册