<template>
  <div class="c-inline-editor">
    <!-- label start -->
    <div class="c-inline-editor__label" v-if="label">
      <span class="u-text--light">{{ label }}</span>
    </div>
    <!-- label end -->

    <!-- editor section start -->
    <div class="l-flex-grid--no-wrap">
      <component
        :is="tag"
        ref="tag"
        class="c-inline-editor__text"
        :class="[{ 'c-inline-editor__text--editor-open': editorOpen }, tagClasses]">

        <!-- display mode start -->
        <div
          v-if="!editorOpen"
          class="c-inline-editor__display-container">

          <!--
              retains line-height and prevents height jumps when the text is empty or if it goes from empty to non-empty.
              more about the problem this solves: https://stackoverflow.com/a/34703809
          -->
          <div class="c-inline-editor__display-resizer"></div>

          <div class="c-inline-editor__display-text">{{ value }}</div>

          <div
            class="c-inline-editor__toggle-edit"
            v-if="!readOnly"
            @click="openEditor">
            <fa-icon
              icon="pen"
              class="c-inline-editor__toggle-edit-icon">
            </fa-icon>
          </div>
        </div>
        <!-- display mode end -->

        <!-- editor mode start -->
        <!--
          v-show is important here - the logic needs to calculate the height
          and apply it to the input before it is shown. Don't switch to v-if.
        -->
        <el-form
          v-show="editorOpen"
          :scope="formKey"
          class="c-inline-editor__form"
          @submit.prevent.native="saveInput">
          <el-form-item
            :scope="formKey"
            class="inline-form-item c-input--has-loader l-flex-grid--no-wrap u-mb0"
            :error-bag="$errors"
            :prop="name"
            v-clickoutside="closeEditor"
            :class="{ 'inline-form-item__active': editorOpen }">

            <el-input
              type="text"
              :name="name"
              ref="editorInput"
              :data-vv-scope="formKey"
              :value="value"
              v-validate="validations"
              :placeholder="placeholder"
              @input="updateValue"
              @keydown.native.esc.stop="closeEditor"
              :disabled="inputDisabled"
              v-loading="loading">
            </el-input>

            <!-- save button start -->
            <div class="c-inline-editor__save-button-wrapper">
              <el-button
                @click="saveInput"
                type="primary"
                class="c-inline-editor__save-button"
                :disabled="submitDisabled"
                size="mini">Save</el-button>
            </div>
            <!-- save button end -->
          </el-form-item>
        </el-form>
        <!-- editor mode end -->

      </component>
    </div>
    <!-- editor section end -->
  </div>
</template>

<script>
import { uniqueId } from 'lodash';
import Clickoutside from 'element-ui/src/utils/clickoutside';

export default {
  inject: ['$validator'],
  directives: { Clickoutside },
  name: 'ElInlineEditor',
  props: {
    tag: {
      type: String,
      required: true,
      validator(value) {
        return ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div'].includes(value);
      }
    },
    value: { type: String, required: true },
    name: { type: String, required: true },
    label: { type: String, required: false },
    tagClasses: { type: String, required: false },
    placeholder: { type: String, required: false },
    validations: { type: [String, Object], required: false },
    loading: { type: Boolean, required: false },
    readOnly: { type: Boolean, required: false, default: false }
  },
  data() {
    return {
      editorOpen: false,
      submitDisabled: true,
      inputDisabled: false,

      formKey: uniqueId('inline-editor-')
    };
  },
  methods: {
    updateValue(value) {
      this.submitDisabled = false;
      this.$emit('input', value);
    },
    /**
     * Do not call this function for simply closing the editor (eg: when loading is set to false or when the user presses enter).
     * The function emits a `close` event and should only be used for cases where the user's intent is to not save or update the value.
     */
    closeEditor() {
      if (this.loading || !this.editorOpen) return;
      if (this.validations) this.$validator.reset({ scope: this.formKey });

      this.editorOpen = false;
      this.submitDisabled = true;
      this.$emit('close');
    },
    async openEditor() {
      await this.normalizeInputHeight();

      this.editorOpen = !this.editorOpen;
      await this.$nextTick();

      this.$refs.editorInput.focus();
    },
    async saveInput() {
      const isValid = await (this.validations ? this.$validator.validateAll(this.formKey) : Promise.resolve(true));

      if (isValid) {
        this.$emit('submit', this.value);
        this.afterSubmitCleanUp();
      }
    },
    afterSubmitCleanUp() {
      this.editorOpen = false;
      this.inputDisabled = false;
      this.submitDisabled = true;
    },
    /**
     * Sets the input's height to match the content's.
     * @returns {Promise} a promise that resolves once the height is calculated and applied to the input.
     * Make sure to show the input once this step is done to avoid any jumps in the UI.
     */
    normalizeInputHeight() {
      const { editorInput, tag } = this.$refs;

      return new Promise(resolve => {
        // wait for dom updates
        setTimeout(() => {
          if (!editorInput || !tag) {
            resolve();
            return;
          }
          editorInput.$el.style.height = `${this.$refs.tag.offsetHeight}px`;
          resolve();
        }, 0);
      });
    }
  },
  mounted() {
    this.normalizeInputHeight();
  },
  watch: {
    /**
     * Handles the case where the `value` starts with an empty string but then gets updated after a while
     * (the kind of flow that's very common with vuex)
    */
    value: {
      immediate: true,
      handler(newVal, oldVal) {
        if (!oldVal && newVal) {
          this.normalizeInputHeight();
        }
      }
    },
    loading: {
      immediate: true,
      handler(isLoading) {
        if (isLoading) {
          this.editorOpen = true;
          this.inputDisabled = true;
          this.submitDisabled = true;
        } else {
          this.afterSubmitCleanUp();
        }
      }
    }
  }
};
</script>
