cascader-panel.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. <template>
  2. <div
  3. :class="[
  4. 'el-cascader-panel',
  5. border && 'is-bordered'
  6. ]"
  7. @keydown="handleKeyDown">
  8. <cascader-menu
  9. ref="menu"
  10. v-for="(menu, index) in menus"
  11. :index="index"
  12. :key="index"
  13. :nodes="menu"></cascader-menu>
  14. </div>
  15. </template>
  16. <script>
  17. import CascaderMenu from './cascader-menu';
  18. import Store from './store';
  19. import merge from 'element-ui/src/utils/merge';
  20. import AriaUtils from 'element-ui/src/utils/aria-utils';
  21. import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
  22. import {
  23. noop,
  24. coerceTruthyValueToArray,
  25. isEqual,
  26. isEmpty,
  27. valueEquals
  28. } from 'element-ui/src/utils/util';
  29. const { keys: KeyCode } = AriaUtils;
  30. const DefaultProps = {
  31. expandTrigger: 'click', // or hover
  32. multiple: false,
  33. checkStrictly: false, // whether all nodes can be selected
  34. emitPath: true, // wether to emit an array of all levels value in which node is located
  35. lazy: false,
  36. lazyLoad: noop,
  37. value: 'value',
  38. label: 'label',
  39. children: 'children',
  40. leaf: 'leaf',
  41. disabled: 'disabled',
  42. hoverThreshold: 500
  43. };
  44. const isLeaf = el => !el.getAttribute('aria-owns');
  45. const getSibling = (el, distance) => {
  46. const { parentNode } = el;
  47. if (parentNode) {
  48. const siblings = parentNode.querySelectorAll('.el-cascader-node[tabindex="-1"]');
  49. const index = Array.prototype.indexOf.call(siblings, el);
  50. return siblings[index + distance] || null;
  51. }
  52. return null;
  53. };
  54. const getMenuIndex = (el, distance) => {
  55. if (!el) return;
  56. const pieces = el.id.split('-');
  57. return Number(pieces[pieces.length - 2]);
  58. };
  59. const focusNode = el => {
  60. if (!el) return;
  61. el.focus();
  62. !isLeaf(el) && el.click();
  63. };
  64. const checkNode = el => {
  65. if (!el) return;
  66. const input = el.querySelector('input');
  67. if (input) {
  68. input.click();
  69. } else if (isLeaf(el)) {
  70. el.click();
  71. }
  72. };
  73. export default {
  74. name: 'ElCascaderPanel',
  75. components: {
  76. CascaderMenu
  77. },
  78. props: {
  79. value: {},
  80. options: Array,
  81. props: Object,
  82. border: {
  83. type: Boolean,
  84. default: true
  85. },
  86. renderLabel: Function
  87. },
  88. provide() {
  89. return {
  90. panel: this
  91. };
  92. },
  93. data() {
  94. return {
  95. checkedValue: null,
  96. checkedNodePaths: [],
  97. store: [],
  98. menus: [],
  99. activePath: [],
  100. loadCount: 0
  101. };
  102. },
  103. computed: {
  104. config() {
  105. return merge({ ...DefaultProps }, this.props || {});
  106. },
  107. multiple() {
  108. return this.config.multiple;
  109. },
  110. checkStrictly() {
  111. return this.config.checkStrictly;
  112. },
  113. leafOnly() {
  114. return !this.checkStrictly;
  115. },
  116. isHoverMenu() {
  117. return this.config.expandTrigger === 'hover';
  118. },
  119. renderLabelFn() {
  120. return this.renderLabel || this.$scopedSlots.default;
  121. }
  122. },
  123. watch: {
  124. value() {
  125. this.syncCheckedValue();
  126. this.checkStrictly && this.calculateCheckedNodePaths();
  127. },
  128. options: {
  129. handler: function() {
  130. this.initStore();
  131. },
  132. immediate: true,
  133. deep: true
  134. },
  135. checkedValue(val) {
  136. if (!isEqual(val, this.value)) {
  137. this.checkStrictly && this.calculateCheckedNodePaths();
  138. this.$emit('input', val);
  139. this.$emit('change', val);
  140. }
  141. }
  142. },
  143. mounted() {
  144. if (!this.isEmptyValue(this.value)) {
  145. this.syncCheckedValue();
  146. }
  147. },
  148. methods: {
  149. initStore() {
  150. const { config, options } = this;
  151. if (config.lazy && isEmpty(options)) {
  152. this.lazyLoad();
  153. } else {
  154. this.store = new Store(options, config);
  155. this.menus = [this.store.getNodes()];
  156. this.syncMenuState();
  157. }
  158. },
  159. syncCheckedValue() {
  160. const { value, checkedValue } = this;
  161. if (!isEqual(value, checkedValue)) {
  162. this.activePath = [];
  163. this.checkedValue = value;
  164. this.syncMenuState();
  165. }
  166. },
  167. syncMenuState() {
  168. const { multiple, checkStrictly } = this;
  169. this.syncActivePath();
  170. multiple && this.syncMultiCheckState();
  171. checkStrictly && this.calculateCheckedNodePaths();
  172. this.$nextTick(this.scrollIntoView);
  173. },
  174. syncMultiCheckState() {
  175. const nodes = this.getFlattedNodes(this.leafOnly);
  176. nodes.forEach(node => {
  177. node.syncCheckState(this.checkedValue);
  178. });
  179. },
  180. isEmptyValue(val) {
  181. const { multiple, config } = this;
  182. const { emitPath } = config;
  183. if (multiple || emitPath) {
  184. return isEmpty(val);
  185. }
  186. return false;
  187. },
  188. syncActivePath() {
  189. const { store, multiple, activePath, checkedValue } = this;
  190. if (!isEmpty(activePath)) {
  191. const nodes = activePath.map(node => this.getNodeByValue(node.getValue()));
  192. this.expandNodes(nodes);
  193. } else if (!this.isEmptyValue(checkedValue)) {
  194. const value = multiple ? checkedValue[0] : checkedValue;
  195. const checkedNode = this.getNodeByValue(value) || {};
  196. const nodes = (checkedNode.pathNodes || []).slice(0, -1);
  197. this.expandNodes(nodes);
  198. } else {
  199. this.activePath = [];
  200. this.menus = [store.getNodes()];
  201. }
  202. },
  203. expandNodes(nodes) {
  204. nodes.forEach(node => this.handleExpand(node, true /* silent */));
  205. },
  206. calculateCheckedNodePaths() {
  207. const { checkedValue, multiple } = this;
  208. const checkedValues = multiple
  209. ? coerceTruthyValueToArray(checkedValue)
  210. : [ checkedValue ];
  211. this.checkedNodePaths = checkedValues.map(v => {
  212. const checkedNode = this.getNodeByValue(v);
  213. return checkedNode ? checkedNode.pathNodes : [];
  214. });
  215. },
  216. handleKeyDown(e) {
  217. const { target, keyCode } = e;
  218. switch (keyCode) {
  219. case KeyCode.up:
  220. const prev = getSibling(target, -1);
  221. focusNode(prev);
  222. break;
  223. case KeyCode.down:
  224. const next = getSibling(target, 1);
  225. focusNode(next);
  226. break;
  227. case KeyCode.left:
  228. const preMenu = this.$refs.menu[getMenuIndex(target) - 1];
  229. if (preMenu) {
  230. const expandedNode = preMenu.$el.querySelector('.el-cascader-node[aria-expanded="true"]');
  231. focusNode(expandedNode);
  232. }
  233. break;
  234. case KeyCode.right:
  235. const nextMenu = this.$refs.menu[getMenuIndex(target) + 1];
  236. if (nextMenu) {
  237. const firstNode = nextMenu.$el.querySelector('.el-cascader-node[tabindex="-1"]');
  238. focusNode(firstNode);
  239. }
  240. break;
  241. case KeyCode.enter:
  242. checkNode(target);
  243. break;
  244. case KeyCode.esc:
  245. case KeyCode.tab:
  246. this.$emit('close');
  247. break;
  248. default:
  249. return;
  250. }
  251. },
  252. handleExpand(node, silent) {
  253. const { activePath } = this;
  254. const { level } = node;
  255. const path = activePath.slice(0, level - 1);
  256. const menus = this.menus.slice(0, level);
  257. if (!node.isLeaf) {
  258. path.push(node);
  259. menus.push(node.children);
  260. }
  261. this.activePath = path;
  262. this.menus = menus;
  263. if (!silent) {
  264. const pathValues = path.map(node => node.getValue());
  265. const activePathValues = activePath.map(node => node.getValue());
  266. if (!valueEquals(pathValues, activePathValues)) {
  267. this.$emit('active-item-change', pathValues); // Deprecated
  268. this.$emit('expand-change', pathValues);
  269. }
  270. }
  271. },
  272. handleCheckChange(value) {
  273. this.checkedValue = value;
  274. },
  275. lazyLoad(node, onFullfiled) {
  276. const { config } = this;
  277. if (!node) {
  278. node = node || { root: true, level: 0 };
  279. this.store = new Store([], config);
  280. this.menus = [this.store.getNodes()];
  281. }
  282. node.loading = true;
  283. const resolve = dataList => {
  284. const parent = node.root ? null : node;
  285. dataList && dataList.length && this.store.appendNodes(dataList, parent);
  286. node.loading = false;
  287. node.loaded = true;
  288. // dispose default value on lazy load mode
  289. if (Array.isArray(this.checkedValue)) {
  290. const nodeValue = this.checkedValue[this.loadCount++];
  291. const valueKey = this.config.value;
  292. const leafKey = this.config.leaf;
  293. if (Array.isArray(dataList) && dataList.filter(item => item[valueKey] === nodeValue).length > 0) {
  294. const checkedNode = this.store.getNodeByValue(nodeValue);
  295. if (!checkedNode.data[leafKey]) {
  296. this.lazyLoad(checkedNode, () => {
  297. this.handleExpand(checkedNode);
  298. });
  299. }
  300. if (this.loadCount === this.checkedValue.length) {
  301. this.$parent.computePresentText();
  302. }
  303. }
  304. }
  305. onFullfiled && onFullfiled(dataList);
  306. };
  307. config.lazyLoad(node, resolve);
  308. },
  309. /**
  310. * public methods
  311. */
  312. calculateMultiCheckedValue() {
  313. this.checkedValue = this.getCheckedNodes(this.leafOnly)
  314. .map(node => node.getValueByOption());
  315. },
  316. scrollIntoView() {
  317. if (this.$isServer) return;
  318. const menus = this.$refs.menu || [];
  319. menus.forEach(menu => {
  320. const menuElement = menu.$el;
  321. if (menuElement) {
  322. const container = menuElement.querySelector('.el-scrollbar__wrap');
  323. const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
  324. menuElement.querySelector('.el-cascader-node.in-active-path');
  325. scrollIntoView(container, activeNode);
  326. }
  327. });
  328. },
  329. getNodeByValue(val) {
  330. return this.store.getNodeByValue(val);
  331. },
  332. getFlattedNodes(leafOnly) {
  333. const cached = !this.config.lazy;
  334. return this.store.getFlattedNodes(leafOnly, cached);
  335. },
  336. getCheckedNodes(leafOnly) {
  337. const { checkedValue, multiple } = this;
  338. if (multiple) {
  339. const nodes = this.getFlattedNodes(leafOnly);
  340. return nodes.filter(node => node.checked);
  341. } else {
  342. return this.isEmptyValue(checkedValue)
  343. ? []
  344. : [this.getNodeByValue(checkedValue)];
  345. }
  346. },
  347. clearCheckedNodes() {
  348. const { config, leafOnly } = this;
  349. const { multiple, emitPath } = config;
  350. if (multiple) {
  351. this.getCheckedNodes(leafOnly)
  352. .filter(node => !node.isDisabled)
  353. .forEach(node => node.doCheck(false));
  354. this.calculateMultiCheckedValue();
  355. } else {
  356. this.checkedValue = emitPath ? [] : null;
  357. }
  358. }
  359. }
  360. };
  361. </script>