md-textarea.vue 10.9 KB
Newer Older
璃白.'s avatar
璃白. 已提交
1
<template>
璃白.'s avatar
璃白. 已提交
2
  <div :class="['md_textarea', { fullScreen, isFocus }]">
璃白.'s avatar
璃白. 已提交
3
    <textarea
璃白.'s avatar
璃白. 已提交
4
      spellcheck="false"
璃白.'s avatar
璃白. 已提交
5
      :id="id"
璃白.'s avatar
璃白. 已提交
6
      @change="$emit('update:text', textContent)"
7
      @input="input"
璃白.'s avatar
璃白. 已提交
8 9
      @focus="setFocus(true)"
      @blur="setFocus(false)"
G
guoweijia 已提交
10
      @paste="pasteFile"
璃白.'s avatar
fix  
璃白. 已提交
11
      @keydown.enter="$emit('enter')"
璃白.'s avatar
璃白. 已提交
12 13
      @keydown.meta.enter.exact="submit"
      @keydown.ctrl.enter.exact="submit"
璃白.'s avatar
fix  
璃白. 已提交
14
      @keydown.tab.prevent="$emit('tab')"
璃白.'s avatar
璃白. 已提交
15
      @keyup="keyup"
璃白.'s avatar
璃白. 已提交
16
      v-model="textContent"
璃白.'s avatar
璃白. 已提交
17
      :placeholder="placeholder"
18 19
      :maxlength="maxLength"
      :rows="rows"
璃白.'s avatar
璃白. 已提交
20
      :disabled="disabled"
璃白.'s avatar
璃白. 已提交
21 22 23
      :style="{
        height: editorHeight,
        overflow: editorOverFlow,
璃白.'s avatar
璃白. 已提交
24 25 26
        cursor: disabled
          ? 'not-allowed'
          : formatType
璃白.'s avatar
璃白. 已提交
27 28 29
          ? `url(https://codechina.csdn.net/codechina/operation-work/uploads/a1b7c2a995b2320dca911e2f2ecb9b88/format.png),text`
          : 'text'
      }"
璃白.'s avatar
璃白. 已提交
30 31
    >
    </textarea>
璃白.'s avatar
璃白. 已提交
32 33 34 35 36 37 38
    <transition name="slide-fade">
      <helpDoc
        v-if="showHelp"
        @updateShowHelp="$emit('updateShowHelp', $event)"
        :showHelp.sync="showHelp"
      />
    </transition>
璃白.'s avatar
璃白. 已提交
39 40 41
    <transition name="slideup-fade">
      <selectUser v-show="showSelectUser" :position="selectUserPosition" />
    </transition>
璃白.'s avatar
璃白. 已提交
42 43 44
  </div>
