md-textarea.vue 13.0 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
璃白. 已提交
11 12
      @keydown.stop.50="keyup"
      @keydown.enter="handleEnter"
璃白.'s avatar
璃白. 已提交
13 14
      @keydown.meta.enter.exact="submit"
      @keydown.ctrl.enter.exact="submit"
璃白.'s avatar
fix  
璃白. 已提交
15
      @keydown.tab.prevent="$emit('tab')"
璃白.'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
    <transition name="slideup-fade">
璃白.'s avatar
璃白. 已提交
40 41 42 43 44 45
      <selectUser
        :userList="userList"
        v-show="showSelectUser"
        :position="selectUserPosition"
        @selectUser="selectUser"
      />
璃白.'s avatar
璃白. 已提交
46
    </transition>
璃白.'s avatar
璃白. 已提交
47 48 49
  </div>
</template>
<script>
50 51 52
import {
  getSelectionInfo,
  getPosition,
璃白.'s avatar
璃白. 已提交
53
  getFilteredTags,
璃白.'s avatar
璃白. 已提交
54
  getLinkTags,
璃白.'s avatar
璃白. 已提交
55
  formatText,
璃白.'s avatar
璃白. 已提交
56
  addLanguageClass,
57 58
  throttle as throttleFn
} from "@/assets/js/utils";
璃白.'s avatar
fix  
璃白. 已提交
59
import marked from "marked";
璃白.'s avatar
璃白. 已提交
60
import selectUser from "./components/user-select";
璃白.'s avatar
璃白. 已提交
61 62
import helpDoc from "./components/help-doc";
import DOMPurify from "dompurify";
璃白.'s avatar
璃白. 已提交
63
export default {
璃白.'s avatar
璃白. 已提交
64
  components: { helpDoc, selectUser },
璃白.'s avatar
璃白. 已提交
65
  props: {
66 67 68 69
    id: {
      type: String,
      default: ""
    },
璃白.'s avatar
fix  
璃白. 已提交
70 71 72 73 74 75 76
    html: {
      type: String,
      default: ""
    },
    htmlMinHeight: {
      default: ""
    },
璃白.'s avatar
璃白. 已提交
77 78 79 80
    fullScreen: {
      type: Boolean,
      default: false
    },
81 82 83 84
    throttleTime: {
      type: Number,
      default: 1000
    },
璃白.'s avatar
璃白. 已提交
85 86 87 88
    isFocus: {
      type: Boolean,
      default: false
    },
璃白.'s avatar
璃白. 已提交
89 90 91 92
    disabled: {
      type: Boolean,
      default: false
    },
璃白.'s avatar
璃白. 已提交
93 94 95 96 97 98
    placeholder: {
      type: String,
      default: false
    },
    fileList: {
      type: Array,
璃白.'s avatar
璃白. 已提交
99
      default: () => []
璃白.'s avatar
璃白. 已提交
100 101
    },
    text: {
璃白.'s avatar
璃白. 已提交
102 103
      type: [String, Number],
      default: ""
璃白.'s avatar
璃白. 已提交
104
    },
105 106 107 108 109 110 111 112
    maxLength: {
      type: [String, Number],
      default: ""
    },
    rows: {
      type: [String, Number],
      default: ""
    },
璃白.'s avatar
璃白. 已提交
113 114 115 116
    height: {
      type: Number,
      default: 0
    },
璃白.'s avatar
璃白. 已提交
117 118
    selectionInfo: {
      type: Object,
璃白.'s avatar
璃白. 已提交
119
      default: () => {}
璃白.'s avatar
璃白. 已提交
120 121 122 123 124 125 126
    },
    formatType: {
      default: ""
    },
    showHelp: {
      type: Boolean,
      default: false
璃白.'s avatar
璃白. 已提交
127 128 129 130
    },
    userList: {
      type: Array,
      default: () => []
璃白.'s avatar
璃白. 已提交
131 132
    }
  },
133

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

      this.$emit("getFilteredTags", filteredTags);
259
      this.$emit("update:html", cleanHtml);
璃白.'s avatar
璃白. 已提交
260
      if (links.length) this.$emit("renderLinksHtml", { vDom, links });
璃白.'s avatar
fix  
璃白. 已提交
261
    },
