<!--
Complex animated column editor for the table edit page
-->

<template>
  <div>
    <input type="hidden" :name="name" :value="JSON.stringify(columns)" v-if="newTable">
    <table :class="[{'table': !newTable}, {'new-table': newTable}, {'mt-3': !newTable}, 'table-narrow', 'table-sm', 'table-fixed', 'td-va-middle']">
      <thead>
        <tr>
          <th v-if="sortable"></th>
          <th :style="tdWidthStyle('name')">Name</th>
          <th :style="tdWidthStyle('type')">Type</th>
          <th :style="tdWidthStyle('title')">Title</th>
          <th></th>
        </tr>
      </thead>
      <transition-group name="col-list" tag="tbody" ref="col-list">
        <tr v-for="(col, i) in columns" :key="col.id" :ref="`col${i}`" :class="{dragging: col._dragging, 'text-success': col._new}">
          <td v-if="sortable">
            <span class="btn-group">
              <button type="button" class="btn btn-outline-secondary drag-btn"
                      @keyup.up="move(i, -1)"
                      @keyup.down="move(i, 1)"
                      :ref="`col${i}-sort`"
                      :style="{visibility: (columns.length > 1) ? 'visible' : 'hidden'}"
                      @mousedown="beginDrag(i, $event)">
                <v-icon class="fa-bars" alt="Drag" />
              </button><!--
           --><button type="button" :class="['btn', 'btn-outline-secondary', {disabled: i==0}]" v-if="manualSort"
                 @click.prevent="move(i, -1)">
                <v-icon class="fa-chevron-up" alt="Move Up" />
              </button><!--
           --><button type="button" :class="['btn', 'btn-outline-secondary', {disabled: i == (columns.length-1)}]"  v-if="manualSort"
                 @click.prevent="move(i, 1)">
                <v-icon class="fa-chevron-down" alt="Move Down" />
              </button>
            </span>
          </td>

          <template v-if="col._editing || newTable">
            <!-- Editable cells -->
            <td :style="tdWidthStyle('name')">
              <input v-model="col.name"
                     class="form-control"
                     type="text">
            </td>

            <td :style="tdWidthStyle('type')">
              <select v-model="col.type"
                      class="form-control custom-select">
                <option v-for="t in colTypes" :value="t">{{t}}</option>
              </select>
            </td>

            <td :style="tdWidthStyle('title')">
              <input v-model="col.title"
                     class="form-control"
                     type="text">
            </td>
          </template>

          <template v-else>
            <!-- Value fields -->
            <td v-for='cell in ["name", "type", "title"]'>
              <span class="text-danger strike" title="Original value" v-if="isChanged(col, cell)">{{col._orig[cell]}}</span>
              <span>{{ col[cell] }}</span>
              <v-icon v-if="isChanged(col, cell)"
                      @click="revertCell(col, cell)"
                      class="fa-undo text-danger pointer"
                      alt="Revert Change" />
            </td>
          </template>

          <td class="text-nowrap"><!--
              Save button
         --><a href="" :class="['mr-1','btn','btn-outline-secondary',{'disabled': col._remove}]"
               v-if="!newTable"
               @click.prevent="toggleColEditing(col)">
              <v-icon :class="col._editing ? 'fa-save' : 'fa-pencil'"
                      :alt="col._editing ? 'Save' : 'Edit'" />
            </a><!--
              Delete button
         --><button type="button" :class="delBtnClass(col)"
               @click.prevent="toggleColDelete(col)">
              <v-icon :class="col._remove ? 'fa-undo' :  'fa-trash-o'"
                      :alt="col._remove ? 'Undo Remove' : 'Remove'" />
            </button><!--
              Add button
         --><button type="button" :class="['x-add-btn', 'btn', 'btn-outline-secondary']"
               v-if="i === columns.length - 1"
               @click.prevent="addCol()">
              <v-icon class="fa-plus" alt="Add Column" />
            </button>
          </td>
        </tr>
      </transition-group>
    </table>
  </div>
</template>

<style lang="scss" scoped>
  @import "base";

  @media screen and (min-width: 625px) {
    table.new-table {
      margin-left: -53px;
    }
  }

  td, th {
    @include pr(1);
    @include py(1);
  }

  th {
    border-top: 0 none;
  }

  tr.dragging {
    position: relative;
    z-index: 1;
    //background-color: $body-bg;
  }
  tr.dragging .btn {
    background-color: $body-bg; // no see-through buttons
  }
  tr.dragging .drag-btn {
    // fake hover
    background-color: $secondary;
    color: color-yiq($secondary);
    box-shadow: $btn-box-shadow, 0 0 0 $btn-focus-width rgba($secondary, .5);
  }

  .col-list-enter-active {
    transition: all .3s cubic-bezier(.2, .3, 0, 1);
  }

  .col-list-enter {
    opacity: 0;
    transform: translateY(100%);
  }
  .col-list-leave {
    // approximate position of delete button
    clip-path: circle(calc(100% + 5em) at calc(100% - 4em) 50%);
  }

  .col-list-leave-active .delete-btn {
    animation: col-list-leave-delete-btn .3s forwards;
  }
  .col-list-leave-active {
    transition: all .3s cubic-bezier(.4, .1, .6, .9);
  }
  .col-list-leave-to {
    clip-path: circle(0 at calc(100% - 4em) 50%);
  }

  @keyframes col-list-leave-delete-btn {
    0% {
      transition-timing-function: cubic-bezier(.2, .4, .6, .9);
    }
    50% {
      transition-timing-function: cubic-bezier(.5, 0, .8, .5);
      transform: scale(1.1);
      opacity: 1;
    }
    100% {
      transform: scale(0);
      opacity: 0;
    }
  }

  .col-list-move {
    transition: transform .3s cubic-bezier(.2, .3, 0, 1);
  }