</template>
<script>
45 46 47
import {
  getSelectionInfo,
  getPosition,
璃白.'s avatar
璃白. 已提交
48
  getFilteredTags,
璃白.'s avatar
璃白. 已提交
49
  getLinkTags,
璃白.'s avatar
璃白. 已提交
50
  formatText,
璃白.'s avatar
璃白. 已提交
51
  addLanguageClass,
52 53
  throttle as throttleFn
} from "@/assets/js/utils";
璃白.'s avatar
fix  
璃白. 已提交
54
import marked from "marked";
璃白.'s avatar
璃白. 已提交
55
import selectUser from "./components/select-user";
璃白.'s avatar
璃白. 已提交
56 57
import helpDoc from "./components/help-doc";
import DOMPurify from "dompurify";
璃白.'s avatar
璃白. 已提交
58
export default {
璃白.'s avatar
璃白. 已提交
59
  components: { helpDoc, selectUser },
璃白.'s avatar
璃白. 已提交
60
  props: {
61 62 63 64
    id: {
      type: String,
      default: ""
    },
璃白.'s avatar
fix  
璃白. 已提交
65 66 67 68 69 70 71
    html: {
      type: String,
      default: ""
    },
    htmlMinHeight: {
      default: ""
    },
璃白.'s avatar
璃白. 已提交
72 73 74 75
    fullScreen: {
      type: Boolean,
      default: false
    },
76 77 78 79
    throttleTime: {
      type: Number,
      default: 1000
    },
璃白.'s avatar
璃白. 已提交
80 81 82 83
    isFocus: {
      type: Boolean,
      default: false
    },
璃白.'s avatar
璃白. 已提交
84 85 86 87
    disabled: {
      type: Boolean,
      default: false
    },
璃白.'s avatar
璃白. 已提交
88 89 90 91 92 93
    placeholder: {
      type: String,
      default: false
    },
    fileList: {
      type: Array,
璃白.'s avatar
璃白. 已提交
94
      default: () => []
璃白.'s avatar
璃白. 已提交
95 96
    },
    text: {
璃白.'s avatar
璃白. 已提交
97 98
      type: [String, Number],
      default: ""
璃白.'s avatar
璃白. 已提交
99
    },
100 101 102 103 104 105 106 107
    maxLength: {
      type: [String, Number],
      default: ""
    },
    rows: {
      type: [String, Number],
      default: ""
    },
璃白.'s avatar
璃白. 已提交
108 109 110 111
    height: {
      type: Number,
      default: 0
    },
璃白.'s avatar
璃白. 已提交
112 113
    selectionInfo: {
      type: Object,
璃白.'s avatar
璃白. 已提交
114
      default: () => {}
璃白.'s avatar
璃白. 已提交
115 116 117 118 119 120 121
    },
    formatType: {
      default: ""
    },
    showHelp: {
      type: Boolean,
      default: false
璃白.'s avatar
璃白. 已提交
122 123
    }
  },
124

璃白.'s avatar
璃白. 已提交
125 126
  data() {
    return {
127
      textContent: "",
128
      editorHeight: "auto",
璃白.'s avatar
璃白. 已提交
129 130 131
      editorOverFlow: "auto",
      showSelectUser: false,
      selectUserPosition: { left: 0, top: 0 }
璃白.'s avatar
璃白. 已提交
132 133 134 135 136
    };
  },
  created() {
    document.addEventListener("mouseup", this.checkSelection);
  },
璃白.'s avatar
fix  
璃白. 已提交
137
  mounted() {
璃白.'s avatar
fix  
璃白. 已提交
138
    this.resetPreviewMinHeight();
璃白.'s avatar
fix  
璃白. 已提交
139
  },
璃白.'s avatar
璃白. 已提交
140
  watch: {
璃白.'s avatar
fix  
璃白. 已提交
141 142 143 144 145 146 147
    isFocus: {
      handler: function(val) {
        if (val) {
          this.resetPreviewMinHeight();
        }
      }
    },
璃白.'s avatar
璃白. 已提交
148 149 150 151
    text: {
      immediate: true,
      handler: function(val) {
        this.textContent = val;
璃白.'s avatar
fix  
璃白. 已提交
152
        this.transferMarkdown(val);
璃白.'s avatar
璃白. 已提交
153
      }
154
    },
155 156
    fullScreen: {
      immediate: true,
璃白.'s avatar
璃白. 已提交
157 158 159 160 161 162
      handler: function(val) {
        if (val) {
          document.body.style.overflow = "hidden";
        } else {
          document.body.style.overflow = "auto";
        }
163
        setTimeout(() => {
璃白.'s avatar
fix  
璃白. 已提交
164
          this.reSizeTextareaHeight();
165 166 167
        }, 0);
      }
    },
168 169 170 171 172
    textContent: {
      immediate: true,
      handler: function() {
        setTimeout(() => {
          if (!this.autoSize) return;
璃白.'s avatar
fix  
璃白. 已提交
173
          this.reSizeTextareaHeight();
174 175
        }, 0);
      }
璃白.'s avatar
璃白. 已提交
176 177 178 179 180
    }
  },
  beforeDestroy() {
    document.removeEventListener("mouseup", this.checkSelection);
  },
181 182
  computed: {
    emitText() {
璃白.'s avatar
fix  
璃白. 已提交
183 184
      // return throttleFn(() => {}, this.throttleTime);
      return () => {
185
        this.$emit("update:text", this.textContent);
璃白.'s avatar
fix  
璃白. 已提交
186
      };
187 188 189 190 191
    },
    autoSize() {
      return this.rows === "auto";
    }
  },
璃白.'s avatar
璃白. 已提交
192
  methods: {
璃白.'s avatar
fix  
璃白. 已提交
193 194 195 196 197 198 199
    resetPreviewMinHeight() {
      setTimeout(() => {
        const textEl = document.getElementById(this.id);
        if (!textEl) return;
        const height = textEl.offsetHeight;
        this.$emit("update:htmlMinHeight", height);
      }, 0);
璃白.'s avatar
fix  
璃白. 已提交
200 201 202
    },
    transferMarkdown(val) {
      marked.setOptions({
203 204 205
        breaks: true,
        gfm: true,
        langPrefix: "language-",
璃白.'s avatar
fix  
璃白. 已提交
206
        highlight: function(code, lang, callback) {
璃白.'s avatar
璃白. 已提交
207
          let html = require("highlight.js").highlightAuto(code).value;
璃白.'s avatar
fix  
璃白. 已提交
208 209 210 211
          return html;
        }
      });
      const str = val + "";
璃白.'s avatar
璃白. 已提交
212 213
      const html = marked(str); // 解析markdown
      const virtualDom = addLanguageClass(html); // 如果没指定语言,添加默认语言
214
      const cleanHtml = DOMPurify.sanitize(virtualDom.innerHTML, {
璃白.'s avatar
璃白. 已提交
215 216 217 218 219 220 221 222 223 224
        FORBID_TAGS: [
          "style",
          "script",
          "select",
          "option",
          "input",
          "textarea",
          "form",
          "button"
        ]
璃白.'s avatar
璃白. 已提交
225 226 227 228
      }); // 去除标签
      const filteredTags = getFilteredTags(html, cleanHtml); // 计算是否有标签被过滤
      // 链接转换为卡片
      const { vDom, links } = getLinkTags(this.id, cleanHtml);
璃白.'s avatar
璃白. 已提交
229 230

      this.$emit("getFilteredTags", filteredTags);
231
      this.$emit("update:html", cleanHtml);
璃白.'s avatar
璃白. 已提交
232
      if (links.length) this.$emit("renderLinksHtml", { vDom, links });
璃白.'s avatar
fix  
璃白. 已提交
233
    },
234 235 236 237
    input() {
      this.$emit("update:textLength", this.textContent.length);
      this.emitText();
    },
璃白.'s avatar
璃白. 已提交
238 239 240 241
    keyup(e) {
      if (e.key === "@") {
        this.createSelectUserDialog();
      }
璃白.'s avatar
璃白. 已提交
242 243 244 245 246
      // console.log(e);

      if (e.code === "Space" || e.code === "Enter") {
        this.showSelectUser = false;
      }
璃白.'s avatar
璃白. 已提交
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
    },
    createSelectUserDialog() {
      const textEl = document.getElementById(this.id);
      if (!textEl) return;
      const height = getComputedStyle(textEl).getPropertyValue("height");
      const width = getComputedStyle(textEl).getPropertyValue("width");
      const scrollTop = textEl.scrollTop;
      const originalText = this.textContent;
      const cursorPoint = getPosition(this.id);
      const selectionInfo = {
        selectionStart: cursorPoint - 1,
        selectionEnd: cursorPoint
      };
      const newText = formatText(
        originalText,
        selectionInfo,
        "<span id='call_position'>",
        "</span>"
      );

      const hideEl = this.createHideEl("clac_position_El_");
      hideEl.style.position = "absolute";
      hideEl.style.width = width;
      hideEl.style.height = height;
      hideEl.style.overflowY = "auto";
      hideEl.style.wordBreak = "break-all";
      hideEl.style.top = "14px";
      hideEl.style.left = 0;
      hideEl.style.whiteSpace = "pre-wrap";
      hideEl.innerHTML = newText;
      this.$nextTick(() => {
        hideEl.scrollTop = scrollTop;
        const pEl = document.getElementById("call_position");
        this.selectUserPosition = {
          left: pEl.getBoundingClientRect().left,
          top: pEl.getBoundingClientRect().top
        };
        textEl.parentNode.removeChild(hideEl);
        this.showSelectUser = true;
      });
    },
    createHideEl(type) {
289 290 291 292 293 294 295 296 297
      const textEl = document.getElementById(this.id);
      if (!textEl) return;
      const fontSize = getComputedStyle(textEl).getPropertyValue("font-size");
      const lineHeight = getComputedStyle(textEl).getPropertyValue(
        "line-height"
      );
      const fontFamily = getComputedStyle(textEl).getPropertyValue(
        "font-family"
      );
璃白.'s avatar
璃白. 已提交
298
      const hideElId = type + this.id;
299 300 301 302 303 304 305 306 307
      let hideEl = document.getElementById(hideElId);
      if (!hideEl) {
        hideEl = document.createElement("div");
        textEl.parentNode.appendChild(hideEl);
      }
      hideEl.id = hideElId;
      hideEl.style.fontSize = fontSize;
      hideEl.style.lineHeight = lineHeight;
      hideEl.style.fontFamily = fontFamily;
璃白.'s avatar
璃白. 已提交
308 309 310 311 312 313 314 315
      return hideEl;
    },
    reSizeTextareaHeight() {
      const textEl = document.getElementById(this.id);
      if (!textEl) return;
      const fontSize = getComputedStyle(textEl).getPropertyValue("font-size");

      const hideEl = this.createHideEl("clac_height_El_");
316 317
      hideEl.innerText = this.textContent;
      const contentHeight = hideEl.offsetHeight;
318 319
      this.editorHeight = this.fullScreen
        ? "calc(100% - 42px)"
璃白.'s avatar
璃白. 已提交
320 321
        : this.height
        ? this.height + "px"
322 323 324 325
        : this.autoSize
        ? `${contentHeight + parseFloat(fontSize) * 1.2}px`
        : "auto";
      this.editorOverFlow =
璃白.'s avatar
fix  
璃白. 已提交
326
        this.autoSize && !this.fullScreen && !this.height ? "hidden" : "auto";
327 328
      textEl.parentNode.removeChild(hideEl);
    },
璃白.'s avatar
璃白. 已提交
329 330 331
    submit() {
      this.$emit("submit");
    },
璃白.'s avatar
璃白. 已提交
332
    setFocus(val) {
璃白.'s avatar
璃白. 已提交
333
      this.$emit("update:isFocus", val);
璃白.'s avatar
璃白. 已提交
334
    },
璃白.'s avatar
璃白. 已提交
335 336
    checkSelection() {
      const info = getSelectionInfo(this.id);
璃白.'s avatar
璃白. 已提交
337 338
      if (!info) {
        const cursorPoint = getPosition(this.id);
璃白.'s avatar
璃白. 已提交
339
        this.$emit("update:selectionInfo", {
璃白.'s avatar
璃白. 已提交
340 341 342
          selectorId: this.id,
          selectionStart: cursorPoint,
          selectionEnd: cursorPoint
璃白.'s avatar
璃白. 已提交
343
        });
璃白.'s avatar
璃白. 已提交
344 345
        return;
      }
璃白.'s avatar
璃白. 已提交
346
      this.$emit("update:selectionInfo", info);
G
guoweijia 已提交
347 348 349 350 351 352 353 354 355 356 357
    },
    pasteFile(event) {
      let fileList = [];
      const items = (event.clipboardData || window.clipboardData).items;
      for (let i = 0; i < items.length; i++) {
        if (items[i].type.indexOf("image") !== -1) {
          fileList.push(items[i].getAsFile());
          break;
        }
      }
      if (!fileList.length) return;
璃白.'s avatar
璃白. 已提交
358
      this.checkSelection();
璃白.'s avatar
璃白. 已提交
359
      this.$emit("update:fileList", fileList);
璃白.'s avatar
璃白. 已提交
360 361 362 363 364 365 366
    }
  }
};
</script>
<style lang="less" scoped>
.md_textarea {
  position: relative;
367
  padding: 14px 0;
璃白.'s avatar
璃白. 已提交
368
  background: var(--md-editor-content-bg-color);
369 370
  // border-left: 1px solid var(--md-editor-border-color);
  // border-right: 1px solid var(--md-editor-border-color);
璃白.'s avatar
璃白. 已提交
371
  transition: border 0.3s;
372
  // padding: 14px;
璃白.'s avatar
璃白. 已提交
373
  box-sizing: border-box;
374 375 376 377
  // &.isFocus {
  //   border-left: 1px solid var(--md-editor-border-color-active);
  //   border-right: 1px solid var(--md-editor-border-color-active);
  // }
378

璃白.'s avatar
璃白. 已提交
379
  &.fullScreen {
380
    height: 100%;
璃白.'s avatar
璃白. 已提交
381 382
    textarea {
      font-size: 20px;
383 384
      max-height: 100%;
      overflow-y: auto;
璃白.'s avatar
璃白. 已提交
385 386
    }
  }
璃白.'s avatar
璃白. 已提交
387 388 389
  &.disabled {
    background: var(--md-editor-content-bg-color-disabled);
  }
390

璃白.'s avatar
璃白. 已提交
391 392 393 394 395
  textarea {
    display: block;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
璃白.'s avatar
璃白. 已提交
396
    background: var(--md-editor-content-bg-color);
397 398
    color: var(--md-editor-text-color-active);
    height: var(--md-editor-height);
璃白.'s avatar
璃白. 已提交
399
    resize: none;
璃白.'s avatar
璃白. 已提交
400 401
    font-size: 14px;
    word-break: break-all;
璃白.'s avatar
璃白. 已提交
402 403 404
    font-family: "Menlo", -apple-system, SF UI Text, Arial, PingFang SC,
      Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, sans-serif, SimHei,
      SimSun;
405 406 407
    &::placeholder {
      color: var(--md-editor-text-color);
    }
璃白.'s avatar
璃白. 已提交
408 409 410
    // &:disabled {
    //   background: var(--md-editor-content-bg-color-disabled);
    // }
璃白.'s avatar
璃白. 已提交
411 412 413 414 415 416 417 418 419 420
  }
  .icon {
    position: absolute;
    top: 20px;
    right: 20px;
    font-size: 32px;
    cursor: pointer;
  }
}
</style>