Merge 1ce93d98f8
into 7e84df37bd
This commit is contained in:
commit
149ebb90c8
26
App.vue
26
App.vue
@ -6,7 +6,7 @@
|
||||
@on-copy="onCopy"
|
||||
@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: {
|
||||
|
10
README.md
10
README.md
@ -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`样式。
|
||||
|
BIN
img/截图.jpg
Normal file
BIN
img/截图.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
81
src/assets/css/anchor.less
Normal file
81
src/assets/css/anchor.less
Normal 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;
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
@import './anchor.less';
|
||||
@margin: 8px 0;
|
||||
@line-height: 22px;
|
||||
|
||||
|
19
src/assets/js/marked/createToc.js
Normal file
19
src/assets/js/marked/createToc.js
Normal 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
|
||||
};
|
@ -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 () {
|
||||
|
155
src/components/Anchor/affix.vue
Normal file
155
src/components/Anchor/affix.vue
Normal 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>
|
81
src/components/Anchor/anchor.less
Normal file
81
src/components/Anchor/anchor.less
Normal 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;
|
||||
}
|
||||
}
|
207
src/components/Anchor/anchor.vue
Normal file
207
src/components/Anchor/anchor.vue
Normal 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>
|
62
src/components/Anchor/anchorLink.vue
Normal file
62
src/components/Anchor/anchorLink.vue
Normal 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>
|
36
src/components/Anchor/anchorLinks.vue
Normal file
36
src/components/Anchor/anchorLinks.vue
Normal 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>
|
92
src/components/Anchor/util.js
Normal file
92
src/components/Anchor/util.js
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
@ -193,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' : '']">
|
||||
|
@ -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);
|
||||
|
@ -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],
|
||||
|
Loading…
Reference in New Issue
Block a user