| <template> | 
|   <div | 
|     class="el-tree" | 
|     :class="{ | 
|       'el-tree--highlight-current': highlightCurrent, | 
|       'is-dragging': !!dragState.draggingNode, | 
|       'is-drop-not-allow': !dragState.allowDrop, | 
|       'is-drop-inner': dragState.dropType === 'inner' | 
|     }" | 
|     role="tree" | 
|   > | 
|     <el-tree-node | 
|       v-for="child in root.childNodes" | 
|       :node="child" | 
|       :props="props" | 
|       :render-after-expand="renderAfterExpand" | 
|       :show-checkbox="showCheckbox" | 
|       :key="getNodeKey(child)" | 
|       :render-content="renderContent" | 
|       @node-expand="handleNodeExpand"> | 
|     </el-tree-node> | 
|     <div class="el-tree__empty-block" v-if="isEmpty"> | 
|       <span class="el-tree__empty-text">{{ emptyText }}</span> | 
|     </div> | 
|     <div | 
|       v-show="dragState.showDropIndicator" | 
|       class="el-tree__drop-indicator" | 
|       ref="dropIndicator"> | 
|     </div> | 
|   </div> | 
| </template> | 
|   | 
| <script> | 
|   import TreeStore from './model/tree-store'; | 
|   import { getNodeKey, findNearestComponent } from './model/util'; | 
|   import ElTreeNode from './tree-node.vue'; | 
|   import {t} from 'element-ui/src/locale'; | 
|   import emitter from 'element-ui/src/mixins/emitter'; | 
|   import { addClass, removeClass } from 'element-ui/src/utils/dom'; | 
|   | 
|   export default { | 
|     name: 'ElTree', | 
|   | 
|     mixins: [emitter], | 
|   | 
|     components: { | 
|       ElTreeNode | 
|     }, | 
|   | 
|     data() { | 
|       return { | 
|         store: null, | 
|         root: null, | 
|         currentNode: null, | 
|         treeItems: null, | 
|         checkboxItems: [], | 
|         dragState: { | 
|           showDropIndicator: false, | 
|           draggingNode: null, | 
|           dropNode: null, | 
|           allowDrop: true | 
|         } | 
|       }; | 
|     }, | 
|   | 
|     props: { | 
|       data: { | 
|         type: Array | 
|       }, | 
|       emptyText: { | 
|         type: String, | 
|         default() { | 
|           return t('el.tree.emptyText'); | 
|         } | 
|       }, | 
|       renderAfterExpand: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       nodeKey: String, | 
|       checkStrictly: Boolean, | 
|       defaultExpandAll: Boolean, | 
|       expandOnClickNode: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       checkOnClickNode: Boolean, | 
|       checkDescendants: { | 
|         type: Boolean, | 
|         default: false | 
|       }, | 
|       autoExpandParent: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       defaultCheckedKeys: Array, | 
|       defaultExpandedKeys: Array, | 
|       currentNodeKey: [String, Number], | 
|       renderContent: Function, | 
|       showCheckbox: { | 
|         type: Boolean, | 
|         default: false | 
|       }, | 
|       draggable: { | 
|         type: Boolean, | 
|         default: false | 
|       }, | 
|       allowDrag: Function, | 
|       allowDrop: Function, | 
|       props: { | 
|         default() { | 
|           return { | 
|             children: 'children', | 
|             label: 'label', | 
|             disabled: 'disabled' | 
|           }; | 
|         } | 
|       }, | 
|       lazy: { | 
|         type: Boolean, | 
|         default: false | 
|       }, | 
|       highlightCurrent: Boolean, | 
|       load: Function, | 
|       filterNodeMethod: Function, | 
|       accordion: Boolean, | 
|       indent: { | 
|         type: Number, | 
|         default: 18 | 
|       }, | 
|       iconClass: String | 
|     }, | 
|   | 
|     computed: { | 
|       children: { | 
|         set(value) { | 
|           this.data = value; | 
|         }, | 
|         get() { | 
|           return this.data; | 
|         } | 
|       }, | 
|   | 
|       treeItemArray() { | 
|         return Array.prototype.slice.call(this.treeItems); | 
|       }, | 
|   | 
|       isEmpty() { | 
|         const { childNodes } = this.root; | 
|         return !childNodes || childNodes.length === 0 || childNodes.every(({visible}) => !visible); | 
|       } | 
|     }, | 
|   | 
|     watch: { | 
|       defaultCheckedKeys(newVal) { | 
|         this.store.setDefaultCheckedKey(newVal); | 
|       }, | 
|   | 
|       defaultExpandedKeys(newVal) { | 
|         this.store.defaultExpandedKeys = newVal; | 
|         this.store.setDefaultExpandedKeys(newVal); | 
|       }, | 
|   | 
|       data(newVal) { | 
|         this.store.setData(newVal); | 
|       }, | 
|   | 
|       checkboxItems(val) { | 
|         Array.prototype.forEach.call(val, (checkbox) => { | 
|           checkbox.setAttribute('tabindex', -1); | 
|         }); | 
|       }, | 
|   | 
|       checkStrictly(newVal) { | 
|         this.store.checkStrictly = newVal; | 
|       } | 
|     }, | 
|   | 
|     methods: { | 
|       filter(value) { | 
|         if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter'); | 
|         this.store.filter(value); | 
|       }, | 
|   | 
|       getNodeKey(node) { | 
|         return getNodeKey(this.nodeKey, node.data); | 
|       }, | 
|   | 
|       getNodePath(data) { | 
|         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getNodePath'); | 
|         const node = this.store.getNode(data); | 
|         if (!node) return []; | 
|         const path = [node.data]; | 
|         let parent = node.parent; | 
|         while (parent && parent !== this.root) { | 
|           path.push(parent.data); | 
|           parent = parent.parent; | 
|         } | 
|         return path.reverse(); | 
|       }, | 
|   | 
|       getCheckedNodes(leafOnly, includeHalfChecked) { | 
|         return this.store.getCheckedNodes(leafOnly, includeHalfChecked); | 
|       }, | 
|   | 
|       getCheckedKeys(leafOnly) { | 
|         return this.store.getCheckedKeys(leafOnly); | 
|       }, | 
|   | 
|       getCurrentNode() { | 
|         const currentNode = this.store.getCurrentNode(); | 
|         return currentNode ? currentNode.data : null; | 
|       }, | 
|   | 
|       getCurrentKey() { | 
|         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getCurrentKey'); | 
|         const currentNode = this.getCurrentNode(); | 
|         return currentNode ? currentNode[this.nodeKey] : null; | 
|       }, | 
|   | 
|       setCheckedNodes(nodes, leafOnly) { | 
|         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes'); | 
|         this.store.setCheckedNodes(nodes, leafOnly); | 
|       }, | 
|   | 
|       setCheckedKeys(keys, leafOnly) { | 
|         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys'); | 
|         this.store.setCheckedKeys(keys, leafOnly); | 
|       }, | 
|   | 
|       setChecked(data, checked, deep) { | 
|         this.store.setChecked(data, checked, deep); | 
|       }, | 
|   | 
|       getHalfCheckedNodes() { | 
|         return this.store.getHalfCheckedNodes(); | 
|       }, | 
|   | 
|       getHalfCheckedKeys() { | 
|         return this.store.getHalfCheckedKeys(); | 
|       }, | 
|   | 
|       setCurrentNode(node) { | 
|         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode'); | 
|         this.store.setUserCurrentNode(node); | 
|       }, | 
|   | 
|       setCurrentKey(key) { | 
|         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey'); | 
|         this.store.setCurrentNodeKey(key); | 
|       }, | 
|   | 
|       getNode(data) { | 
|         return this.store.getNode(data); | 
|       }, | 
|   | 
|       remove(data) { | 
|         this.store.remove(data); | 
|       }, | 
|   | 
|       append(data, parentNode) { | 
|         this.store.append(data, parentNode); | 
|       }, | 
|   | 
|       insertBefore(data, refNode) { | 
|         this.store.insertBefore(data, refNode); | 
|       }, | 
|   | 
|       insertAfter(data, refNode) { | 
|         this.store.insertAfter(data, refNode); | 
|       }, | 
|   | 
|       handleNodeExpand(nodeData, node, instance) { | 
|         this.broadcast('ElTreeNode', 'tree-node-expand', node); | 
|         this.$emit('node-expand', nodeData, node, instance); | 
|       }, | 
|   | 
|       updateKeyChildren(key, data) { | 
|         if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild'); | 
|         this.store.updateChildren(key, data); | 
|       }, | 
|   | 
|       initTabIndex() { | 
|         this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]'); | 
|         this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]'); | 
|         const checkedItem = this.$el.querySelectorAll('.is-checked[role=treeitem]'); | 
|         if (checkedItem.length) { | 
|           checkedItem[0].setAttribute('tabindex', 0); | 
|           return; | 
|         } | 
|         this.treeItems[0] && this.treeItems[0].setAttribute('tabindex', 0); | 
|       }, | 
|   | 
|       handleKeydown(ev) { | 
|         const currentItem = ev.target; | 
|         if (currentItem.className.indexOf('el-tree-node') === -1) return; | 
|         const keyCode = ev.keyCode; | 
|         this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]'); | 
|         const currentIndex = this.treeItemArray.indexOf(currentItem); | 
|         let nextIndex; | 
|         if ([38, 40].indexOf(keyCode) > -1) { // up、down | 
|           ev.preventDefault(); | 
|           if (keyCode === 38) { // up | 
|             nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0; | 
|           } else { | 
|             nextIndex = (currentIndex < this.treeItemArray.length - 1) ? currentIndex + 1 : 0; | 
|           } | 
|           this.treeItemArray[nextIndex].focus(); // 选中 | 
|         } | 
|         if ([37, 39].indexOf(keyCode) > -1) { // left、right 展开 | 
|           ev.preventDefault(); | 
|           currentItem.click(); // 选中 | 
|         } | 
|         const hasInput = currentItem.querySelector('[type="checkbox"]'); | 
|         if ([13, 32].indexOf(keyCode) > -1 && hasInput) { // space enter选中checkbox | 
|           ev.preventDefault(); | 
|           hasInput.click(); | 
|         } | 
|       } | 
|     }, | 
|   | 
|     created() { | 
|       this.isTree = true; | 
|   | 
|       this.store = new TreeStore({ | 
|         key: this.nodeKey, | 
|         data: this.data, | 
|         lazy: this.lazy, | 
|         props: this.props, | 
|         load: this.load, | 
|         currentNodeKey: this.currentNodeKey, | 
|         checkStrictly: this.checkStrictly, | 
|         checkDescendants: this.checkDescendants, | 
|         defaultCheckedKeys: this.defaultCheckedKeys, | 
|         defaultExpandedKeys: this.defaultExpandedKeys, | 
|         autoExpandParent: this.autoExpandParent, | 
|         defaultExpandAll: this.defaultExpandAll, | 
|         filterNodeMethod: this.filterNodeMethod | 
|       }); | 
|   | 
|       this.root = this.store.root; | 
|   | 
|       let dragState = this.dragState; | 
|       this.$on('tree-node-drag-start', (event, treeNode) => { | 
|         if (typeof this.allowDrag === 'function' && !this.allowDrag(treeNode.node)) { | 
|           event.preventDefault(); | 
|           return false; | 
|         } | 
|         event.dataTransfer.effectAllowed = 'move'; | 
|   | 
|         // wrap in try catch to address IE's error when first param is 'text/plain' | 
|         try { | 
|           // setData is required for draggable to work in FireFox | 
|           // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox | 
|           event.dataTransfer.setData('text/plain', ''); | 
|         } catch (e) {} | 
|         dragState.draggingNode = treeNode; | 
|         this.$emit('node-drag-start', treeNode.node, event); | 
|       }); | 
|   | 
|       this.$on('tree-node-drag-over', (event, treeNode) => { | 
|         const dropNode = findNearestComponent(event.target, 'ElTreeNode'); | 
|         const oldDropNode = dragState.dropNode; | 
|         if (oldDropNode && oldDropNode !== dropNode) { | 
|           removeClass(oldDropNode.$el, 'is-drop-inner'); | 
|         } | 
|         const draggingNode = dragState.draggingNode; | 
|         if (!draggingNode || !dropNode) return; | 
|   | 
|         let dropPrev = true; | 
|         let dropInner = true; | 
|         let dropNext = true; | 
|         let userAllowDropInner = true; | 
|         if (typeof this.allowDrop === 'function') { | 
|           dropPrev = this.allowDrop(draggingNode.node, dropNode.node, 'prev'); | 
|           userAllowDropInner = dropInner = this.allowDrop(draggingNode.node, dropNode.node, 'inner'); | 
|           dropNext = this.allowDrop(draggingNode.node, dropNode.node, 'next'); | 
|         } | 
|         event.dataTransfer.dropEffect = dropInner ? 'move' : 'none'; | 
|         if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) { | 
|           if (oldDropNode) { | 
|             this.$emit('node-drag-leave', draggingNode.node, oldDropNode.node, event); | 
|           } | 
|           this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event); | 
|         } | 
|   | 
|         if (dropPrev || dropInner || dropNext) { | 
|           dragState.dropNode = dropNode; | 
|         } | 
|   | 
|         if (dropNode.node.nextSibling === draggingNode.node) { | 
|           dropNext = false; | 
|         } | 
|         if (dropNode.node.previousSibling === draggingNode.node) { | 
|           dropPrev = false; | 
|         } | 
|         if (dropNode.node.contains(draggingNode.node, false)) { | 
|           dropInner = false; | 
|         } | 
|         if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) { | 
|           dropPrev = false; | 
|           dropInner = false; | 
|           dropNext = false; | 
|         } | 
|   | 
|         const targetPosition = dropNode.$el.getBoundingClientRect(); | 
|         const treePosition = this.$el.getBoundingClientRect(); | 
|   | 
|         let dropType; | 
|         const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.45 : 1)) : -1; | 
|         const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.55 : 0)) : 1; | 
|   | 
|         let indicatorTop = -9999; | 
|         const distance = event.clientY - targetPosition.top; | 
|         if (distance < targetPosition.height * prevPercent) { | 
|           dropType = 'before'; | 
|         } else if (distance > targetPosition.height * nextPercent) { | 
|           dropType = 'after'; | 
|         } else if (dropInner) { | 
|           dropType = 'inner'; | 
|         } else { | 
|           dropType = 'none'; | 
|         } | 
|   | 
|         const iconPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect(); | 
|         const dropIndicator = this.$refs.dropIndicator; | 
|         if (dropType === 'before') { | 
|           indicatorTop = iconPosition.top - treePosition.top; | 
|         } else if (dropType === 'after') { | 
|           indicatorTop = iconPosition.bottom - treePosition.top; | 
|         } | 
|         dropIndicator.style.top = indicatorTop + 'px'; | 
|         dropIndicator.style.left = (iconPosition.right - treePosition.left) + 'px'; | 
|   | 
|         if (dropType === 'inner') { | 
|           addClass(dropNode.$el, 'is-drop-inner'); | 
|         } else { | 
|           removeClass(dropNode.$el, 'is-drop-inner'); | 
|         } | 
|   | 
|         dragState.showDropIndicator = dropType === 'before' || dropType === 'after'; | 
|         dragState.allowDrop = dragState.showDropIndicator || userAllowDropInner; | 
|         dragState.dropType = dropType; | 
|         this.$emit('node-drag-over', draggingNode.node, dropNode.node, event); | 
|       }); | 
|   | 
|       this.$on('tree-node-drag-end', (event) => { | 
|         const { draggingNode, dropType, dropNode } = dragState; | 
|         event.preventDefault(); | 
|         event.dataTransfer.dropEffect = 'move'; | 
|   | 
|         if (draggingNode && dropNode) { | 
|           const draggingNodeCopy = { data: draggingNode.node.data }; | 
|           if (dropType !== 'none') { | 
|             draggingNode.node.remove(); | 
|           } | 
|           if (dropType === 'before') { | 
|             dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node); | 
|           } else if (dropType === 'after') { | 
|             dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node); | 
|           } else if (dropType === 'inner') { | 
|             dropNode.node.insertChild(draggingNodeCopy); | 
|           } | 
|           if (dropType !== 'none') { | 
|             this.store.registerNode(draggingNodeCopy); | 
|           } | 
|   | 
|           removeClass(dropNode.$el, 'is-drop-inner'); | 
|   | 
|           this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event); | 
|           if (dropType !== 'none') { | 
|             this.$emit('node-drop', draggingNode.node, dropNode.node, dropType, event); | 
|           } | 
|         } | 
|         if (draggingNode && !dropNode) { | 
|           this.$emit('node-drag-end', draggingNode.node, null, dropType, event); | 
|         } | 
|   | 
|         dragState.showDropIndicator = false; | 
|         dragState.draggingNode = null; | 
|         dragState.dropNode = null; | 
|         dragState.allowDrop = true; | 
|       }); | 
|     }, | 
|   | 
|     mounted() { | 
|       this.initTabIndex(); | 
|       this.$el.addEventListener('keydown', this.handleKeydown); | 
|     }, | 
|   | 
|     updated() { | 
|       this.treeItems = this.$el.querySelectorAll('[role=treeitem]'); | 
|       this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]'); | 
|     } | 
|   }; | 
| </script> |