<template>
  <div class="mutable-list-wrapper">
    <draggable
      class="mutable-list-draggable"
      v-model="sortList"
      tag="div"
      v-bind="dragOptions"
      @start="drag = true"
      @end="drag = false"
    >
      <transition-group
        tag="ul"
        :name="!drag ? 'mutable-list' : null"
        class="mutable-list"
        :class="[`is-${type}`, { 'is-sort': allowSort }]"
      >
        <li
          class="mutable-list-item"
          v-for="(item, index) in value"
          :key="getItemKey(item, index, value)"
        >
          <slot
            name="item"
            v-bind="{
              item: item,
              index: index,
              handleRemove: () => handleRemove(index),
              showRemove: showRemove(index),
              handleAdd,
              showAdd: showAdd(index),
              handleCurrentChange: () => handleCurrentChange(item, index),
              label: getItemLabel(item, index, value),
            }"
          >
            <el-tag
              v-if="type === 'tag'"
              class="tag"
              size="medium"
              type="primary"
              :effect="currentItem === item ? 'dark' : 'plain'"
              :closable="showRemove(index)"
              @click="handleCurrentChange(item, index)"
              @close="handleRemove(index)"
            >
              {{ getItemLabel(item, index, value) }}
            </el-tag>
            <span
              @click="handleCurrentChange(item, index)"
              :class="{ 'is-current': current === item }"
              v-else
            >
              {{ getItemLabel(item, index, value) }}
            </span>
          </slot>

          <div
            class="buttons"
            v-if="(type !== 'tag' && showRemove(index)) || showAdd(index)"
          >
            <slot
              name="actions"
              v-bind="{
                item: item,
                index: index,
                handleRemove: () => handleRemove(index),
                showRemove: type !== 'tag' && showRemove(index),
                handleAdd,
                showAdd: showAdd(index),
              }"
            >
              <el-button
                type="text"
                class="button-minus"
                @click="handleRemove(index)"
                v-if="type !== 'tag' && showRemove(index)"
              />
              <el-button
                type="text"
                class="button-plus"
                @click="handleAdd"
                v-if="showAdd(index)"
              />
            </slot>
          </div>
        </li>
      </transition-group>
    </draggable>
  </div>
</template>

<script>
import draggable from 'vuedraggable'
import isFunction from 'lodash/isFunction'
import get from 'lodash/get'