262
    input() {
璃白.'s avatar
璃白. 已提交
263
      if (this.showSelectUser) this.handleQueryUser();
264 265 266
      this.$emit("update:textLength", this.textContent.length);
      this.emitText();
    },
璃白.'s avatar
璃白. 已提交
267 268 269
    selectUser(user) {
      const originalText = this.textContent;
      const queryInfo = this.queryInfo;
璃白.'s avatar
璃白. 已提交
270
      console.log(queryInfo);
璃白.'s avatar
璃白. 已提交
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
      const cursorPosition = getPosition(this.id);
      const username = user.name + " ";
      const newText =
        originalText.slice(0, queryInfo.startPosition) +
        username +
        originalText.slice(queryInfo.endPosition);
      this.textContent = newText;
      this.emitText();
      this.showSelectUser = false;
      this.$nextTick(() => {
        const textEl = document.getElementById(this.id);
        textEl.setSelectionRange(
          cursorPosition + username.length,
          cursorPosition + username.length
        );
        textEl.focus();
      });
    },
    handleQueryUser() {
      const endPosition = getPosition(this.id);
璃白.'s avatar
璃白. 已提交
291
      const startPosition = this.queryInfo.startPosition;
璃白.'s avatar
璃白. 已提交
292 293 294 295 296 297 298 299 300 301
      const keyWord = this.textContent.slice(startPosition, endPosition);
      this.queryInfo.endPosition = endPosition;

      if (endPosition < startPosition || keyWord.slice(-1) === " ") {
        this.showSelectUser = false;
        return;
      }
      this.queryInfo.keyWord = keyWord;
      this.$emit("queryUserList", keyWord);
    },
璃白.'s avatar
璃白. 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
    keyup(e) {
      if (e.key === "@") {
        this.createSelectUserDialog();
      }
    },
    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;
璃白.'s avatar
璃白. 已提交
345 346
        this.queryInfo.startPosition = getPosition(this.id) + 1;
        this.queryInfo.endPosition = getPosition(this.id) + 1;
璃白.'s avatar
璃白. 已提交
347
        this.$emit("queryUserList", this.queryInfo.keyWord);
璃白.'s avatar
璃白. 已提交
348 349 350
      });
    },
    createHideEl(type) {
351 352 353 354 355 356 357 358 359
      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
璃白. 已提交
360
      const hideElId = type + this.id;
361 362 363 364 365 366 367 368 369
      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
璃白. 已提交
370 371 372 373 374 375 376 377
      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_");
378 379
      hideEl.innerText = this.textContent;
      const contentHeight = hideEl.offsetHeight;
380 381
      this.editorHeight = this.fullScreen
        ? "calc(100% - 42px)"
璃白.'s avatar
璃白. 已提交
382 383
        : this.height
        ? this.height + "px"
384 385 386 387
        : this.autoSize
        ? `${contentHeight + parseFloat(fontSize) * 1.2}px`
        : "auto";
      this.editorOverFlow =
璃白.'s avatar
fix  
璃白. 已提交
388
        this.autoSize && !this.fullScreen && !this.height ? "hidden" : "auto";
389 390
      textEl.parentNode.removeChild(hideEl);
    },
璃白.'s avatar
璃白. 已提交
391 392 393 394 395 396 397 398 399 400 401 402
    handleEnter() {
      if (this.showSelectUser) {
        const textEl = document.getElementById(this.id);
        textEl.blur();
        setTimeout(() => {
          textEl.focus();
          // this.showSelectUser = false;
        }, 0);
        return;
      }
      this.$emit("enter");
    },
璃白.'s avatar
璃白. 已提交
403 404 405
    submit() {
      this.$emit("submit");
    },
