md-editor-v3
Version:
Markdown editor for vue3, developed in jsx and typescript, dark theme、beautify content by prettier、render articles directly、paste or clip the picture and upload it...
647 lines (646 loc) • 22.2 kB
JavaScript
;
const vue = require("vue");
const config = require("./config.cjs");
const index = require("./index5.cjs");
const eventName = require("./event-name.cjs");
const util = require("@vavt/util");
const DATA_LINE_SELECTOR = `.${config.prefix}-preview > [data-line]`;
const getComputedStyleNum = (ele, key) => {
return +getComputedStyle(ele).getPropertyValue(key).replace("px", "");
};
const scrollAutoWithScale = (pEle, cEle) => {
const addEvent = util.debounce(() => {
pEle.removeEventListener("scroll", scrollHandler);
pEle.addEventListener("scroll", scrollHandler);
cEle.removeEventListener("scroll", scrollHandler);
cEle.addEventListener("scroll", scrollHandler);
}, 50);
const scrollHandler = (e) => {
const pHeight = pEle.clientHeight;
const cHeight = cEle.clientHeight;
const pScrollHeight = pEle.scrollHeight;
const cScrollHeight = cEle.scrollHeight;
const scale = (pScrollHeight - pHeight) / (cScrollHeight - cHeight);
if (e.target === pEle) {
cEle.removeEventListener("scroll", scrollHandler);
cEle.scrollTo({
top: pEle.scrollTop / scale
// behavior: 'smooth'
});
addEvent();
} else {
pEle.removeEventListener("scroll", scrollHandler);
pEle.scrollTo({
top: cEle.scrollTop * scale
// behavior: 'smooth'
});
addEvent();
}
};
return [
() => {
addEvent().finally(() => {
pEle.dispatchEvent(new Event("scroll"));
});
},
() => {
pEle.removeEventListener("scroll", scrollHandler);
cEle.removeEventListener("scroll", scrollHandler);
}
];
};
const scrollAuto = (pEle, cEle, codeMirrorUt) => {
const { view } = codeMirrorUt;
const smoothScroll = util.createSmoothScroll();
const getTopByLine = (line) => {
return view.lineBlockAt(view.state.doc.line(line + 1).from).top;
};
const getBottomByLine = (line) => {
return view.lineBlockAt(view.state.doc.line(line + 1).from).bottom;
};
let blockMap = [];
let elesHasLineNumber = [];
let startLines = [];
const buildMap = () => {
blockMap = [];
elesHasLineNumber = Array.from(
cEle.querySelectorAll(DATA_LINE_SELECTOR)
);
startLines = elesHasLineNumber.map((item) => Number(item.dataset.line));
const tempStartLines = [...startLines];
const { lines } = view.state.doc;
let start = tempStartLines.shift() || 0;
let end = tempStartLines.shift() || lines;
for (let i = 0; i < lines; i++) {
if (i === end) {
start = i;
end = tempStartLines.shift() || lines;
}
blockMap.push({
start,
end: end - 1
});
}
};
const getLineNumber = (pMaxScrollLength, cMaxScrollLength) => {
let lineNumer = 1;
for (let i = elesHasLineNumber.length - 1; i - 1 >= 0; i--) {
const curr = elesHasLineNumber[i];
const sibling = elesHasLineNumber[i - 1];
if (curr.offsetTop + curr.offsetHeight > cMaxScrollLength && sibling.offsetTop < cMaxScrollLength) {
lineNumer = Number(sibling.dataset.line);
break;
}
}
for (let i = blockMap.length - 1; i >= 0; i--) {
const itemBottom = getBottomByLine(blockMap[i].end);
const itemTop = getTopByLine(blockMap[i].start);
if (itemBottom > pMaxScrollLength && itemTop <= pMaxScrollLength) {
lineNumer = lineNumer < blockMap[i].start ? lineNumer : blockMap[i].start;
break;
}
}
return lineNumer;
};
let pLock = 0;
let cLock = 0;
const pEleHandler = () => {
var _a, _b, _c;
if (cLock !== 0) {
return false;
}
pLock++;
const { scrollDOM, contentHeight } = view;
let cElePaddingTop = getComputedStyleNum(cEle, "padding-top");
const blockInfo = view.lineBlockAtHeight(scrollDOM.scrollTop);
const { number: currLine } = view.state.doc.lineAt(blockInfo.from);
const blockData = blockMap[currLine - 1];
if (!blockData) {
return false;
}
let scale = 1;
const startEle = cEle.querySelector(`[data-line="${blockData.start}"]`) || ((_a = cEle.firstElementChild) == null ? void 0 : _a.firstElementChild);
const endEle = cEle.querySelector(`[data-line="${blockData.end + 1}"]`) || ((_b = cEle.lastElementChild) == null ? void 0 : _b.lastElementChild);
const pMaxScrollLength = scrollDOM.scrollHeight - scrollDOM.clientHeight;
const cMaxScrollLength = cEle.scrollHeight - cEle.clientHeight;
let startTop = getTopByLine(blockData.start);
let endBottom = getBottomByLine(blockData.end);
let startEleOffetTop = startEle.offsetTop;
let blockHeight = endEle.offsetTop - startEleOffetTop;
if (startTop === 0) {
startEleOffetTop = 0;
if (startEle === endEle) {
cElePaddingTop = 0;
endBottom = contentHeight - scrollDOM.offsetHeight;
blockHeight = cMaxScrollLength;
} else {
blockHeight = endEle.offsetTop;
}
}
scale = (scrollDOM.scrollTop - startTop) / (endBottom - startTop);
const endElePos = endEle == ((_c = cEle.lastElementChild) == null ? void 0 : _c.lastElementChild) ? endEle.offsetTop + endEle.clientHeight : endEle.offsetTop;
if (endBottom >= pMaxScrollLength || endElePos > cMaxScrollLength) {
const lineNumer = getLineNumber(pMaxScrollLength, cMaxScrollLength);
startTop = getTopByLine(lineNumer);
scale = (scrollDOM.scrollTop - startTop) / (pMaxScrollLength - startTop);
const _startEle = cEle.querySelector(`[data-line="${lineNumer}"]`);
if (startTop > 0 && _startEle) {
startEleOffetTop = _startEle.offsetTop;
}
blockHeight = cMaxScrollLength - startEleOffetTop + getComputedStyleNum(cEle, "padding-top");
}
const scrollToTop = startEleOffetTop - cElePaddingTop + blockHeight * scale;
smoothScroll(cEle, scrollToTop, () => {
pLock--;
});
};
const cEleHandler = () => {
var _a, _b, _c, _d, _e, _f;
if (pLock !== 0) {
return;
}
cLock++;
const { scrollDOM } = view;
const cScrollTop = cEle.scrollTop;
const cScrollHeight = cEle.scrollHeight;
const pMaxScrollLength = scrollDOM.scrollHeight - scrollDOM.clientHeight;
const cMaxScrollLength = cEle.scrollHeight - cEle.clientHeight;
let realEleStart = (_a = cEle.firstElementChild) == null ? void 0 : _a.firstElementChild;
let realEleEnd = (_b = cEle.firstElementChild) == null ? void 0 : _b.lastElementChild;
if (startLines.length > 0) {
let virtualLine = Math.ceil(
startLines[startLines.length - 1] * (cScrollTop / cScrollHeight)
);
let startLineIndex = startLines.findLastIndex((value) => value <= virtualLine);
startLineIndex = startLineIndex === -1 ? 0 : startLineIndex;
virtualLine = startLines[startLineIndex];
for (let i = startLineIndex; i >= 0 && i < startLines.length; ) {
const currentElementTop = elesHasLineNumber[i].offsetTop;
if (currentElementTop > cScrollTop) {
if (i - 1 >= 0) {
i--;
continue;
}
virtualLine = -1;
startLineIndex = i;
break;
} else {
if (i + 1 < startLines.length && elesHasLineNumber[i + 1].offsetTop < cScrollTop) {
i++;
continue;
}
virtualLine = startLines[i];
startLineIndex = i;
break;
}
}
switch (startLineIndex) {
case -1: {
realEleStart = (_c = cEle.firstElementChild) == null ? void 0 : _c.firstElementChild;
realEleEnd = elesHasLineNumber[startLineIndex];
break;
}
case startLines.length - 1: {
realEleStart = elesHasLineNumber[startLineIndex];
realEleEnd = (_d = cEle.firstElementChild) == null ? void 0 : _d.lastElementChild;
break;
}
default: {
realEleStart = elesHasLineNumber[startLineIndex];
realEleEnd = elesHasLineNumber[startLineIndex + 1 === elesHasLineNumber.length ? startLineIndex : startLineIndex + 1];
}
}
}
let eleStartOffsetTop = realEleStart === ((_e = cEle.firstElementChild) == null ? void 0 : _e.firstElementChild) ? 0 : realEleStart.offsetTop - getComputedStyleNum(realEleStart, "margin-top");
let eleEndOffsetTop = realEleEnd.offsetTop;
let scale = 0;
const { start, end } = blockMap[Number(realEleStart.dataset.line || 0)];
let firstLineScrollTop = getTopByLine(start);
const endLineScrollTop = getTopByLine(
end + 1 === view.state.doc.lines ? end : end + 1
);
let blockHeight = 0;
if (endLineScrollTop > pMaxScrollLength || realEleEnd.offsetTop + realEleEnd.offsetHeight > cMaxScrollLength) {
const lineNumer = getLineNumber(pMaxScrollLength, cMaxScrollLength);
const _startEle = cEle.querySelector(`[data-line="${lineNumer}"]`);
eleStartOffsetTop = _startEle ? _startEle.offsetTop - getComputedStyleNum(_startEle, "margin-top") : eleStartOffsetTop;
firstLineScrollTop = getTopByLine(lineNumer);
scale = (cScrollTop - eleStartOffsetTop) / (cMaxScrollLength - eleStartOffsetTop);
blockHeight = pMaxScrollLength - firstLineScrollTop;
} else if (realEleStart === ((_f = cEle.firstElementChild) == null ? void 0 : _f.firstElementChild)) {
if (realEleStart === realEleEnd) {
eleEndOffsetTop = realEleEnd.offsetTop + realEleEnd.offsetHeight + +getComputedStyle(realEleEnd).marginBottom.replace("px", "");
blockHeight = endLineScrollTop;
} else {
blockHeight = endLineScrollTop;
}
scale = Math.max(cScrollTop / eleEndOffsetTop, 0);
} else {
scale = Math.max(
(cScrollTop - eleStartOffsetTop) / (eleEndOffsetTop - eleStartOffsetTop),
0
);
blockHeight = endLineScrollTop - firstLineScrollTop;
}
smoothScroll(pEle, firstLineScrollTop + blockHeight * scale, () => {
cLock--;
});
};
const scrollHandler = (e) => {
var _a;
const { scrollDOM, contentHeight } = view;
const scrollDomHeight = scrollDOM.clientHeight;
if (contentHeight <= scrollDomHeight || cEle.firstElementChild.clientHeight <= cEle.clientHeight) {
return false;
}
if (view.state.doc.lines <= ((_a = blockMap[blockMap.length - 1]) == null ? void 0 : _a.end)) {
return false;
}
if (e.target === pEle) {
pEleHandler();
} else {
cEleHandler();
}
};
return [
() => {
buildMap();
pEle.addEventListener("scroll", scrollHandler);
cEle.addEventListener("scroll", scrollHandler);
pEle.dispatchEvent(new Event("scroll"));
},
() => {
pEle.removeEventListener("scroll", scrollHandler);
cEle.removeEventListener("scroll", scrollHandler);
}
];
};
const props$1 = {
tocItem: {
type: Object,
default: () => ({})
},
mdHeadingId: {
type: Function,
default: () => {
}
},
onActive: {
type: Function,
default: () => {
}
},
onClick: {
type: Function,
default: () => {
}
},
scrollElementOffsetTop: {
type: Number,
default: 0
}
};
const CatalogLink = /* @__PURE__ */ vue.defineComponent({
props: props$1,
setup(props2) {
const scrollElementRef = vue.inject("scrollElementRef");
const rootNodeRef = vue.inject("roorNodeRef");
const currRef = vue.ref();
vue.watch(() => props2.tocItem.active, (active) => {
if (active) {
props2.onActive(props2.tocItem, currRef.value);
}
});
vue.onMounted(() => {
if (props2.tocItem.active) {
props2.onActive(props2.tocItem, currRef.value);
}
});
return () => {
const {
tocItem,
mdHeadingId,
onClick,
scrollElementOffsetTop
} = props2;
return vue.createVNode("div", {
"ref": currRef,
"class": [`${config.prefix}-catalog-link`, tocItem.active && `${config.prefix}-catalog-active`],
"onClick": (e) => {
e.stopPropagation();
onClick(e, tocItem);
if (e.defaultPrevented) {
return;
}
const id = mdHeadingId(tocItem.text, tocItem.level, tocItem.index);
const targetHeadEle = rootNodeRef.value.getElementById(id);
const scrollContainer = scrollElementRef.value;
if (targetHeadEle && scrollContainer) {
let par = targetHeadEle.offsetParent;
let offsetTop = targetHeadEle.offsetTop;
if (scrollContainer.contains(par)) {
while (par && scrollContainer != par) {
offsetTop += par == null ? void 0 : par.offsetTop;
par = par == null ? void 0 : par.offsetParent;
}
}
const pel = targetHeadEle.previousElementSibling;
let currMarginTop = 0;
if (!pel) {
currMarginTop = getComputedStyleNum(targetHeadEle, "margin-top");
}
scrollContainer == null ? void 0 : scrollContainer.scrollTo({
top: offsetTop - scrollElementOffsetTop - currMarginTop,
behavior: "smooth"
});
}
}
}, [vue.createVNode("span", {
"title": tocItem.text
}, [tocItem.text]), tocItem.children && tocItem.children.length > 0 && vue.createVNode("div", {
"class": `${config.prefix}-catalog-wrapper`
}, [tocItem.children.map((item) => vue.createVNode(CatalogLink, {
"mdHeadingId": mdHeadingId,
"key": `${tocItem.text}-link-${item.level}-${item.text}`,
"tocItem": item,
"onActive": props2.onActive,
"onClick": onClick,
"scrollElementOffsetTop": scrollElementOffsetTop
}, null))])]);
};
}
});
const props = {
/**
* 编辑器的Id,务必与需要绑定的编辑器Id相同
*/
editorId: {
type: String,
default: void 0
},
class: {
type: String,
default: ""
},
mdHeadingId: {
type: Function,
default: (text) => text
},
/**
* 指定滚动的容器,选择器需带上对应的符号,默认预览框
* 元素必须定位!!!!!!
*
* 默认:#md-editor-preview-wrapper
*/
scrollElement: {
type: [String, Object],
default: void 0
},
theme: {
type: String,
default: "light"
},
/**
* 高亮标题相对滚动容器顶部偏移量,即距离该值时,高亮当前目录菜单项
*
* 默认:20px
*/
offsetTop: {
type: Number,
default: 20
},
/**
* 滚动区域的固定顶部高度
*
* 默认:0
*/
scrollElementOffsetTop: {
type: Number,
default: 0
},
onClick: {
type: Function,
default: void 0
},
onActive: {
type: Function,
default: void 0
},
/**
* 滚动容器是否在web component中,默认不在
*
* 在其中的话通过document查询不到
*/
isScrollElementInShadow: {
type: Boolean,
default: false
},
/**
* 设置与哪个区域同步,默认与内容区域同步
*
* >= v5.3.0
*/
syncWith: {
type: String,
default: "preview"
},
/**
* 控制最大显示的目录层级
*/
catalogMaxDepth: {
type: Number,
default: void 0
}
};
const MdCatalog = /* @__PURE__ */ vue.defineComponent({
name: "MdCatalog",
props,
emits: ["onClick", "onActive"],
setup(props2, ctx) {
const editorId = props2.editorId;
const defaultScrollElement = `#${editorId}-preview-wrapper`;
const state = vue.reactive({
list: [],
show: false,
scrollElement: props2.scrollElement || defaultScrollElement
});
const activeItem = vue.shallowRef();
const catalogRef = vue.ref();
const scrollElementRef = vue.ref();
const scrollContainerRef = vue.ref();
const rootNodeRef = vue.ref();
const editorViewRef = vue.shallowRef();
const indicatorStyles = vue.ref({});
vue.provide("scrollElementRef", scrollElementRef);
vue.provide("roorNodeRef", rootNodeRef);
const catalogs = vue.computed(() => {
const tocItems = [];
state.list.forEach((listItem, index2) => {
if (props2.catalogMaxDepth && listItem.level > props2.catalogMaxDepth) {
return;
}
const {
text,
level,
line
} = listItem;
const item = {
level,
text,
line,
index: index2 + 1,
active: activeItem.value === listItem
};
if (tocItems.length === 0) {
tocItems.push(item);
} else {
let lastItem = tocItems[tocItems.length - 1];
if (item.level > lastItem.level) {
for (let i = lastItem.level + 1; i <= 6; i++) {
const {
children
} = lastItem;
if (!children) {
lastItem.children = [item];
break;
}
lastItem = children[children.length - 1];
if (item.level <= lastItem.level) {
children.push(item);
break;
}
}
} else {
tocItems.push(item);
}
}
});
return tocItems;
});
const getScrollElement = () => {
var _a;
if (state.scrollElement instanceof HTMLElement) {
return state.scrollElement;
}
let scrollRoot = document;
if (state.scrollElement === defaultScrollElement || props2.isScrollElementInShadow) {
scrollRoot = (_a = catalogRef.value) == null ? void 0 : _a.getRootNode();
}
return scrollRoot.querySelector(state.scrollElement);
};
const findActiveHeading = (list) => {
if (list.length === 0) {
activeItem.value = void 0;
state.list = [];
return false;
}
const {
activeHead
} = list.reduce((activeData, link, index$1) => {
var _a;
let relativeTop = 0;
if (props2.syncWith === "preview") {
const linkEle = (_a = rootNodeRef.value) == null ? void 0 : _a.getElementById(props2.mdHeadingId(link.text, link.level, index$1 + 1));
if (linkEle instanceof HTMLElement) {
relativeTop = index.getRelativeTop(linkEle, scrollElementRef.value);
}
} else {
const view = editorViewRef.value;
if (view) {
const top = view.lineBlockAt(view.state.doc.line(link.line + 1).from).top;
const scrollTop = view.scrollDOM.scrollTop;
relativeTop = top - scrollTop;
}
}
if (relativeTop < props2.offsetTop && relativeTop > activeData.minTop) {
return {
activeHead: link,
minTop: relativeTop
};
}
return activeData;
}, {
activeHead: list[0],
minTop: Number.MIN_SAFE_INTEGER
});
activeItem.value = activeHead;
state.list = list;
};
const onActive = (tocItem, ele) => {
var _a;
indicatorStyles.value.top = ele.offsetTop + getComputedStyleNum(ele, "padding-top") + "px";
(_a = props2.onActive) == null ? void 0 : _a.call(props2, tocItem, ele);
ctx.emit("onActive", tocItem, ele);
};
const scrollHandler = () => {
findActiveHeading(state.list);
};
const catalogChangedHandler = (_list) => {
var _a, _b, _c;
(_a = scrollContainerRef.value) == null ? void 0 : _a.removeEventListener("scroll", scrollHandler);
if (props2.syncWith === "editor") {
scrollContainerRef.value = (_b = editorViewRef.value) == null ? void 0 : _b.scrollDOM;
} else {
const scrollElement = getScrollElement();
scrollElementRef.value = scrollElement;
scrollContainerRef.value = scrollElement === document.documentElement ? document : scrollElement;
}
findActiveHeading(_list);
(_c = scrollContainerRef.value) == null ? void 0 : _c.addEventListener("scroll", scrollHandler);
};
const getEditorView = (view) => {
editorViewRef.value = view;
};
vue.watch([() => props2.syncWith, editorViewRef, () => props2.catalogMaxDepth], () => {
catalogChangedHandler(state.list);
});
vue.onMounted(() => {
rootNodeRef.value = catalogRef.value.getRootNode();
eventName.bus.on(editorId, {
name: eventName.CATALOG_CHANGED,
callback: catalogChangedHandler
});
eventName.bus.on(editorId, {
name: eventName.GET_EDITOR_VIEW,
callback: getEditorView
});
eventName.bus.emit(editorId, eventName.PUSH_CATALOG);
eventName.bus.emit(editorId, eventName.SEND_EDITOR_VIEW);
});
vue.onBeforeUnmount(() => {
var _a;
eventName.bus.remove(editorId, eventName.CATALOG_CHANGED, catalogChangedHandler);
eventName.bus.remove(editorId, eventName.GET_EDITOR_VIEW, getEditorView);
(_a = scrollContainerRef.value) == null ? void 0 : _a.removeEventListener("scroll", scrollHandler);
});
return () => vue.createVNode("div", {
"class": [`${config.prefix}-catalog`, props2.theme === "dark" && `${config.prefix}-catalog-dark`, props2.class || ""],
"ref": catalogRef
}, [catalogs.value.length > 0 && vue.createVNode(vue.Fragment, null, [vue.createVNode("div", {
"class": `${config.prefix}-catalog-indicator`,
"style": indicatorStyles.value
}, null), vue.createVNode("div", {
"class": `${config.prefix}-catalog-container`
}, [catalogs.value.map((item) => {
return vue.createVNode(CatalogLink, {
"mdHeadingId": props2.mdHeadingId,
"tocItem": item,
"key": `link-${item.level}-${item.text}`,
"onActive": onActive,
"onClick": (e, t) => {
var _a;
(_a = props2.onClick) == null ? void 0 : _a.call(props2, e, t);
ctx.emit("onClick", e, t);
},
"scrollElementOffsetTop": props2.scrollElementOffsetTop
}, null);
})])])]);
}
});
MdCatalog.install = (app) => {
app.component(MdCatalog.name, MdCatalog);
return app;
};
exports.MdCatalog = MdCatalog;
exports.scrollAuto = scrollAuto;
exports.scrollAutoWithScale = scrollAutoWithScale;