This commit is contained in:
tony 2020-02-27 12:51:23 +08:00 committed by GitHub
commit 149ebb90c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 800 additions and 20 deletions

30
App.vue
View File

@ -6,7 +6,7 @@
@on-copy="onCopy"
@on-upload-image="onUpladImage"
@on-save="onSave"
:height="500"
:height="fullHeight"
/>
</div>
</template>
@ -21,11 +21,33 @@
components: {
Markdown
},
data: function () {
return {
val: ''
mounted() {
const that = this
window.onresize = () => {
return (() => {
window.fullHeight = document.documentElement.clientHeight
that.fullHeight = window.fullHeight
})()
}
},
data: function () {
return {
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: {
onReady(data) {
console.log(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`样式。

BIN
img/截图.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

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;

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

@ -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' : '']">

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

@ -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],