璃白.'s avatar
璃白. 已提交
406
    setFocus(val) {
璃白.'s avatar
璃白. 已提交
407
      this.$emit("update:isFocus", val);
璃白.'s avatar
璃白. 已提交
408
    },
璃白.'s avatar
璃白. 已提交
409 410
    checkSelection() {
      const info = getSelectionInfo(this.id);
璃白.'s avatar
璃白. 已提交
411 412
      if (!info) {
        const cursorPoint = getPosition(this.id);
璃白.'s avatar
璃白. 已提交
413
        this.$emit("update:selectionInfo", {
璃白.'s avatar
璃白. 已提交
414 415 416
          selectorId: this.id,
          selectionStart: cursorPoint,
          selectionEnd: cursorPoint
璃白.'s avatar
璃白. 已提交
417
        });
璃白.'s avatar
璃白. 已提交
418 419
        return;
      }
璃白.'s avatar
璃白. 已提交
420
      this.$emit("update:selectionInfo", info);
G
guoweijia 已提交
421 422 423 424 425 426 427 428 429 430 431
    },
    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
璃白. 已提交
432
      this.checkSelection();
璃白.'s avatar
璃白. 已提交
433
      this.$emit("update:fileList", fileList);
璃白.'s avatar
璃白. 已提交
434 435 436 437 438 439 440
    }
  }
};
</script>
<style lang="less" scoped>
.md_textarea {
  position: relative;
441
  padding: 14px 0;
璃白.'s avatar
璃白. 已提交
442
  background: var(--md-editor-content-bg-color);
443 444
  // border-left: 1px solid var(--md-editor-border-color);
  // border-right: 1px solid var(--md-editor-border-color);
璃白.'s avatar
璃白. 已提交
445
  transition: border 0.3s;
446
  // padding: 14px;
璃白.'s avatar
璃白. 已提交
447
  box-sizing: border-box;
448 449 450 451
  // &.isFocus {
  //   border-left: 1px solid var(--md-editor-border-color-active);
  //   border-right: 1px solid var(--md-editor-border-color-active);
  // }
452

璃白.'s avatar
璃白. 已提交
453
  &.fullScreen {
454
    height: 100%;
璃白.'s avatar
璃白. 已提交
455 456
    textarea {
      font-size: 20px;
457 458
      max-height: 100%;
      overflow-y: auto;
璃白.'s avatar
璃白. 已提交
459 460
    }
  }
璃白.'s avatar
璃白. 已提交
461 462 463
  &.disabled {
    background: var(--md-editor-content-bg-color-disabled);
  }
464

璃白.'s avatar
璃白. 已提交
465 466 467 468 469
  textarea {
    display: block;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
璃白.'s avatar
璃白. 已提交
470
    background: var(--md-editor-content-bg-color);
471 472
    color: var(--md-editor-text-color-active);
    height: var(--md-editor-height);
璃白.'s avatar
璃白. 已提交
473
    resize: none;
璃白.'s avatar
璃白. 已提交
474
    font-size: 14px;
璃白.'s avatar
璃白. 已提交
475
    line-height: 18px;
璃白.'s avatar
璃白. 已提交
476
    word-break: break-all;
璃白.'s avatar
璃白. 已提交
477 478 479
    font-family: "Menlo", -apple-system, SF UI Text, Arial, PingFang SC,
      Hiragino Sans GB, Microsoft YaHei, WenQuanYi Micro Hei, sans-serif, SimHei,
      SimSun;
480 481 482
    &::placeholder {
      color: var(--md-editor-text-color);
    }
璃白.'s avatar
璃白. 已提交
483 484 485
    // &:disabled {
    //   background: var(--md-editor-content-bg-color-disabled);
    // }
璃白.'s avatar
璃白. 已提交
486 487 488 489 490 491 492 493 494 495
  }
  .icon {
    position: absolute;
    top: 20px;
    right: 20px;
    font-size: 32px;
    cursor: pointer;
  }
}
</style>