</style>

<script>
import { query } from './table-editor-utils'

export default {
  props: {
    route:      { type: String,  default: null },
    sortable:   { type: Boolean, default: true },
    manualSort: { type: Boolean, default: false },
    newTable:   { type: Boolean, default: false },
    name:       { type: String,  required: true },
    xColumns:   { type: Array,   required: true },
  },
  data () {
    return {
      newColNum: 0,
      columns: this.xColumns,
      colTypes: ['string', 'int', 'float', 'bool'],
      debouncedSortUpdate: _.debounce(() => this.submitColOrder(), 350)
    }
  },
  methods: {
    /** Send a query to the server */
    query (data, sucfn, erfn) {
      query(this.route, data, sucfn, erfn)
    },

    submitColOrder() {
      let ids = this.columns.map((c) => c.id)
      this.query ({
        action: 'col.sort',
        order: ids,
      })
    },

    colPos(col) {
      for (let n = 0; n < this.columns.length; n++) {
        if (this.columns[n].id == col.id) return n;
      }
      throw `Col ${col.id} not found in list`;
    },

    /** Save a change */
    submitColChange (col) {
      if (_.isUndefined(col)) return
      let n = this.colPos(col)

      this.query({
        action: 'col.update',
        data: col
      }, (resp) => {
        this.$set(this.columns, n, resp.data)
      }, (er) => {
        if (!_.isUndefined(er.errors)) {
          this.$set(this.columns[n], '_errors', er.errors)
        }
      })
    },

    /** Add a column at the end */
    addCol () {
      if (this.newTable) {
        this.columns.push({
          id: '_' + (this.newColNum++),
          name: '',
          type: 'string',
          title: '',
        })
      } else {
        this.query({
          action: 'col.add',
        }, (resp) => {
          this.columns.push(resp.data)
        })
      }
    },

    /** Move a column (dir is positive or negative) */
    move (pos1, dir) {
      let pos2 = pos1 + dir

      // clamp
      if (pos2 < 0) {
        pos2 = 0
      } else if (pos2 >= this.columns.length) {
        pos2 = this.columns.length - 1
      }

      let columns = this.columns
      let col1 = columns.splice(pos1, 1)[0] // unwrap returned 1-element array
      columns.splice(pos2, 0, col1)
      this.columns = columns

      // Put focus on the new field
      // For some reason, it loses it when moving down
      setTimeout(() => this.$refs[`col${pos2}-sort`][0].focus(), 0)

      this.debouncedSortUpdate();
    },

    /** User started dragging a column */
    beginDrag (i, evt) {
      const column = this.columns[i]
      column._dragging = true
      this.$set(this.columns, i, column) // notify vue

      let currentIndex = i

      const dragMoveListener = e => {
        let cursorIndex = 0

        // find cursor index by going through the list and adding the li
        // heights (can’t use their positions because they may be animating at
        // that moment)
        let accumY = this.$refs['col-list'].$el.getBoundingClientRect().top
        for (let i = 0; i < this.columns.length; i++) {
          if (e.clientY > accumY) {
            cursorIndex = i
          }
          accumY += $(this.$refs['col' + i]).outerHeight(true)
        }

        if (cursorIndex !== currentIndex) {
          this.move(currentIndex, cursorIndex - currentIndex)
          currentIndex = cursorIndex
        }
      }

      const dragEndListener = e => {
        column._dragging = false
        this.$set(this.columns, currentIndex, column) // notify vue

        $(window).off('mousemove', dragMoveListener)
        $(window).off('mouseup', dragEndListener)
      }

      $(window).on('mousemove', dragMoveListener)
      $(window).on('mouseup', dragEndListener)
    },

    /** Compute classes for the column's delete button */
    delBtnClass(col) {
      return [
        'mr-1',
        'btn',
        'btn-outline-danger',
        'delete-btn',
        {'active': col._remove},
        {disabled: this.newTable && this.columns.length === 1}
      ]
    },

    /** Delete / undelete a column; New columns vanish. */
    toggleColDelete(col) {
      if (_.isUndefined(col)) return
      let n = this.colPos(col)

      if (this.newTable) {
        if (this.columns.length == 1) return; // can't delete the last col

        // hard delete
        this.columns.splice(n, 1)
      } else {
        let remove = !col._remove

        this.query({
          action: remove ? 'col.remove' : 'col.restore',
          id: col.id
        }, (resp) => {
          // if response is null, this was a New col
          // and it was discarded without a way back - hard drop
          if (_.isEmpty(resp.data)) {
            this.$delete(this.columns, n)
          }
          else {
            this.$set(this.columns, n, resp.data)
          }
        })
      }
    },

    /** Toggle editing state - edit or save */
    toggleColEditing (col) {
      if (col._remove) return false // can't edit col marked for removal
      let editing = !col._editing

      let n = this.colPos(col)

      if (!editing) {
        this.submitColChange(col)
      } else {
        this.$set(this.columns[n], '_editing', true)
      }
    },

    /** Test if a value cell is changed */
    isChanged (col, cell) {
      return col._changed && col._changed.indexOf(cell) > -1
    },

    /** Revert a value cell */
    revertCell (col, cell) {
      this.submitColChange(_.merge({}, col, {[cell]: col._orig[cell]}))
    },

    /** compute styles for a field cell */
    tdWidthStyle(cell) {
      let w = 10;
      if (cell == 'name') w = '14'
      if (cell == 'type') w = '12'
      if (cell == 'title') w = '14'

      return {width: `${w}rem`};
    }
  }
}
</script>
