tree.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. <template>
  2. <div
  3. class="el-tree"
  4. :class="{
  5. 'el-tree--highlight-current': highlightCurrent,
  6. 'is-dragging': !!dragState.draggingNode,
  7. 'is-drop-not-allow': !dragState.allowDrop,
  8. 'is-drop-inner': dragState.dropType === 'inner'
  9. }"
  10. role="tree"
  11. >
  12. <el-tree-node
  13. v-for="child in root.childNodes"
  14. :node="child"
  15. :props="props"
  16. :render-after-expand="renderAfterExpand"
  17. :show-checkbox="showCheckbox"
  18. :key="getNodeKey(child)"
  19. :render-content="renderContent"
  20. @node-expand="handleNodeExpand">
  21. </el-tree-node>
  22. <div class="el-tree__empty-block" v-if="isEmpty">
  23. <span class="el-tree__empty-text">{{ emptyText }}</span>
  24. </div>
  25. <div
  26. v-show="dragState.showDropIndicator"
  27. class="el-tree__drop-indicator"
  28. ref="dropIndicator">
  29. </div>
  30. </div>
  31. </template>
  32. <script>
  33. import TreeStore from './model/tree-store';
  34. import { getNodeKey, findNearestComponent } from './model/util';
  35. import ElTreeNode from './tree-node.vue';
  36. import {t} from 'element-ui/src/locale';
  37. import emitter from 'element-ui/src/mixins/emitter';
  38. import { addClass, removeClass } from 'element-ui/src/utils/dom';
  39. export default {
  40. name: 'ElTree',
  41. mixins: [emitter],
  42. components: {
  43. ElTreeNode
  44. },
  45. data() {
  46. return {
  47. store: null,
  48. root: null,
  49. currentNode: null,
  50. treeItems: null,
  51. checkboxItems: [],
  52. dragState: {
  53. showDropIndicator: false,
  54. draggingNode: null,
  55. dropNode: null,
  56. allowDrop: true
  57. }
  58. };
  59. },
  60. props: {
  61. data: {
  62. type: Array
  63. },
  64. emptyText: {
  65. type: String,
  66. default() {
  67. return t('el.tree.emptyText');
  68. }
  69. },
  70. renderAfterExpand: {
  71. type: Boolean,
  72. default: true
  73. },
  74. nodeKey: String,
  75. checkStrictly: Boolean,
  76. defaultExpandAll: Boolean,
  77. expandOnClickNode: {
  78. type: Boolean,
  79. default: true
  80. },
  81. checkOnClickNode: Boolean,
  82. checkDescendants: {
  83. type: Boolean,
  84. default: false
  85. },
  86. autoExpandParent: {
  87. type: Boolean,
  88. default: true
  89. },
  90. defaultCheckedKeys: Array,
  91. defaultExpandedKeys: Array,
  92. currentNodeKey: [String, Number],
  93. renderContent: Function,
  94. showCheckbox: {
  95. type: Boolean,
  96. default: false
  97. },
  98. draggable: {
  99. type: Boolean,
  100. default: false
  101. },
  102. allowDrag: Function,
  103. allowDrop: Function,
  104. props: {
  105. default() {
  106. return {
  107. children: 'children',
  108. label: 'label',
  109. disabled: 'disabled'
  110. };
  111. }
  112. },
  113. lazy: {
  114. type: Boolean,
  115. default: false
  116. },
  117. highlightCurrent: Boolean,
  118. load: Function,
  119. filterNodeMethod: Function,
  120. accordion: Boolean,
  121. indent: {
  122. type: Number,
  123. default: 18
  124. },
  125. iconClass: String
  126. },
  127. computed: {
  128. children: {
  129. set(value) {
  130. this.data = value;
  131. },
  132. get() {
  133. return this.data;
  134. }
  135. },
  136. treeItemArray() {
  137. return Array.prototype.slice.call(this.treeItems);
  138. },
  139. isEmpty() {
  140. const { childNodes } = this.root;
  141. return !childNodes || childNodes.length === 0 || childNodes.every(({visible}) => !visible);
  142. }
  143. },
  144. watch: {
  145. defaultCheckedKeys(newVal) {
  146. this.store.setDefaultCheckedKey(newVal);
  147. },
  148. defaultExpandedKeys(newVal) {
  149. this.store.defaultExpandedKeys = newVal;
  150. this.store.setDefaultExpandedKeys(newVal);
  151. },
  152. data(newVal) {
  153. this.store.setData(newVal);
  154. },
  155. checkboxItems(val) {
  156. Array.prototype.forEach.call(val, (checkbox) => {
  157. checkbox.setAttribute('tabindex', -1);
  158. });
  159. },
  160. checkStrictly(newVal) {
  161. this.store.checkStrictly = newVal;
  162. }
  163. },
  164. methods: {
  165. filter(value) {
  166. if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter');
  167. this.store.filter(value);
  168. },
  169. getNodeKey(node) {
  170. return getNodeKey(this.nodeKey, node.data);
  171. },
  172. getNodePath(data) {
  173. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getNodePath');
  174. const node = this.store.getNode(data);
  175. if (!node) return [];
  176. const path = [node.data];
  177. let parent = node.parent;
  178. while (parent && parent !== this.root) {
  179. path.push(parent.data);
  180. parent = parent.parent;
  181. }
  182. return path.reverse();
  183. },
  184. getCheckedNodes(leafOnly, includeHalfChecked) {
  185. return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
  186. },
  187. getCheckedKeys(leafOnly) {
  188. return this.store.getCheckedKeys(leafOnly);
  189. },
  190. getCurrentNode() {
  191. const currentNode = this.store.getCurrentNode();
  192. return currentNode ? currentNode.data : null;
  193. },
  194. getCurrentKey() {
  195. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getCurrentKey');
  196. const currentNode = this.getCurrentNode();
  197. return currentNode ? currentNode[this.nodeKey] : null;
  198. },
  199. setCheckedNodes(nodes, leafOnly) {
  200. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes');
  201. this.store.setCheckedNodes(nodes, leafOnly);
  202. },
  203. setCheckedKeys(keys, leafOnly) {
  204. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys');
  205. this.store.setCheckedKeys(keys, leafOnly);
  206. },
  207. setChecked(data, checked, deep) {
  208. this.store.setChecked(data, checked, deep);
  209. },
  210. getHalfCheckedNodes() {
  211. return this.store.getHalfCheckedNodes();
  212. },
  213. getHalfCheckedKeys() {
  214. return this.store.getHalfCheckedKeys();
  215. },
  216. setCurrentNode(node) {
  217. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode');
  218. this.store.setUserCurrentNode(node);
  219. },
  220. setCurrentKey(key) {
  221. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey');
  222. this.store.setCurrentNodeKey(key);
  223. },
  224. getNode(data) {
  225. return this.store.getNode(data);
  226. },
  227. remove(data) {
  228. this.store.remove(data);
  229. },
  230. append(data, parentNode) {
  231. this.store.append(data, parentNode);
  232. },
  233. insertBefore(data, refNode) {
  234. this.store.insertBefore(data, refNode);
  235. },
  236. insertAfter(data, refNode) {
  237. this.store.insertAfter(data, refNode);
  238. },
  239. handleNodeExpand(nodeData, node, instance) {
  240. this.broadcast('ElTreeNode', 'tree-node-expand', node);
  241. this.$emit('node-expand', nodeData, node, instance);
  242. },
  243. updateKeyChildren(key, data) {
  244. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
  245. this.store.updateChildren(key, data);
  246. },
  247. initTabIndex() {
  248. this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
  249. this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
  250. const checkedItem = this.$el.querySelectorAll('.is-checked[role=treeitem]');
  251. if (checkedItem.length) {
  252. checkedItem[0].setAttribute('tabindex', 0);
  253. return;
  254. }
  255. this.treeItems[0] && this.treeItems[0].setAttribute('tabindex', 0);
  256. },
  257. handleKeydown(ev) {
  258. const currentItem = ev.target;
  259. if (currentItem.className.indexOf('el-tree-node') === -1) return;
  260. const keyCode = ev.keyCode;
  261. this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
  262. const currentIndex = this.treeItemArray.indexOf(currentItem);
  263. let nextIndex;
  264. if ([38, 40].indexOf(keyCode) > -1) { // up、down
  265. ev.preventDefault();
  266. if (keyCode === 38) { // up
  267. nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
  268. } else {
  269. nextIndex = (currentIndex < this.treeItemArray.length - 1) ? currentIndex + 1 : 0;
  270. }
  271. this.treeItemArray[nextIndex].focus(); // 选中
  272. }
  273. if ([37, 39].indexOf(keyCode) > -1) { // left、right 展开
  274. ev.preventDefault();
  275. currentItem.click(); // 选中
  276. }
  277. const hasInput = currentItem.querySelector('[type="checkbox"]');
  278. if ([13, 32].indexOf(keyCode) > -1 && hasInput) { // space enter选中checkbox
  279. ev.preventDefault();
  280. hasInput.click();
  281. }
  282. }
  283. },
  284. created() {
  285. this.isTree = true;
  286. this.store = new TreeStore({
  287. key: this.nodeKey,
  288. data: this.data,
  289. lazy: this.lazy,
  290. props: this.props,
  291. load: this.load,
  292. currentNodeKey: this.currentNodeKey,
  293. checkStrictly: this.checkStrictly,
  294. checkDescendants: this.checkDescendants,
  295. defaultCheckedKeys: this.defaultCheckedKeys,
  296. defaultExpandedKeys: this.defaultExpandedKeys,
  297. autoExpandParent: this.autoExpandParent,
  298. defaultExpandAll: this.defaultExpandAll,
  299. filterNodeMethod: this.filterNodeMethod
  300. });
  301. this.root = this.store.root;
  302. let dragState = this.dragState;
  303. this.$on('tree-node-drag-start', (event, treeNode) => {
  304. if (typeof this.allowDrag === 'function' && !this.allowDrag(treeNode.node)) {
  305. event.preventDefault();
  306. return false;
  307. }
  308. event.dataTransfer.effectAllowed = 'move';
  309. // wrap in try catch to address IE's error when first param is 'text/plain'
  310. try {
  311. // setData is required for draggable to work in FireFox
  312. // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
  313. event.dataTransfer.setData('text/plain', '');
  314. } catch (e) {}
  315. dragState.draggingNode = treeNode;
  316. this.$emit('node-drag-start', treeNode.node, event);
  317. });
  318. this.$on('tree-node-drag-over', (event, treeNode) => {
  319. const dropNode = findNearestComponent(event.target, 'ElTreeNode');
  320. const oldDropNode = dragState.dropNode;
  321. if (oldDropNode && oldDropNode !== dropNode) {
  322. removeClass(oldDropNode.$el, 'is-drop-inner');
  323. }
  324. const draggingNode = dragState.draggingNode;
  325. if (!draggingNode || !dropNode) return;
  326. let dropPrev = true;
  327. let dropInner = true;
  328. let dropNext = true;
  329. let userAllowDropInner = true;
  330. if (typeof this.allowDrop === 'function') {
  331. dropPrev = this.allowDrop(draggingNode.node, dropNode.node, 'prev');
  332. userAllowDropInner = dropInner = this.allowDrop(draggingNode.node, dropNode.node, 'inner');
  333. dropNext = this.allowDrop(draggingNode.node, dropNode.node, 'next');
  334. }
  335. event.dataTransfer.dropEffect = dropInner ? 'move' : 'none';
  336. if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
  337. if (oldDropNode) {
  338. this.$emit('node-drag-leave', draggingNode.node, oldDropNode.node, event);
  339. }
  340. this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event);
  341. }
  342. if (dropPrev || dropInner || dropNext) {
  343. dragState.dropNode = dropNode;
  344. }
  345. if (dropNode.node.nextSibling === draggingNode.node) {
  346. dropNext = false;
  347. }
  348. if (dropNode.node.previousSibling === draggingNode.node) {
  349. dropPrev = false;
  350. }
  351. if (dropNode.node.contains(draggingNode.node, false)) {
  352. dropInner = false;
  353. }
  354. if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
  355. dropPrev = false;
  356. dropInner = false;
  357. dropNext = false;
  358. }
  359. const targetPosition = dropNode.$el.getBoundingClientRect();
  360. const treePosition = this.$el.getBoundingClientRect();
  361. let dropType;
  362. const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.45 : 1)) : -1;
  363. const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.55 : 0)) : 1;
  364. let indicatorTop = -9999;
  365. const distance = event.clientY - targetPosition.top;
  366. if (distance < targetPosition.height * prevPercent) {
  367. dropType = 'before';
  368. } else if (distance > targetPosition.height * nextPercent) {
  369. dropType = 'after';
  370. } else if (dropInner) {
  371. dropType = 'inner';
  372. } else {
  373. dropType = 'none';
  374. }
  375. const iconPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect();
  376. const dropIndicator = this.$refs.dropIndicator;
  377. if (dropType === 'before') {
  378. indicatorTop = iconPosition.top - treePosition.top;
  379. } else if (dropType === 'after') {
  380. indicatorTop = iconPosition.bottom - treePosition.top;
  381. }
  382. dropIndicator.style.top = indicatorTop + 'px';
  383. dropIndicator.style.left = (iconPosition.right - treePosition.left) + 'px';
  384. if (dropType === 'inner') {
  385. addClass(dropNode.$el, 'is-drop-inner');
  386. } else {
  387. removeClass(dropNode.$el, 'is-drop-inner');
  388. }
  389. dragState.showDropIndicator = dropType === 'before' || dropType === 'after';
  390. dragState.allowDrop = dragState.showDropIndicator || userAllowDropInner;
  391. dragState.dropType = dropType;
  392. this.$emit('node-drag-over', draggingNode.node, dropNode.node, event);
  393. });
  394. this.$on('tree-node-drag-end', (event) => {
  395. const { draggingNode, dropType, dropNode } = dragState;
  396. event.preventDefault();
  397. event.dataTransfer.dropEffect = 'move';
  398. if (draggingNode && dropNode) {
  399. const draggingNodeCopy = { data: draggingNode.node.data };
  400. if (dropType !== 'none') {
  401. draggingNode.node.remove();
  402. }
  403. if (dropType === 'before') {
  404. dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node);
  405. } else if (dropType === 'after') {
  406. dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node);
  407. } else if (dropType === 'inner') {
  408. dropNode.node.insertChild(draggingNodeCopy);
  409. }
  410. if (dropType !== 'none') {
  411. this.store.registerNode(draggingNodeCopy);
  412. }
  413. removeClass(dropNode.$el, 'is-drop-inner');
  414. this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event);
  415. if (dropType !== 'none') {
  416. this.$emit('node-drop', draggingNode.node, dropNode.node, dropType, event);
  417. }
  418. }
  419. if (draggingNode && !dropNode) {
  420. this.$emit('node-drag-end', draggingNode.node, null, dropType, event);
  421. }
  422. dragState.showDropIndicator = false;
  423. dragState.draggingNode = null;
  424. dragState.dropNode = null;
  425. dragState.allowDrop = true;
  426. });
  427. },
  428. mounted() {
  429. this.initTabIndex();
  430. this.$el.addEventListener('keydown', this.handleKeydown);
  431. },
  432. updated() {
  433. this.treeItems = this.$el.querySelectorAll('[role=treeitem]');
  434. this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
  435. }
  436. };
  437. </script>