Skip to content

02 Text Editors - v_textarea...

v_textarea

v_textarea is a component with

  • file upload support
  • markdown support
  • value
  • readonly
  • tab - -4 default edit ,-3 preview
  • advanced - default true
  • required
  • valuetype - default md
  • css - default xd-white
  • placeholder default comment
JavaScript
Vue.component('v_textarea', {
  template: `
<div :class="css" class="xd-border v_textarea" :data-component="$options.name">

  <div class="xd-container xd-gap print-hide" style="justify-content:right;">
    <a :class="{'xd-strong': local_tab==-4}" class="xd-button  xd-small " @click="local_tab= -4" v-if="readonly===false">{{$t('edit')}}</a>

    <a :class="{'xd-strong': local_tab==-3}" v-if="readonly===false" class="xd-button   xd-small " @click="renderPreview">{{$t('view')}}</a>
    <a class="xd--target xd-button   xd-small" v-if="readonly===false && advanced"
      href="https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax" target="_blank"
      data-ga-click="Markdown Toolbar, click, help" rel="noreferrer"><span role="tooltip" aria-label="Styling with Markdown is supported"
        class="Tooltip__TooltipBase-sc-17tf59c-0 cXRxE tooltipped-nw"><svg aria-hidden="true" focusable="false" role="img" class="Octicon-sc-9kayk9-0 CmyWV" viewBox="0 0 16 16" width="16" height="16"
          fill="currentColor" style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;">
          <path
            d="M14.85 3c.63 0 1.15.52 1.14 1.15v7.7c0 .63-.51 1.15-1.15 1.15H1.15C.52 13 0 12.48 0 11.84V4.15C0 3.52.52 3 1.15 3ZM9 11V5H7L5.5 7 4 5H2v6h2V8l1.5 1.92L7 8v3Zm2.99.5L14.5 8H13V5h-2v3H9.5Z">
          </path>
        </svg></span></a>

    <select tabindex="-1" v-model="selected_paste_mode" class="xd-input  xd-small" v-if="readonly===false && advanced">
      <option v-for="option in paste_mode_options" v-bind:value="option.value">
        {{ option.text }}
      </option>
    </select>

  </div>
  <div style="clear:both"></div>

  <div v-show="local_tab===-4">
    <textarea tabindex="-1" class="xd-box" @blur="$emit('blur')" :class="css" @keydown.tab.prevent="tabber($event)" style="border:none;font-family: monospace ;" :required="required ? true : false"
      :rows="rows" @paste="onPaste" ref="el" @input="update" :placeholder="$t(placeholder)">{{content}}</textarea>

    <div class="xd-padding xd-hide-print">
      <!--UPLOAD-->
      <form enctype="multipart/form-data" novalidate v-if="isInitial || isSaving">

        <input type="file" tabindex="-1" ref="iinput" multiple :name="files.uploadFieldName" :disabled="isSaving"
          @change="filesChange($event.target.name, $event.target.files); files.fileCount = $event.target.files.length" class="xd-small">
        <p v-if="isSaving">
          Uploading {{ files.fileCount }} files...
        </p>

      </form>
      <!--SUCCESS-->
      <div v-if="isSuccess">
        <h3>
          Upload OK
          <a href="javascript:void(0)" @click="reset()">click to Upload next file</a>
        </h3>
        <ul class="list-unstyled">
          <li v-for="item in files.uploadedFiles">
            <img :src="item.url" class="img-responsive img-thumbnail" :alt="item.originalName">
          </li>
        </ul>
      </div>
      <!--##################################### UPLOAD FAILED ##########################-->
      <div v-if="isFailed">
        <h3>Upload FAILED <a href="javascript:void(0)" @click="reset()">click to Try again</a></h3>

        <pre>{{ files.uploadError }}</pre>
      </div>
    </div>
  </div>
  <div v-if="local_tab===-3" v-html="preview" class="xd-box" :class="css">

  </div>
</div>`,
  props: {
    value: { type: String },
    readonly: { type: Boolean, default: false },// hide edits
    tab: { type: Number, default: -4 }, //-4 edit
    advanced: { type: Boolean, default: true },
    required: { type: Boolean, default: false },
    valuetype: { type: String, default: '.md' },
    css: { type: String, default: 'xd-white' },
    placeholder: { type: String, default: 'comment' },
  },
  data() {
    return {
      local_tab: this.tab,
      content: this.value,  //initial value
      maxwidth: 1200,       //max width of a images 1200
      selected_paste_mode: '',
      paste_mode_options:
        [{ text: 'Paste Text', value: '' },
        { text: 'Paste Word', value: '/api/core/system/officehtml' },
        ],

      preview: 'Working ...',
      files: {
        uploadedFiles: [],
        uploadError: null,
        files: [],
        fileCount: null,
        uploadFieldName: 'upload',
        cursorLocation: null  //lokacja kursora gdzie wstawic
      },
      currentStatus: null
    };
  },

  methods: {
    insert(main_string, ins_string, pos) {
      if (typeof (pos) === "undefined") {
        pos = 0;
      }
      if (!ins_string) {
        ins_string = '';
      }
      if (!main_string) {
        main_string = '';
      }

      if (main_string === '' || main_string === null)
        return ins_string;
      else
        return main_string.slice(0, pos) + '\n' + ins_string + main_string.slice(pos);
    },
    tabber(event) {
      var text;
      this.value ? text = this.value : text = "";

      originalSelectionStart = event.target.selectionStart,
        textStart = text.slice(0, originalSelectionStart),
        textEnd = text.slice(originalSelectionStart);

      var result = `${textStart}\t${textEnd}`
      event.target.value = result // required to make the cursor stay in place.
      event.target.selectionEnd = event.target.selectionStart = originalSelectionStart + 1
      this.$emit('input', event.target.value);
    },
    onPaste(e) {
      if (e.clipboardData.types.indexOf('text/html') !== -1 && this.selected_paste_mode.indexOf('/api') !== -1) {
        e.preventDefault();

        this.axpost(this.selected_paste_mode, { painput: e.clipboardData.getData('text/html') }, false, true).then((response) => {

          e.target.value = this.insert(e.target.value, response.data.paoutput, e.target.selectionStart);
          e.target.dispatchEvent(new Event('input'));

        });

      }

      else if (e.clipboardData.files.length > 0) {
        e.preventDefault();
        this.$refs.iinput.files = e.clipboardData.files;
        this.$refs.iinput.dispatchEvent(new Event('change'));
        this.reset();

      }

    },
    update(event) {
      // this.content = 'f'; //event.target.value;
      this.$emit('input', event.target.value);
      // this.content = event.target.value; // pobranie z inputa
      this.files.cursorLocation = event.target.selectionStart;
    },

    renderPreview() {

      this.local_tab = -3;

      if (this.value) {
        this.axpost('/api/core/system/preview', { painput: this.value, patype: this.valuetype }, { skipNotification: true }).then((response) => {

          this.preview = response.data.paoutput;
          // this.$nextTick(function () {
          //   Prism.highlightAll();
          // });
        })
      }
      else {
        this.preview = '-'
      }

    },
    //FILE UPLOAD
    reset() {
      // reset form to initial state
      this.currentStatus = STATUS_INITIAL;
      this.uploadedFiles = [];
      this.uploadError = null;
    },
    save(formData) {
      // upload data to the server
      this.currentStatus = STATUS_SAVING;

      let link = this.$root.sys.api + '/api/core/files/';

      const options = {
        headers: {
          "content-type": "multipart/form-data",
          "accept": "*/*"
        }
      }
      this.axpost(link, formData, options).then((response) => {
        this.currentStatus = STATUS_SUCCESS;
        //zamiana nazw
        Object.entries(response.data.files).forEach(entry => {
          const [k, v] = entry;
          let oldv = '[Uploading... ' + k + ']';
          let newv = ""
          if (this.endsWithAny(this.images_arr(), v))
            newv = '[' + k + '](' + link + v + '?w=' + this.maxwidth + ')';
          else
            newv = '[' + k + '](' + link + v + ')';

          this.$refs.el.value = this.$refs.el.value.replace(oldv, newv);
          this.content = this.$refs.el.value;
        });

        this.$emit('input', this.$refs.el.value);
      }).catch((response) => {
        this.currentStatus = STATUS_FAILED;
      })


    },
    filesChange(fieldName, fileList) {
      // handle file changes
      const formData = new FormData();
      if (!fileList.length) return;

      // append the files to FormData
      Array
        .from(Array(fileList.length).keys())
        .map((x) => {
          formData.append(fieldName, fileList[x], fileList[x].name);
          if (this.endsWithAny(this.images_arr(), fileList[x].name) || this.endsWithAny(this.video_arr(), fileList[x].name)) {
            this.$refs.el.value = this.insert(this.$refs.el.value, '![Uploading... ' + fileList[x].name + ']\n', this.$refs.el.selectionStart);
          }
          else {
            this.$refs.el.value = this.insert(this.$refs.el.value, '[Uploading... ' + fileList[x].name + ']\n', this.$refs.el.selectionStart);
          }

        });
      // save it
      this.save(formData);
    }
  },
  computed: {

    rows() { //number of rows to display
      if (!this.value)
        return 2;

      x = this.value.split('\n').length

      if (x < 5)
        return 5;
      else if (x > 25)
        return 25;
      else
        return x + 1;

    },

    isInitial() {
      return this.currentStatus === STATUS_INITIAL;
    },
    isSaving() {
      return this.currentStatus === STATUS_SAVING;
    },
    isSuccess() {
      return this.currentStatus === STATUS_SUCCESS;
    },
    isFailed() {
      return this.currentStatus === STATUS_FAILED;
    }
  },
  mounted() {


    //jest text lub preview
    if (this.value || this.local_tab == -3)
      this.renderPreview();
    else
      this.preview = '';

    this.reset();
  },
});
HTML
<v_textarea  v-model="value" style="border:0" :readonly="true" v-model="api.description"></v_textarea>

