为vue-maarkdown添加固定目录

This commit is contained in:
zhaoxuhui 2019-12-13 18:28:20 +08:00 committed by 1520268401@qq.com
commit debadf7236
27 changed files with 867 additions and 56 deletions

30
App.vue
View File

@ -4,9 +4,9 @@
v-model="val"
@on-ready="onReady"
@on-copy="onCopy"
@on-paste-image="onPasteImage"
@on-upload-image="onUpladImage"
@on-save="onSave"
:height="500"
:height="fullHeight"
/>
</div>
</template>
@ -21,9 +21,31 @@
components: {
Markdown
},
mounted() {
const that = this
window.onresize = () => {
return (() => {
window.fullHeight = document.documentElement.clientHeight
that.fullHeight = window.fullHeight
})()
}
},
data: function () {
return {
val: ''
val: '',
fullHeight: document.documentElement.clientHeight
}
},
watch: {
fullHeight(val) {
if (!this.timer) {
this.fullHeight = val
this.timer = true
let that = this
setTimeout(function() {
that.timer = false
}, 400)
}
}
},
methods: {
@ -33,7 +55,7 @@
onCopy(text) {
console.log(text);
},
onPasteImage(file) {
onUpladImage(file) {
console.log(file)
},
onSave(data) {

View File

@ -342,7 +342,15 @@ export default index.setOptions({
预览区域和文档预览组件暂不支持自动生成目录,实现自动生成目录思路目前想到的大致有
- 重写`renderer.heading` 方法为生成的标题添加id输入特定快捷键如`[TOC]`时,查找预览区域内的的所有标题标签,分析等级关系,生成目录标签
属性 | 说明| 类型| 默认值
-|-|-|-
affix |固定模式| Boolean |true
offset-top |距离窗口顶部达到指定偏移量后触发 |Number| 0
offset-bottom |距离窗口底部达到指定偏移量后触发| Number| -
bounds| 锚点区域边界单位px| Number| 5
scroll-offset| 点击滚动的额外距离| Number| 0
container| 指定滚动的容器 |String | HTMLElement| -
show-ink| 是否显示小圆点| Boolean |false
### icon替换
项目内所有的icon和命名参考`/assets/font/index.html`替换时需注意预览区域的checkbox为icon注意一并替换
修改`/assets/css/index.less`内的`input[type="checkbox"]`的`:after`样式。

View File

@ -1,3 +1,8 @@
### 2.1.0
- onSave 事件内返回结果增加html字段
- 头部按钮增加本地图片上传,默认关闭
- 增加on-upload-image事件废除on-paste-image事件
### 2.0.0
- 新增专业版编辑器
- 新增预览组件

View File

@ -5,9 +5,13 @@
编辑器保存事件,自动保存或者手动保存时触发,支持`ctrl+s`或`command+s`触发保存,返回值类型为`Object`,包含当前输入值`value`和选择的代码块主题`theme`。
### on-paste-image
### on-paste-image [已废除,可改用on-upload-image]
监听编辑器粘贴图片事件,在编辑区域内手动粘贴图片时触发,可用于支持粘贴插入图片文件,返回`file`文件,上传文件后可结合`on-ready`事件内返回的`insertContent`插入图片。
### on-copy
复制代码块内容触发时返回当前代码块的textcopyCode开启时才有效。
### on-upload-image
图片上传事件,用户自定义上传图片,复制粘贴图片截图、文件和点开菜单栏上传按钮时式触发,返回`file`文件,上传文件后可结合`on-ready`事件内返回的`insertContent`插入图片。

View File

@ -47,6 +47,7 @@ ol|有序列表|是
code |代码块|是
link |链接|是
image|image|是
uploadImage|本地图片|否
table |表格|是
checked|已完成列表|是
notChecked |未完成列表|是

View File

@ -1,7 +1,7 @@
{
"name": "vue-meditor",
"description": "一款使用marked和highlight.js开发的一款markdown编辑器",
"version": "2.0.3",
"version": "2.1.0",
"author": "zhaoxuhui<1258835133@qq.com>",
"license": "MIT",
"private": false,

View File

@ -0,0 +1,81 @@
// Anchor
@primary-color : #2d8cf0;
@anchor-border-width: 2px;
@border-color-split : #e8eaec; // inside
@body-background : #fff;
@transition-time : .2s;
@text-color : #515a6e;
.anchor{
&-wrapper{
overflow: auto;
padding-left: 4px;
margin-left: -4px;
}
&{
position: relative;
padding-left: @anchor-border-width;
&-ink {
position: absolute;
height: 100%;
left: 0;
top: 0;
&:before {
content: ' ';
position: relative;
width: @anchor-border-width;
height: 100%;
display: block;
background-color: @border-color-split;
margin: 0 auto;
}
&-ball {
display: inline-block;
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid @primary-color;
background-color: @body-background;
left: 50%;
transition: top @transition-time ease-in-out;
transform: translate(-50%, 2px);
}
}
&.fixed &-ink &-ink-ball {
display: none;
}
}
&-link {
padding: 8px 0 6px 16px !important;
line-height: 1;
&-title {
font-size: 12px;
text-decoration:none;
display: block;
position: relative;
transition: all .3s;
color: @text-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
&:only-child {
margin-bottom: 0;
}
}
&-active > &-title {
color: @primary-color;
}
}
&-link &-link {
padding-top: 6px;
padding-bottom: 6px;
}
}

View File

@ -1,3 +1,4 @@
@import './anchor.less';
@margin: 8px 0;
@line-height: 22px;

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -20,9 +20,6 @@ Created by iconfont
/>
<missing-glyph />
<glyph glyph-name="fujian" unicode="&#58907;" d="M874.7 722.4q32.4-32.4 48.15-72.45t15.75-82.35-15.75-82.35-48.15-72.45l-344.7-342q-32.4-32.4-79.65-54t-99.9-25.2-107.55 14.4-101.7 64.8q-45.9 45.9-63.9 99.45t-14.4 106.65 24.75 99.9 53.55 79.2l306 303.3q8.1 8.1 23.85 4.05t23.85-12.15q7.2-8.1 11.7-23.85t-3.6-23.85l-305.1-302.4q-24.3-24.3-40.5-56.7t-18.9-69.3 10.8-74.7 48.6-72.9q31.5-31.5 69.75-44.55t76.05-10.8 72 16.65 58.5 38.7l343.8 341.1q24.3 24.3 34.65 50.4t9.9 51.75-12.15 49.95-32.4 45q-38.7 37.8-86.85 35.55t-96.75-50.85l-310.5-308.7q-20.7-20.7-20.25-45.9t16.65-41.4q19.8-19.8 45.45-16.2t41.85 19.8l281.7 279.9q8.1 8.1 23.85 4.05t23.85-12.15 12.6-23.85-3.6-23.85l-282.6-279.9q-32.4-32.4-62.1-45t-55.8-11.25-48.6 13.95-41.4 30.6q-15.3 14.4-27.45 36.9t-13.95 49.5 9.45 57.6 43.65 62.1q16.2 16.2 28.8 29.7 10.8 11.7 20.25 20.7t10.35 9.9l251.1 250.2q32.4 32.4 70.65 50.85t77.85 21.15 77.85-10.8 70.65-45.9z" horiz-adv-x="1024" />
<glyph glyph-name="md" unicode="&#58884;" d="M471.289899 141.73866699999996c-6.827051 16.384-16.043051 37.205333-27.648 62.464-10.923008 25.258667-22.869683 52.565333-35.839991 81.92-12.970995 29.354667-26.965649 59.392-41.984 90.112-14.336286 31.402667-27.989606 60.757333-40.96 88.064-12.970914 27.989333-25.258901 52.906667-36.864 74.752-10.92288 21.841067-19.797538 38.5664-26.624 50.176-7.509521-80.554667-13.653517-167.936-18.432-262.144-4.778842-93.525333-8.874837-188.074667-12.288-283.648l-97.28 0c2.730581 61.44 5.80259 123.221333 9.216 185.344 3.413248 62.805333 7.167915 124.245333 11.264 184.32 4.778573 60.757333 9.557235 119.808 14.336 177.152 5.461222 57.339733 11.263885 111.611733 17.408 162.816l87.04 0c18.431774-30.0416 38.229086-65.540267 59.392-106.496 21.162377-40.964267 42.325022-83.972267 63.488-129.024 21.162313-44.373333 41.642274-89.088 61.439991-134.144 19.796949-44.373333 37.887573-84.992 54.272-121.856 16.383531 36.864 34.474197 77.482667 54.272 121.856 19.796821 45.056 40.276821 89.770667 61.44 134.144 21.162112 45.051733 42.324736 88.059733 63.488 129.024 21.162027 40.955733 40.95936 76.4544 59.392 106.496l87.04 0c22.126421-218.018133 38.979968-435.549867 50.560683-667.84 0.56704-11.370667 44.931072 2.005333 45.472853-9.412267l-82.925739-85.713067-109.592363 85.713067c-0.523093 14.647467 50.366165-3.114667 49.810987 11.485867-3.06816 80.661333-6.62464 160.5888-10.670421 239.7824-4.779349 94.208-10.923349 181.589333-18.432 262.144-6.827349-11.6096-16.043349-28.334933-27.648-50.176-10.923307-21.845333-22.869973-46.762667-35.84-74.752-12.971264-27.306667-26.965931-56.661333-41.984-88.064-14.336555-30.72-27.989888-60.757333-40.96-90.112-12.971179-29.354667-25.259179-56.661333-36.864-81.92-10.923179-25.258667-19.797803-46.08-26.624-62.464l-79.872 0" horiz-adv-x="1024" />
@ -62,9 +59,6 @@ Created by iconfont
<glyph glyph-name="under-line" unicode="&#58970;" d="M166.12017822 73.63925171000005h684.40402222v-57.03552246H166.12017822v57.03552246z m0 684.40237427h294.74972535v-44.84481812l-63.66192628-4.48406982-14.53656005-13.00396729v-321.53302002c0-61.28723145 13.0328064-105.0856018 39.09924316-131.39511108 26.06643677-26.3053894 68.83895874-39.46426391 128.32580567-39.46426392 54.80667114 0 94.48928833 13.97872925 119.05197143 41.93206787 24.5626831 27.95745849 36.84402466 73.7663269 36.84402466 137.4472046V692.12118531l-15.54016114 15.69424438-65.66665649 5.38055419V758.0416259799999h233.09335328v-44.84481812l-62.66326905-5.38055419-14.03640747-15.69424439v-318.84439087c0-85.20309448-21.38626099-146.93939209-64.16290283-185.20394898-42.77252198-38.26950073-111.78451538-57.40219117-207.02114869-57.40219115-49.79690552 0-93.48980713 6.50527954-131.08364868 19.50759887-37.5954895 13.00643921-67.0880127 31.61178589-88.47592163 55.83251953-16.37402344 19.13269043-27.98794555 41.03393555-34.83764649 65.69549561-6.85134888 24.66567993-10.27661133 60.01831055-10.27661132 106.05789184V695.70794677l-14.53656006 13.00396729-64.66305542 4.48406982V758.0416259799999z" horiz-adv-x="1024" />
<glyph glyph-name="checked1" unicode="&#60064;" d="M810.666667 768H213.333333c-47.146667 0-85.333333-38.186667-85.333333-85.333333v-597.333334c0-47.146667 38.186667-85.333333 85.333333-85.333333h597.333334c47.146667 0 85.333333 38.186667 85.333333 85.333333V682.666667c0 47.146667-38.186667 85.333333-85.333333 85.333333zM426.666667 170.66666699999996L213.333333 384l60.373334 60.373333L426.666667 291.41333299999997l323.626666 323.626667L810.666667 554.666667 426.666667 170.66666699999996z" horiz-adv-x="1024" />
<glyph glyph-name="close" unicode="&#59024;" d="M869.16373333 77.84106667000003l-303.82826666 303.82826666 303.82826666 303.95733334c14.62826667 14.62826667 14.62826667 38.448 0 53.07626666s-38.448 14.62826667-53.07626666 0l-303.82826667-303.82826666-303.95733333 303.82826666c-14.62826667 14.62826667-38.448 14.62826667-53.07626667 0s-14.62826667-38.448 0-53.07626666l303.95733333-303.95733334-303.95733333-303.82826666c-14.62826667-14.62826667-14.62826667-38.448 0-53.07626667s38.448-14.62826667 53.07626667 0l303.95733333 303.95733333 303.95733333-303.95733333c14.62826667-14.62826667 38.448-14.62826667 53.07626667 0 14.4992 14.62826667 14.4992 38.448-0.13013333 53.07626667z" horiz-adv-x="1024" />
@ -110,9 +104,15 @@ Created by iconfont
<glyph glyph-name="ol" unicode="&#59120;" d="M349.29890773 763.63588267h542.33697494a54.23369707 54.23369707 0 0 0 0-108.4673952h-542.33697494a54.23369707 54.23369707 0 0 0 0 108.4673952z m0-325.4021856h542.33697494a54.23369707 54.23369707 0 0 0 0-108.46739414h-542.33697494a54.23369707 54.23369707 0 0 0 0 108.46739414z m0-325.40218454h542.33697494a54.23369707 54.23369707 0 0 0 0-108.4673952h-542.33697494a54.23369707 54.23369707 0 0 0 0 108.4673952zM97.1122144 760.3818602700001v25.48983786a167.03978773 167.03978773 0 0 1 24.9475008 0 35.25190293 35.25190293 0 0 1 18.43945707 10.30440214 33.08255573 33.08255573 0 0 1 7.59271786 13.55842453 31.45554453 31.45554453 0 0 1 0 7.59271787H182.801456v-189.27560427h-40.67527253V760.3818602700001z m0-420.3111552a108.4673952 108.4673952 0 0 0 37.96358827 40.6752736 279.8458784 279.8458784 0 0 1 33.6248928 26.57451093 39.59059947 39.59059947 0 0 1 11.93141333 27.65918613 30.3708704 30.3708704 0 0 1-6.50804373 20.06646827 23.86282667 23.86282667 0 0 1-19.5241312 8.13505387 23.3204896 23.3204896 0 0 1-26.03217494-12.4737504 63.45342613 63.45342613 0 0 1-4.33869546-23.3204896h-35.251904a92.7396224 92.7396224 0 0 0 8.677392 39.59059946 54.23369707 54.23369707 0 0 0 54.23369706 28.7438592 67.7921216 67.7921216 0 0 0 48.26799147-16.81244586 58.5723936 58.5723936 0 0 0 17.89712-44.471632 60.7417408 60.7417408 0 0 0-12.4737504-37.96358827 126.90685227 126.90685227 0 0 0-27.65918613-24.40516373l-15.18543467-10.8467392-19.5241312-14.64309867a42.8446208 42.8446208 0 0 1-8.677392-10.30440213h83.5198944v-33.08255574H86.80781227a90.02793813 90.02793813 0 0 0 8.67739093 37.4212512z m27.11684907-304.2510432a43.38695787 43.38695787 0 0 1 3.7963584-18.98179414 25.48983787 25.48983787 0 0 1 24.9475008-14.10076053 27.65918613 27.65918613 0 0 1 17.35478293 5.4233696 28.2015232 28.2015232 0 0 1 8.13505493 21.6934784 24.9475008 24.9475008 0 0 1-15.18543466 25.48983787 78.0965248 78.0965248 0 0 1-27.11684907 7.0503808v27.11684906a71.58848107 71.58848107 0 0 1 25.48983787 3.7963584 22.23581547 22.23581547 0 0 1 12.4737504 23.32049067 24.9475008 24.9475008 0 0 1-6.50804374 17.89712 23.3204896 23.3204896 0 0 1-17.89712 7.0503808 22.77815253 22.77815253 0 0 1-19.5241312-8.677392 35.79424 35.79424 0 0 1-5.96570666-22.77815253h-35.251904a94.90897067 94.90897067 0 0 0 4.8810336 27.116848 61.28407787 61.28407787 0 0 0 14.6430976 20.60880533 50.97967573 50.97967573 0 0 0 17.354784 10.30440213 76.4695136 76.4695136 0 0 0 24.9475008 2.71168534 66.70744747 66.70744747 0 0 0 44.47163093-14.1007616 47.18331627 47.18331627 0 0 0 16.81244693-37.96358827 42.30228373 42.30228373 0 0 0-10.3044032-28.7438592 34.16722987 34.16722987 0 0 0-13.01608746-9.76206613 27.11684907 27.11684907 0 0 0 14.64309866-8.67739094 47.18331627 47.18331627 0 0 0 14.64309867-36.33657706 61.28407787 61.28407787 0 0 0-16.81244587-42.84462187 64.53810027 64.53810027 0 0 0-49.89500266-18.43945707 60.7417408 60.7417408 0 0 0-54.23369707 26.574512 79.18119787 79.18119787 0 0 0-9.219728 36.87891414z" horiz-adv-x="1024" />
<glyph glyph-name="upload-img" unicode="&#59001;" d="M885.94295467 791.41546667h-748.96725334C102.64029867 791.41546667 74.5472 762.93461333 74.5472 728.113152V95.09546666999995c0-34.816 28.09309867-63.30231467 62.42304-63.30231467h530.51938133V95.09546666999995h-499.318784c-18.726912 0-31.20059733 15.826944-31.20059733 31.653888V696.46472533c0 15.82148267 12.47368533 31.64842667 31.20059733 31.64842667h686.56605867c18.73237333 0 31.20605867-15.826944 31.20605867-31.64842667v-411.46231466H948.36053333v443.11074133c0 34.82146133-28.082176 63.30231467-62.41757866 63.30231467zM819.02523733 296.52582399999994c-6.23684267 6.30237867-12.46276267 9.44810667-21.81256533 9.44810667-9.34434133 0-15.58664533-3.145728-21.81256533-9.44810667l-87.25572267-88.15684267c-12.468224-12.599296-12.468224-31.490048 0-44.089344 12.46276267-12.58837333 31.162368-12.58837333 43.62513067 0l34.28625066 34.64123734v-195.22082134C766.05576533-12.039509329999987 778.51306667-27.784533330000045 797.212672-27.784533330000045c18.694144 0 31.15690667 12.59383467 31.15690667 31.48458666v195.22082134l37.39921066-37.781504c12.46276267-12.59383467 31.162368-12.59383467 43.630592 0 12.46276267 12.58837333 12.46276267 31.490048 0 44.07842133l-90.374144 91.308032z m-408.223744 236.158976c15.57572267 34.63031467 6.23684267 75.56846933-18.69960533 103.90186667-28.04394667 28.33885867-71.67453867 34.63031467-105.94440533 22.04194133-34.291712-15.75594667-56.098816-53.53745067-56.098816-91.31895467 0-50.37533867 40.50670933-91.308032 90.36868266-91.308032 37.404672 0 74.79842133 22.03648 90.374144 56.672256v0.01092267zM292.388864 567.31511467c0 9.44810667 3.10749867 15.745024 9.34980267 22.04194133 3.10749867 9.44264533 12.45730133 12.59383467 21.80164266 12.59383467 15.58664533 0 31.17329067-12.59383467 31.17329067-28.33885867 0-15.73956267-12.47368533-31.48458667-28.049408-34.63031467-18.70506667 0-34.28078933 12.58837333-34.28078933 28.33339734h0.00546133z m-104.333312-384.62532267c12.54468267-12.53376 31.36989867-12.53376 43.91458133 0l131.72736 131.57444267 109.7728-109.64718934c3.14026667-3.129344 3.14026667-3.129344 6.275072-3.129344 12.550144-6.26961067 25.08936533-6.26961067 37.63950934 3.129344L827.88352 514.74432c12.54468267 12.53376 12.54468267 31.326208 0 43.86542933-12.55560533 12.52829867-31.37536 12.52829867-43.91458133 0l-288.54408534-288.21640533L385.65751467 383.17533867l-3.13480534 3.13480533c-12.550144 9.39349333-28.229632 9.39349333-40.779776-3.13480533L184.93166933 226.53883732999998c-9.404416-12.53376-9.404416-31.33166933 3.13480534-43.859968l-0.01092267 0.01092267z m0 0" horiz-adv-x="1024" />
<glyph glyph-name="save" unicode="&#58952;" d="M788.69067383 752.15185547h-26.03759766v0.65917969H261.42932128v-0.65917969h-26.03759764c-50.92163086 0-92.20275879-41.19873047-92.2027588-92.12036133v-552.72216797c0-50.92163086 41.28112793-92.12036133 92.2027588-92.12036133h553.29895019c50.92163086 0 92.20275879 41.28112793 92.20275879 92.12036133V660.03149414c0 50.8392334-41.36352539 92.12036133-92.20275879 92.12036133z m-467.93518067-58.58459473H703.16210938v-118.48754882c0-21.83532715-19.03381348-39.55078125-42.51708985-39.55078125H363.272583c-23.48327637 0-42.51708984 17.71545411-42.51708983 39.55078125V693.5672607500001z m501.47094727-575.46386719c0-24.38964844-19.85778809-44.24743653-44.32983399-44.24743652H246.02099609c-24.4720459 0-44.32983398 19.77539063-44.32983398 44.24743652V649.31982422c0 24.38964844 19.85778809 44.24743653 44.32983398 44.24743653h15.32592774V568.5703125c0-50.92163086 40.78674317-92.12036133 91.1315918-92.12036133h318.96057129c50.34484864 0 91.1315918 41.28112793 91.13159179 92.12036133V693.5672607500001h15.24353027c24.4720459 0 44.32983398-19.77539063 44.32983399-44.24743653v-531.21643067zM607.58105469 568.2407226600001c16.31469727 0 29.66308594 13.34838867 29.66308593 29.66308593v32.95898438c0 16.31469727-13.34838867 29.66308594-29.66308593 29.66308594s-29.66308594-13.34838867-29.66308594-29.66308594v-32.95898438c0-16.31469727 13.34838867-29.66308594 29.66308594-29.66308593z" horiz-adv-x="1024" />
<glyph glyph-name="check-box" unicode="&#60504;" d="M810.666667 768H213.333333c-47.146667 0-85.333333-38.186667-85.333333-85.333333v-597.333334c0-47.146667 38.186667-85.333333 85.333333-85.333333h597.333334c47.146667 0 85.333333 38.186667 85.333333 85.333333V682.666667c0 47.146667-38.186667 85.333333-85.333333 85.333333zM426.666667 170.66666699999996L213.333333 384l60.373334 60.373333L426.666667 291.41333299999997l323.626666 323.626667L810.666667 554.666667 426.666667 170.66666699999996z" horiz-adv-x="1024" />
</font>

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,19 @@
export default {
add(text, level) {
const anchor = `toc${level}${++this.index}`;
const item = { anchor, level, text };
const items = this.tocItems;
if (item.level <= 3) {
items.push(item);
}
return anchor;
},
reset: function() {
this.tocItems = [];
this.index = 0;
},
tocItems: [],
index: 0
};

View File

@ -1,3 +1,4 @@
import tocObj from './createToc';
var block = {
newline: /^\n+/,
code: /^( {4}[^\n]+\n*)+/,
@ -947,20 +948,22 @@ Renderer.prototype.html = function (html) {
};
Renderer.prototype.heading = function (text, level, raw, slugger) {
if (this.options.headerIds) {
return '<h'
+ level
+ ' id="'
+ this.options.headerPrefix
+ slugger.slug(raw)
+ '">'
+ text
+ '</h'
+ level
+ '>\n';
}
// ignore IDs
return '<h' + level + '>' + text + '</h' + level + '>\n';
// if (this.options.headerIds) {
// return '<h'
// + level
// + ' id="'
// + this.options.headerPrefix
// + slugger.slug(raw)
// + '">'
// + text
// + '</h'
// + level
// + '>\n';
// }
// // ignore IDs
// return '<h' + level + '>' + text + '</h' + level + '>\n';
let anchor = tocObj.add(text, level);
return `<a id="${anchor}" href="#${anchor}" class="anchor-fix"><h${level}>${text}</h${level}></a>\n`;
};
Renderer.prototype.hr = function () {

View File

@ -0,0 +1,155 @@
<template>
<div>
<div ref="point" :class="classes" :style="styles">
<slot></slot>
</div>
<div v-show="slot" :style="slotStyle"></div>
</div>
</template>
<script>
import { on, off } from '../Anchor/util';
const prefixCls = 'affix';
function getScroll(target, top) {
const prop = top ? 'pageYOffset' : 'pageXOffset';
const method = top ? 'scrollTop' : 'scrollLeft';
let ret = target[prop];
if (typeof ret !== 'number') {
ret = window.document.documentElement[method];
}
return ret;
}
function getOffset(element) {
const rect = element.getBoundingClientRect();
const scrollTop = getScroll(window, true);
const scrollLeft = getScroll(window);
const docEl = window.document.body;
const clientTop = docEl.clientTop || 0;
const clientLeft = docEl.clientLeft || 0;
return {
top: rect.top + scrollTop - clientTop,
left: rect.left + scrollLeft - clientLeft
};
}
export default {
name: 'Affix',
props: {
offsetTop: {
type: Number,
default: 0
},
offsetBottom: {
type: Number
},
useCapture: {
type: Boolean,
default: false
}
},
data () {
return {
affix: false,
styles: {},
slot: false,
slotStyle: {}
};
},
computed: {
offsetType () {
let type = 'top';
if (this.offsetBottom >= 0) {
type = 'bottom';
}
return type;
},
classes () {
return [
{
[`${prefixCls}`]: this.affix
}
];
}
},
mounted () {
// window.addEventListener('scroll', this.handleScroll, false);
// window.addEventListener('resize', this.handleScroll, false);
on(window, 'scroll', this.handleScroll, this.useCapture);
on(window, 'resize', this.handleScroll, this.useCapture);
this.$nextTick(() => {
this.handleScroll();
});
},
beforeDestroy () {
// window.removeEventListener('scroll', this.handleScroll, false);
// window.removeEventListener('resize', this.handleScroll, false);
off(window, 'scroll', this.handleScroll, this.useCapture);
off(window, 'resize', this.handleScroll, this.useCapture);
},
methods: {
handleScroll () {
const affix = this.affix;
const scrollTop = getScroll(window, true);
const elOffset = getOffset(this.$el);
const windowHeight = window.innerHeight;
const elHeight = this.$el.getElementsByTagName('div')[0].offsetHeight;
// Fixed Top
if ((elOffset.top - this.offsetTop) < scrollTop && this.offsetType == 'top' && !affix) {
this.affix = true;
this.slotStyle = {
width: this.$refs.point.clientWidth + 'px',
height: this.$refs.point.clientHeight + 'px'
};
this.slot = true;
this.styles = {
top: `${this.offsetTop}px`,
left: `${elOffset.left}px`,
width: `${this.$el.offsetWidth}px`
};
this.$emit('on-change', true);
} else if ((elOffset.top - this.offsetTop) > scrollTop && this.offsetType == 'top' && affix) {
this.slot = false;
this.slotStyle = {};
this.affix = false;
this.styles = null;
this.$emit('on-change', false);
}
// Fixed Bottom
if ((elOffset.top + this.offsetBottom + elHeight) > (scrollTop + windowHeight) && this.offsetType == 'bottom' && !affix) {
this.affix = true;
this.styles = {
bottom: `${this.offsetBottom}px`,
left: `${elOffset.left}px`,
width: `${this.$el.offsetWidth}px`
};
this.$emit('on-change', true);
} else if ((elOffset.top + this.offsetBottom + elHeight) < (scrollTop + windowHeight) && this.offsetType == 'bottom' && affix) {
this.affix = false;
this.styles = null;
this.$emit('on-change', false);
}
}
}
};
</script>
<style lang="less">
.affix {
position: fixed;
z-index: 10;
}
</style>

View File

@ -0,0 +1,81 @@
// Anchor
@primary-color : #2d8cf0;
@anchor-border-width: 2px;
@border-color-split : #e8eaec; // inside
@body-background : #fff;
@transition-time : .2s;
@text-color : #515a6e;
.anchor{
&-wrapper{
overflow: auto;
padding-left: 4px;
margin-left: -4px;
}
&{
position: relative;
padding-left: @anchor-border-width;
&-ink {
position: absolute;
height: 100%;
left: 0;
top: 0;
&:before {
content: ' ';
position: relative;
width: @anchor-border-width;
height: 100%;
display: block;
background-color: @border-color-split;
margin: 0 auto;
}
&-ball {
display: inline-block;
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid @primary-color;
background-color: @body-background;
left: 50%;
transition: top @transition-time ease-in-out;
transform: translate(-50%, 2px);
}
}
&.fixed &-ink &-ink-ball {
display: none;
}
}
&-link {
padding: 8px 0 6px 16px;
line-height: 1;
&-title {
font-size: 12px;
text-decoration:none;
display: block;
position: relative;
transition: all .3s;
color: @text-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
&:only-child {
margin-bottom: 0;
}
}
&-active > &-title {
color: @primary-color;
}
}
&-link &-link {
padding-top: 6px;
padding-bottom: 6px;
}
}

View File

@ -0,0 +1,207 @@
<template>
<component :is="wrapperComponent" :offset-top="offsetTop" :offset-bottom="offsetBottom" @on-change="handleAffixStateChange">
<div :class="`${prefix}-wrapper`" :style="wrapperStyle">
<div :class="`${prefix}`">
<div :class="`${prefix}-ink`">
<span v-show="showInk" :class="`${prefix}-ink-ball`" :style="{top: `${inkTop}px`}"></span>
</div>
<slot></slot>
</div>
</div>
</component>
</template>
<script>
import { scrollTop, findComponentsDownward, sharpMatcherRegx } from './util';
import Affix from './affix';
import { on, off } from './util';
export default {
name: 'Anchor',
components:{Affix},
provide () {
return {
anchorCom: this
};
},
data () {
return {
prefix: 'anchor',
isAffixed: false, // current affixed state
inkTop: 0,
animating: false, // if is scrolling now
currentLink: '', // current show link => #href -> currentLink = #href
currentId: '', // current show title id => #href -> currentId = href
scrollContainer: null,
scrollElement: null,
titlesOffsetArr: [],
wrapperTop: 0,
upperFirstTitle: true
};
},
props: {
affix: {
type: Boolean,
default: true
},
offsetTop: {
type: Number,
default: 0
},
offsetBottom: Number,
bounds: {
type: Number,
default: 5
},
// container: [String, HTMLElement], // HTMLElement SSR
container: null,
showInk: {
type: Boolean,
default: false
},
scrollOffset: {
type: Number,
default: 0
}
},
computed: {
wrapperComponent () {
return this.affix ? 'Affix' : 'div';
},
wrapperStyle () {
return {
maxHeight: this.offsetTop ? `calc(100vh - ${this.offsetTop}px)` : '100vh'
};
},
containerIsWindow () {
return this.scrollContainer === window;
}
},
methods: {
handleAffixStateChange (state) {
this.isAffixed = this.affix && state;
},
handleScroll (e) {
console.log(this.titlesOffsetArr,'this.titlesOffsetArr[0]');
this.upperFirstTitle = e.target.scrollTop < this.titlesOffsetArr[0].offset;
if (this.animating) return;
this.updateTitleOffset();
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || e.target.scrollTop;
this.getCurrentScrollAtTitleId(scrollTop);
},
handleHashChange () {
const url = window.location.href;
const sharpLinkMatch = sharpMatcherRegx.exec(url);
if (!sharpLinkMatch) return;
this.currentLink = sharpLinkMatch[0];
this.currentId = sharpLinkMatch[1];
},
handleScrollTo () {
const anchor = document.getElementById(this.currentId);
const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`);
let offset = this.scrollOffset;
if (currentLinkElementA) {
offset = parseFloat(currentLinkElementA.getAttribute('data-scroll-offset'));
}
if (!anchor) return;
const offsetTop = anchor.offsetTop - this.wrapperTop - offset;
this.animating = true;
scrollTop(this.scrollContainer, this.scrollElement.scrollTop, offsetTop, 600, () => {
this.animating = false;
});
this.handleSetInkTop();
},
handleSetInkTop () {
const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`);
if (!currentLinkElementA) return;
const elementATop = currentLinkElementA.offsetTop;
const top = (elementATop < 0 ? this.offsetTop : elementATop);
this.inkTop = top;
},
updateTitleOffset () {
const links = findComponentsDownward(this, 'AnchorLink').map(link => {
return link.href;
});
const idArr = links.map(link => {
console.log(link,'link');
return link.split('#')[1];
});
let offsetArr = [];
idArr.forEach(id => {
const titleEle = document.getElementById(id);
if (titleEle) offsetArr.push({
link: `#${id}`,
offset: titleEle.offsetTop - this.scrollElement.offsetTop
});
});
this.titlesOffsetArr = offsetArr;
},
getCurrentScrollAtTitleId (scrollTop) {
let i = -1;
let len = this.titlesOffsetArr.length;
let titleItem = {
link: '#',
offset: 0
};
scrollTop += this.bounds;
while (++i < len) {
let currentEle = this.titlesOffsetArr[i];
let nextEle = this.titlesOffsetArr[i + 1];
if (scrollTop >= currentEle.offset && scrollTop < ((nextEle && nextEle.offset) || Infinity)) {
titleItem = this.titlesOffsetArr[i];
break;
}
}
this.currentLink = titleItem.link;
this.handleSetInkTop();
},
getContainer () {
this.scrollContainer = this.container ? (typeof this.container === 'string' ? document.querySelector(this.container) : this.container) : window;
this.scrollElement = this.container ? this.scrollContainer : (document.documentElement || document.body);
},
removeListener () {
off(this.scrollContainer, 'scroll', this.handleScroll);
off(window, 'hashchange', this.handleHashChange);
},
init () {
// const anchorLink = findComponentDownward(this, 'AnchorLink');
this.handleHashChange();
this.$nextTick(() => {
this.removeListener();
this.getContainer();
this.wrapperTop = this.containerIsWindow ? 0 : this.scrollElement.offsetTop;
this.handleScrollTo();
this.handleSetInkTop();
this.updateTitleOffset();
if (this.titlesOffsetArr[0]) {
this.upperFirstTitle = this.scrollElement.scrollTop < this.titlesOffsetArr[0].offset;
}
on(this.scrollContainer, 'scroll', this.handleScroll);
on(window, 'hashchange', this.handleHashChange);
});
}
},
watch: {
'$route' () {
this.handleHashChange();
this.$nextTick(() => {
this.handleScrollTo();
});
},
container () {
this.init();
},
currentLink (newHref, oldHref) {
this.$emit('on-change', newHref, oldHref);
}
},
mounted () {
this.init();
},
beforeDestroy () {
this.removeListener();
}
};
</script>
<style lang="less" scoped>
@import "./anchor.less";
</style>

View File

@ -0,0 +1,62 @@
<template>
<div :class="anchorLinkClasses">
<a :class="linkTitleClasses" :href="href" :data-scroll-offset="scrollOffset" :data-href="href" @click.prevent="goAnchor" :title="title">{{ title }}</a>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'AnchorLink',
inject: ['anchorCom'],
props: {
href: String,
title: String,
scrollOffset: {
type: Number,
default () {
return this.anchorCom.scrollOffset;
}
}
},
data () {
return {
prefix: 'anchor-link'
};
},
computed: {
anchorLinkClasses () {
return [
this.prefix,
this.anchorCom.currentLink === this.href ? `${this.prefix}-active` : ''
];
},
linkTitleClasses () {
return [
`${this.prefix}-title`
];
}
},
methods: {
goAnchor () {
this.currentLink = this.href;
this.anchorCom.handleHashChange();
this.anchorCom.handleScrollTo();
this.anchorCom.$emit('on-select', this.href);
const isRoute = this.$router;
if (isRoute) {
this.$router.push(this.href, () => {});
} else {
window.location.href = this.href;
}
}
},
mounted () {
this.$nextTick(() => {
this.anchorCom.init();
});
}
};
</script>
<style lang="less" scoped>
@import "./anchor.less";
</style>

View File

@ -0,0 +1,36 @@
<template>
<div>
<!-- 递归 render -->
<template v-for="child in item">
<AnchorLink :href="`#${child.anchor}`" :title="child.text" :key="child.anchor">
<template v-if="child.children">
<AnchorLinks :item="child.children"></AnchorLinks>
</template>
</AnchorLink>
</template>
</div>
</template>
</div>
</template>
<script>
import AnchorLink from './anchorLink'
export default {
mounted() {
console.log(this.item, 'links')
},
props: {
item: {
type: Array,
default: []
}
},
name: 'AnchorLinks',
components: { AnchorLink }
}
</script>
<style lang="less" scoped>
@import './anchor.less';
</style>

View File

@ -0,0 +1,92 @@
import Vue from "vue";
const isServer = Vue.prototype.$isServer;
// Find components downward
export function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child);
const foundChilds = findComponentsDownward(child, componentName);
return components.concat(foundChilds);
}, []);
}
// scrollTop animation
export function scrollTop(el, from = 0, to, duration = 500, endCallback) {
if (!window.requestAnimationFrame) {
window.requestAnimationFrame =
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return window.setTimeout(callback, 1000 / 60);
};
}
const difference = Math.abs(from - to);
const step = Math.ceil((difference / duration) * 50);
function scroll(start, end, step) {
if (start === end) {
endCallback && endCallback();
return;
}
let d = start + step > end ? end : start + step;
if (start > end) {
d = start - step < end ? end : start - step;
}
if (el === window) {
window.scrollTo(d, d);
} else {
el.scrollTop = d;
}
window.requestAnimationFrame(() => scroll(d, end, step));
}
scroll(from, to, step);
}
export const sharpMatcherRegx = /#([^#]+)$/;
export const dimensionMap = {
xs: "480px",
sm: "576px",
md: "768px",
lg: "992px",
xl: "1200px",
xxl: "1600px"
};
/* istanbul ignore next */
export const on = (function() {
if (!isServer && document.addEventListener) {
return function(element, event, handler, useCapture = false) {
if (element && event && handler) {
element.addEventListener(event, handler, useCapture);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent("on" + event, handler);
}
};
}
})();
/* istanbul ignore next */
export const off = (function() {
if (!isServer && document.removeEventListener) {
return function(element, event, handler, useCapture = false) {
if (element && event) {
element.removeEventListener(event, handler, useCapture);
}
};
} else {
return function(element, event, handler) {
if (element && event) {
element.detachEvent("on" + event, handler);
}
};
}
})();

View File

@ -87,6 +87,9 @@
<li v-if="tools.image" name="图片">
<span @click="insertImage" class="iconfont icon-img"></span>
</li>
<li v-if="tools.uploadImage" name="本地图片">
<span @click="chooseImage" class="iconfont icon-upload-img"></span>
</li>
<li v-if="tools.table" name="表格">
<span @click="insertTable" class="iconfont icon-table"></span>
</li>
@ -190,6 +193,12 @@
>
<div v-html="html" ref="previewInner"></div>
</div>
<!-- 目录 -->
<Anchor :offset-top="40" style="margin-left:20px;width:20%">
<template v-for="item in toc">
<AnchorLink :href="`#${item.anchor}`" :title="item.text" :key="item.anchor" />
</template>
</Anchor>
</div>
<!-- 预览图片-->
<div :class="['preview-img', previewImgModal ? 'active' : '']">

View File

@ -7,7 +7,7 @@ import codemirrorConfig from '../../assets/js/codemirror/config';
import '../../assets/js/codemirror/styles/codemirror.css';
import common from '../../mixins/common';
import marked from '../../config/marked';
import tocObj from "../../assets/js/marked/createToc";
export default {
name: 'markdown-pro',
mixins: [common],
@ -345,6 +345,10 @@ export default {
html = html.replace(/<pre>/g, '<div class="code-block"><span class="copy-code">' + this.copyBtnText + '</span><pre>').replace(/<\/pre>/g, '</pre></div>')
}
this.html = html;
//toc
this.toc = tocObj.tocItems;
tocObj.reset()
this.addImageClickListener();
this.addCopyListener();
this.$emit('input', currentValue);

View File

@ -85,6 +85,9 @@
<li v-if="tools.image" name="图片">
<span @click="insertImage" class="iconfont icon-img"></span>
</li>
<li v-if="tools.uploadImage" name="本地图片">
<span @click="chooseImage" class="iconfont icon-upload-img"></span>
</li>
<li v-if="tools.table" name="表格">
<span @click="insertTable" class="iconfont icon-table"></span>
</li>

View File

@ -18,6 +18,7 @@ export default {
code: true,
link: true,
image: true,
uploadImage:false,
table: true,
checked: true,
notChecked: true,

View File

@ -1,8 +1,11 @@
import {saveFile} from '../utils';
import defaultTools from '../config/tools';
import AnchorLink from '../components/Anchor/anchorLink'
import Anchor from '../components/Anchor/anchor'
export default {
name: 'markdown',
components: {Anchor,AnchorLink},
props: {
value: {
type: [String, Number],
@ -75,6 +78,7 @@ export default {
timerId: null, // 定时器id
themeName: '',
themeSlideDown: false,
imgSlideDown: false,
imgs: [],
scrolling: true, // 同步滚动
editorScrollHeight: 0,
@ -160,7 +164,7 @@ export default {
if (item) {
const file = item.getAsFile();
if (/image/gi.test(file.type)) {
this.$emit('on-paste-image', file);
this.$emit('on-upload-image', file);
e.preventDefault();
}
}
@ -200,6 +204,19 @@ export default {
this.previewImgModal = true;
};
},
chooseImage() {// 选择图片
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = ()=>{
const files = input.files;
if(files[0]){
this.$emit('on-upload-image', files[0]);
input.value = '';
}
}
input.click();
},
addCopyListener() {// 监听复制操作
setTimeout(() => {
const btns = document.querySelectorAll(