loomio/loomio

View on GitHub
vue/src/components/lmo_textarea/md_editor.vue

Summary

Maintainability
Test Coverage
<script lang="js">
import { convertToHtml } from '@/shared/services/format_converter';
import { CommonMentioning, MdMentioning } from './mentioning';
import Records from '@/shared/services/records';
import FilesList from './files_list.vue';
import SuggestionList from './suggestion_list';
import Attaching from './attaching';

export default
{
  mixins: [CommonMentioning, MdMentioning, Attaching],
  props: {
    model: Object,
    field: String,
    label: String,
    placeholder: String,
    shouldReset: Boolean,
    maxLength: Number,
    autofocus: {
      type: Boolean,
      default: false
    }
  },

  components: {
    FilesList,
    SuggestionList
  },

  data() {
    return {preview: false};
  },

  watch: {
    shouldReset: 'reset'
  },

  methods: {
    reset() {
      this.preview = false;
      this.resetFiles();
    },

    convertToHtml() {
      convertToHtml(this.model, this.field);
      Records.users.saveExperience('html-editor.uses-markdown', false);
    },

    onPaste(event) {
      const items = Array.from(event.clipboardData.items);

      if (items.filter(item => item.getAsFile()).length === 0) { return; }

      event.preventDefault();
      this.handleUploads(items.map(item => {
        return new File([item.getAsFile()],
                 event.clipboardData.getData('text/plain') || Date.now(),
                 {lastModified: Date.now(), type: item.type});
      })
      );
    },

    handleUploads(files) {
      Array.from(files).forEach(file => {
        if ((/image/i).test(file.type)) {
          this.insertImage(file);
        } else {
          this.attachFile({file});
        }
      });
    },

    insertImage(file) {
      const name = file.name.replace(/[\W_]+/g, '') | 'file';

      const uploadingText = pct => `![uploading-${name}](${"*".repeat(parseInt(pct / 5))})`;

      const insertPlaceholder = text => {
        const beforeText = this.model[this.field].slice(0, this.textarea().selectionStart);
        const afterText = this.model[this.field].slice(this.textarea().selectionStart);
        this.model[this.field] = beforeText + "\n" + text + "\n" + afterText;
      };

      const updatePlaceholder = text => {
        this.model[this.field] = this.model[this.field].replace(new RegExp(`!\\[uploading-${name}\\]\\(\\**\\)`), text);
      };

      insertPlaceholder(uploadingText(0));

      return this.attachImageFile({
        file,
        onProgress: e => {
          updatePlaceholder(uploadingText(parseInt((e.loaded / e.total) * 100)));
        },

        onComplete: blob => {
          updatePlaceholder(`![${name}](${blob.preview_url})`);
        },

        onFailure: () => {
          updatePlaceholder(`![${name}](${this.$t('formatting.upload_failed')}`);
        }
      });
    },

    onDrop(event) {
      if (!event.dataTransfer || !event.dataTransfer.files || !event.dataTransfer.files.length) { return; }
      event.preventDefault();
      this.handleUploads(event.dataTransfer.files);
    },

    onDragOver(event) { return false; }
  },

  computed: {
    previewAction() {
      if (this.preview) { return 'common.action.edit'; } else { return 'common.action.preview'; }
    },
    previewIcon() {
      if (this.preview) { return 'mdi-pencil'; } else { return 'mdi-eye'; }
    }
  }
};

</script>

<template lang="pug">
div(style="position: relative")
  v-textarea(
    v-if="!preview"
    filled
    ref="field"
    v-model="model[field]"
    :placeholder="placeholder"
    :label="label"
    @keyup="onKeyUp"
    @keydown="onKeyDown"
    @paste="onPaste"
    @drop="onDrop"
    @dragover.prevent="onDragOver")
  formatted-text(v-if="preview" :model="model" :column="field")
  v-sheet.pa-4.my-4.poll-common-outcome-panel(v-if="preview && model[field].trim().length == 0" color="primary lighten-5" elevation="2")
    p(v-t="'common.empty'")

  v-layout.menubar(align-center :aria-label="$t('formatting.formatting_tools')")
    v-btn(icon @click='$refs.filesField.click()' :title="$t('formatting.attach')")
      common-icon(name="mdi-paperclip")
    v-btn(text x-small @click="convertToHtml(model, field)" v-t="'formatting.wysiwyg'")
    v-spacer
    v-btn.mr-4(text x-small @click="preview = !preview" v-t="previewAction")

    slot(name="actions")
  suggestion-list(:query="query" :loading="fetchingMentions" :mentionable="mentionable" :positionStyles="suggestionListStyles" :navigatedUserIndex="navigatedUserIndex" showUsername @select-user="selectUser")

  files-list(:files="files" v-on:removeFile="removeFile")

  form(style="display: block" @change="fileSelected")
    input.d-none(ref="filesField" type="file" name="files" multiple=true)
</template>