v_editable_div

Simple editable div

Props:

  • slot

Events:

  • @input
  • @updated
JavaScript
Vue.component('v_editable_div', {
  template: `
  <div  contenteditable="true" v-on="listeners" @blur="onBlur()" >
    <slot></slot>
  </div>
`,
  data() {
    return {
      oldValue: ''
    }
  },
  computed: {
    listeners() {
      return { ...this.$listeners, input: this.onInput };
    },
  },
  methods: {
    onInput(e) {
      this.$emit('input', e.target.innerHTML);
    },
    onBlur() {
      if (this.oldValue != this.$el.innerHTML) {
        this.$emit('updated', this.$el.innerHTML);
      }
    }
  },
  watch: {
  },
  mounted() {
    this.oldValue = this.$el.innerHTML;
  }
});
HTML
<v_editable_div :content="" @update=""></v_editable_div>

codemirror

Source code editor with syntax highlighting

JavaScript
var selected= this.codemirror.getSelection();  // get selection
this.codemirror.replaceSelection( ); // replace selection
JavaScript
data: function () {
        return {
            cmOption: {
                name: "gfm",
                connect: 'align',
                mode:"text/x-markdown",
              //  theme: 'base16-dark',
                theme: "default",
                lineNumbers: true,
                collapseIdentical: false
            },
            }}
HTML
<codemirror v-model="content" :options="cmOption" </codemirror>