Initial project

This commit is contained in:
Timi
2025-07-08 16:33:11 +08:00
parent 1a5a16be74
commit f862530142
80 changed files with 8301 additions and 129 deletions

View 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;
}

View File

@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "~/utils/Toolkit";
export const MarkdownEditor = Toolkit.withInstall(view);
export default MarkdownEditor;

View 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>