export default {
  name: 'MutableList',
  components: {
    draggable,
  },
  props: {
    // 是否允许排序
    allowSort: {
      type: Boolean,
      default: false,
    },
    // 数据列表
    value: {
      type: Array,
      default: () => [],
    },
    // 类型，tag将会渲染tag标签，list需要用户自定义渲染
    type: {
      type: String,
      default: 'list',
      validator: val => {
        return ['tag', 'list'].includes(val)
      },
    },
    // 最小列表项数量，默认为0
    minNumber: {
      type: Number,
      default: 0,
    },
    // 最大列表项数量，默认为正无穷
    maxNumber: {
      type: Number,
      default: Infinity,
    },
    // 项目名称，接收属性路径或者一个生成函数
    itemLabel: {
      type: [String, Function],
      default: 'name',
    },
    // 列表项目key，接收属性路径或者一个生成函数,默认使用索引
    itemKey: {
      type: [String, Function],
      default: () => (item, index, value) => index,
    },
    // 生成列表项目
    generateItem: {
      // 允许任意类型,暂不支持promise
      default: () => {},
      validator: val => true,
    },
    // 移除前执行函数：
    // 如果返回falsy,则不执行移除；
    // 如果返回promise，列表项将等待promise状态resolve之后执行移除
    beforeRemove: {
      type: Function,
      default: undefined,
    },
    // 自定义移除处理函数
    customRemove: {
      type: Function,
      default: undefined,
    },
    // 移除后执行函数
    afterRemove: {
      type: Function,
      default: undefined,
    },
    // 新增前执行函数：
    // 如果返回falsy,则不执行新增；
    // 如果返回promise，列表项将等待promise状态resolve之后执行新增
    beforeAdd: {
      type: Function,
      default: undefined,
    },
    // 自定义添加处理函数
    customAdd: {
      type: Function,
      default: undefined,
    },
    // 新增后执行函数
    afterAdd: {
      type: Function,
      default: undefined,
    },
    // 当前项
    current: {
      default: null,
      validator: val => true,
    },
    // 自定义是否显示移除按钮
    customShowRemove: {
      type: Function,
      default: undefined,
    },
    // 自定义是否显示添加按钮
    customShowAdd: {
      type: Function,
      default: undefined,
    },
  },
  data() {
    return {
      drag: false,
      currentItem: null,
      sortList: [],
    }
  },
  computed: {
    dragOptions() {
      return {
        animation: 200,
        group: 'description',
        disabled: !this.allowSort,
        ghostClass: 'ghost',
      }
    },
  },
  watch: {
    current(val) {
      this.currentItem = val
    },
    value: {
      immediate: true,
      handler(val) {
        this.sortList = val
      },
    },
    sortList(val, oldVal) {
      this.$emit('input', val)
      this.$emit('sort', val, oldVal)
    },
  },
  methods: {
    // 获取label
    getItemLabel(item, index, list) {
      return isFunction(this.itemLabel)
        ? this.itemLabel(item, index, list)
        : get(item, this.itemLabel)
    },

    // 获取key
    getItemKey(item, index, list) {
      return isFunction(this.itemKey)
        ? this.itemKey(item, index, list)
        : get(item, this.itemKey)
    },

    // 是否显示移除按钮
    showRemove(index) {
      return (
        this.value.length > this.minNumber &&
        (this.customShowRemove ? this.customShowRemove(index) : true)
      )
    },

    // 是否显示添加按钮
    showAdd(index) {
      const length = this.value.length
      return (
        index === length - 1 &&
        length < this.maxNumber &&
        (this.customShowAdd ? this.customShowAdd(index) : true)
      )
    },

    // 生成数据项
    getItem(list) {
      return isFunction(this.generateItem)
        ? this.generateItem(list)
        : this.generateItem
    },

    // 处理当前项改变
    handleCurrentChange(item, index) {
      if (item === this.currentItem) {
        return
      }
      this.$emit('update:current', item)
      this.$emit('current-change', item, index)
    },

    // 执行移除操作
    doRemove(index) {
      const newList = [...this.value]
      const [item] = newList.splice(index, 1)
      this.$emit('remove', item, index)
      this.$emit('input', newList)
    },

    // 执行新增操作
    doAdd(item) {
      const newList = [...this.value, item]
      this.$emit('add', item)
      this.$emit('input', newList)
    },

    // 处理移除
    handleRemove(index) {
      if (this.value.length <= this.minNumber) {
        return
      }

      const removeItem = this.value[index]

      try {
        // 移除操作队列
        const removeActionQueue = []

        this.beforeRemove &&
          removeActionQueue.push(() =>
            this.beforeRemove(removeItem, index, this.value)
          )

        removeActionQueue.push(beforeValue => {
          if (this.customRemove) {
            return this.customRemove(removeItem, index, this.value, beforeValue)
          } else {
            return this.doRemove(index)
          }
        })

        this.afterRemove &&
          removeActionQueue.push(removeValue =>
            this.afterRemove(removeItem, index, this.value, removeValue)
          )

        return removeActionQueue
          .reduce(
            (promiseChain, item) => promiseChain.then(item),
            Promise.resolve()
          )
          .catch(error => {
            console.log(error)
          })
      } catch (error) {
        console.log(error)
      }
    },

    // 处理新增
    async handleAdd() {
      if (this.value.length >= this.maxNumber) {
        return
      }
      try {
        const item = this.getItem(this.value)

        // 添加操作队列
        const addActionQueue = []

        this.beforeAdd && addActionQueue.push(() => this.beforeAdd(this.value))

        addActionQueue.push(async beforeValue => {
          if (this.customAdd) {
            return this.customAdd(item, this.value, beforeValue)
          } else {
            return this.doAdd(item)
          }
        })

        this.afterAdd &&
          addActionQueue.push(addValue =>
            this.afterAdd(item, this.value, addValue)
          )

        return addActionQueue
          .reduce(
            (promiseChain, item) => promiseChain.then(item),
            Promise.resolve()
          )
          .catch(error => {
            console.log(error)
          })
      } catch (error) {
        console.log(error)
      }
    },
  },
}
</script>

<style lang="scss" scoped>
.mutable-list {
  display: flex;
  list-style-type: none;
  flex-direction: column;
  &.is-tag {
    display: inline-flex;
    flex-direction: row;
    .mutable-list-item + .mutable-list-item {
      margin: 0;
      margin-left: 10px;
    }
  }

  &.is-sort {
    .mutable-list-item {
      cursor: move;
    }
  }

  &-enter,
  &-leave-to {
    opacity: 0;
    transform: translate3d(0, 30, 0);
  }
  &-enter.is-tag,
  &-leave-to.is-tag {
    opacity: 0;
    transform: translate3d(30, 0, 0);
  }

  &-enter-active,
  &-leave-active {
    transition: all 0.5s ease-in-out;
  }

  &-move {
    transition: transform 0.5s;
  }
}

// 拖拽的幽灵节点
.ghost {
  opacity: 0.5;
}

.mutable-list-item,
.buttons {
  display: flex;
  flex-direction: row;
  align-items: center;
}

.mutable-list-item + .mutable-list-item {
  margin-top: 16px;
}

.buttons {
  min-width: 80px; // 避免按钮移除抖动
  margin-left: 20px;
}

.button-plus,
.button-minus {
  display: inline-block;
  width: 16px;
  height: 16px;
  padding: 4px;
  vertical-align: middle;
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
  background-origin: content-box;
  box-sizing: content-box;
}

.button-plus {
  background-image: url('./images/icon-plus.png');
}

.button-minus {
  background-image: url('./images/icon-minus.png');
}

.tag {
  cursor: pointer;
}
</style>
