md-textarea.vue 13.2 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
  rerender,
璃白.'s avatar
璃白. 已提交
57
  addLanguageClass,
58 59
  throttle as throttleFn
} from "@/assets/js/utils";
璃白.'s avatar
fix  
璃白. 已提交
60
import marked from "marked";
璃白.'s avatar
璃白. 已提交
61
import selectUser from "./components/user-select";
璃白.'s avatar
璃白. 已提交
62 63
import helpDoc from "./components/help-doc";
import DOMPurify from "dompurify";
璃白.'s avatar
璃白. 已提交
64
export default {
璃白.'s avatar
璃白. 已提交
65
  components: { helpDoc, selectUser },
璃白.'s avatar
璃白. 已提交
66
  props: {
67 68 69 70
    id: {
      type: String,
      default: ""
    },
璃白.'s avatar
fix  
璃白. 已提交
71 72 73 74 75 76 77
    html: {
      type: String,
      default: ""
    },
    htmlMinHeight: {
      default: ""
    },
璃白.'s avatar
璃白. 已提交
78 79 80 81
    fullScreen: {
      type: Boolean,
      default: false
    },
82 83 84 85
    throttleTime: {
      type: Number,
      default: 1000
    },
璃白.'s avatar
璃白. 已提交
86 87 88 89
    isFocus: {
      type: Boolean,
      default: false
    },
璃白.'s avatar
璃白. 已提交
90 91 92 93
    disabled: {
      type: Boolean,
      default: false
    },
璃白.'s avatar
璃白. 已提交
94 95 96 97 98 99
    placeholder: {
      type: String,
      default: false
    },
    fileList: {
      type: Array,
璃白.'s avatar
璃白. 已提交
100
      default: () => []
璃白.'s avatar
璃白. 已提交
101 102
    },
    text: {
璃白.'s avatar
璃白. 已提交
103 104
      type: [String, Number],
      default: ""
璃白.'s avatar
璃白. 已提交
105
    },
106 107 108 109 110 111 112 113
    maxLength: {
      type: [String, Number],
      default: ""
    },
    rows: {
      type: [String, Number],
      default: ""
    },
璃白.'s avatar
璃白. 已提交
114 115 116 117
    height: {
      type: Number,
      default: 0
    },
璃白.'s avatar
璃白. 已提交
118 119
    selectionInfo: {
      type: Object,
璃白.'s avatar
璃白. 已提交
120
      default: () => {}
璃白.'s avatar
璃白. 已提交
121 122 123 124 125 126 127
    },
    formatType: {
      default: ""
    },
    showHelp: {
      type: Boolean,
      default: false
璃白.'s avatar
璃白. 已提交
128 129 130 131
    },
    userList: {
      type: Array,
      default: () => []
璃白.'s avatar
璃白. 已提交
132 133
    }
  },
134

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

      this.$emit("getFilteredTags", filteredTags);
263
      this.$emit("update:html", cleanHtml);
璃白.'s avatar
璃白. 已提交
264
      if (links.length) this.$emit("renderLinksHtml", { vDom, links });
璃白.'s avatar
fix  
璃白. 已提交
265
    },
266
    input() {
璃白.'s avatar
璃白. 已提交
267
      if (this.showSelectUser) this.handleQueryUser();
268 269 270
      this.$emit("update:textLength", this.textContent.length);
      this.emitText();
    },
璃白.'s avatar
璃白. 已提交
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    selectUser(user) {
      const originalText = this.textContent;
      const queryInfo = this.queryInfo;
      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
璃白. 已提交
294
      const startPosition = this.queryInfo.startPosition;
璃白.'s avatar
璃白. 已提交
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;
      }
璃白.'s avatar
璃白. 已提交
302

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

璃白.'s avatar
璃白. 已提交
459
  &.fullScreen {
460
    height: 100%;
璃白.'s avatar
璃白. 已提交
461 462
    textarea {
      font-size: 20px;
463 464
      max-height: 100%;
      overflow-y: auto;
璃白.'s avatar
璃白. 已提交
465 466
    }
  }
璃白.'s avatar
璃白. 已提交
467 468 469
  &.disabled {
    background: var(--md-editor-content-bg-color-disabled);
  }
470

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