Initial project
This commit is contained in:
112
src/components/markdown-editor/CalcTextareaHeight.ts
Normal file
112
src/components/markdown-editor/CalcTextareaHeight.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
// from element ui
|
||||
// https://github.com/ElemeFE/element/blob/dev/packages/input/src/calcTextareaHeight.js
|
||||
|
||||
let tempTextArea: HTMLTextAreaElement | null;
|
||||
|
||||
const HIDDEN_STYLE = `
|
||||
height:0 !important;
|
||||
visibility:hidden !important;
|
||||
overflow:hidden !important;
|
||||
position:absolute !important;
|
||||
z-index:-1000 !important;
|
||||
top:0 !important;
|
||||
right:0 !important
|
||||
`;
|
||||
|
||||
const CONTEXT_STYLE = [
|
||||
"letter-spacing",
|
||||
"line-height",
|
||||
"padding-top",
|
||||
"padding-bottom",
|
||||
"font-family",
|
||||
"font-weight",
|
||||
"font-size",
|
||||
"text-rendering",
|
||||
"text-transform",
|
||||
"width",
|
||||
"text-indent",
|
||||
"padding-left",
|
||||
"padding-right",
|
||||
"border-width",
|
||||
"box-sizing"
|
||||
];
|
||||
|
||||
type NodeStyling = {
|
||||
contextStyle: string;
|
||||
paddingSize: number;
|
||||
borderSize: number;
|
||||
boxSizing: string;
|
||||
}
|
||||
|
||||
export type Result = {
|
||||
height: number;
|
||||
minHeight: number;
|
||||
}
|
||||
|
||||
function calculateNodeStyling(targetElement: HTMLTextAreaElement): NodeStyling {
|
||||
const style = window.getComputedStyle(targetElement);
|
||||
|
||||
const boxSizing = style.getPropertyValue("box-sizing");
|
||||
|
||||
const paddingSize = (
|
||||
parseFloat(style.getPropertyValue("padding-bottom")) +
|
||||
parseFloat(style.getPropertyValue("padding-top"))
|
||||
);
|
||||
|
||||
const borderSize = (
|
||||
parseFloat(style.getPropertyValue("border-bottom-width")) +
|
||||
parseFloat(style.getPropertyValue("border-top-width"))
|
||||
);
|
||||
|
||||
const contextStyle = CONTEXT_STYLE
|
||||
.map(name => `${name}:${style.getPropertyValue(name)}`)
|
||||
.join(";");
|
||||
|
||||
return {contextStyle, paddingSize, borderSize, boxSizing};
|
||||
}
|
||||
|
||||
export default function calc(el: HTMLTextAreaElement, minRows = 1, maxRows?: number) {
|
||||
if (!tempTextArea) {
|
||||
tempTextArea = document.createElement("textarea") as HTMLTextAreaElement;
|
||||
document.body.appendChild(tempTextArea);
|
||||
}
|
||||
const {paddingSize, borderSize, boxSizing, contextStyle} = calculateNodeStyling(el);
|
||||
|
||||
tempTextArea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
|
||||
tempTextArea.value = el.value || el.placeholder || "";
|
||||
|
||||
let height = tempTextArea.scrollHeight;
|
||||
const result: Result = {
|
||||
height: 0,
|
||||
minHeight: 0
|
||||
};
|
||||
|
||||
if (boxSizing === "border-box") {
|
||||
height = height + borderSize;
|
||||
} else if (boxSizing === "content-box") {
|
||||
height = height - paddingSize;
|
||||
}
|
||||
|
||||
tempTextArea.value = "";
|
||||
const singleRowHeight = tempTextArea.scrollHeight - paddingSize;
|
||||
|
||||
if (minRows) {
|
||||
let minHeight = singleRowHeight * minRows;
|
||||
if (boxSizing === "border-box") {
|
||||
minHeight = minHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.max(minHeight, height);
|
||||
result.minHeight = minHeight;
|
||||
}
|
||||
if (maxRows) {
|
||||
let maxHeight = singleRowHeight * maxRows;
|
||||
if (boxSizing === "border-box") {
|
||||
maxHeight = maxHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.min(maxHeight, height);
|
||||
}
|
||||
result.height = height;
|
||||
tempTextArea.parentNode && tempTextArea.parentNode.removeChild(tempTextArea);
|
||||
tempTextArea = null;
|
||||
return result;
|
||||
}
|
||||
5
src/components/markdown-editor/index.ts
Normal file
5
src/components/markdown-editor/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "~/utils/Toolkit";
|
||||
|
||||
export const MarkdownEditor = Toolkit.withInstall(view);
|
||||
export default MarkdownEditor;
|
||||
192
src/components/markdown-editor/index.vue
Normal file
192
src/components/markdown-editor/index.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
class="tui-markdown-editor diselect"
|
||||
:class="{ 'fold': isFold }"
|
||||
>
|
||||
<div class="editor">
|
||||
<div class="header">
|
||||
<icon class="icon" name="WRITING" />
|
||||
<slot name="editorHeader">
|
||||
<h4 class="title">
|
||||
<span>源码</span>
|
||||
<span class="light-gray word-space">Markdown</span>
|
||||
</h4>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="editor">
|
||||
<textarea ref="textArea" class="text-area" v-model="_data"></textarea>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="preview" :class="{ 'showing': showingPreview }">
|
||||
<div class="header">
|
||||
<icon
|
||||
v-if="isFold"
|
||||
class="icon cur-pointer"
|
||||
:name="showingPreview ? 'ARROW_1_E' : 'ARROW_1_W'"
|
||||
@click="showingPreview = !showingPreview"
|
||||
/>
|
||||
<slot name="previewHeader">
|
||||
<h4 class="title">预览</h4>
|
||||
</slot>
|
||||
</div>
|
||||
<div ref="previewContent" class="content">
|
||||
<markdown-view :content="_data" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Icon, MarkdownView } from "~/index";
|
||||
import calcHeight from "./CalcTextareaHeight";
|
||||
|
||||
defineOptions({
|
||||
name: "MarkdownEditor"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
data?: string,
|
||||
minRows?: number,
|
||||
maxRows?: number
|
||||
}>(), {
|
||||
data: "",
|
||||
minRows: 8,
|
||||
maxRows: 32
|
||||
});
|
||||
const { data, minRows, maxRows } = toRefs(props);
|
||||
|
||||
const _data = ref(data.value);
|
||||
const textArea = ref<HTMLTextAreaElement>();
|
||||
const textAreaHeight = ref(30);
|
||||
const previewContent = ref<HTMLDivElement>();
|
||||
|
||||
const emit = defineEmits(["update:data"]);
|
||||
|
||||
watch(_data, () => {
|
||||
emit("update:data", _data.value);
|
||||
calcTextAreaHeight();
|
||||
});
|
||||
watch(data, () => {
|
||||
_data.value = data.value;
|
||||
calcTextAreaHeight();
|
||||
});
|
||||
|
||||
// 自适应折叠预览
|
||||
const root = ref<HTMLDivElement>();
|
||||
const isFold = ref(false);
|
||||
const showingPreview = ref(false);
|
||||
onMounted(() => {
|
||||
if (root.value) {
|
||||
const foldObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
if (entries && 0 < entries.length) {
|
||||
isFold.value = entries[0].contentRect.width < 650;
|
||||
}
|
||||
});
|
||||
foldObserver.observe(root.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 自适应高度
|
||||
const calcTextAreaHeight = () => {
|
||||
if (textArea.value) {
|
||||
textAreaHeight.value = calcHeight(textArea.value, minRows.value, maxRows.value).height;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
calcTextAreaHeight();
|
||||
if (textArea.value) {
|
||||
const textareaHeightObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
if (entries && 0 < entries.length) {
|
||||
if (previewContent.value) {
|
||||
previewContent.value.style.height = entries[0].contentRect.height + "px";
|
||||
}
|
||||
}
|
||||
});
|
||||
textareaHeightObserver.observe(textArea.value);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
textArea
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-markdown-editor {
|
||||
width: 100%;
|
||||
border: var(--tui-border);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--tui-dark-white);
|
||||
|
||||
.icon {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
line-height: 30px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
border-right: var(--tui-border);
|
||||
flex-direction: column;
|
||||
|
||||
.text-area {
|
||||
width: calc(100% - .5rem * 2);
|
||||
height: v-bind("textAreaHeight + 'px'");
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: .5rem;
|
||||
font-size: 14px;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 50%;
|
||||
background: #FFF;
|
||||
|
||||
.content {
|
||||
padding: .5rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.fold {
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview {
|
||||
left: calc(100% - 5rem);
|
||||
width: calc(100% - 2rem);
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
border-left: var(--tui-border);
|
||||
transition: left .5s var(--tui-bezier);
|
||||
box-shadow: -2px 0 0 var(--tui-shadow-color);
|
||||
|
||||
&.showing {
|
||||
left: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user