cascader-panel.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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. options: {
  125. handler: function() {
  126. this.initStore();
  127. },
  128. immediate: true,
  129. deep: true
  130. },
  131. value() {
  132. this.syncCheckedValue();
  133. this.checkStrictly && this.calculateCheckedNodePaths();
  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 (!isEmpty(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.checkedValue = value;
  163. this.syncMenuState();
  164. }
  165. },
  166. syncMenuState() {
  167. const { multiple, checkStrictly } = this;
  168. this.syncActivePath();
  169. multiple && this.syncMultiCheckState();
  170. checkStrictly && this.calculateCheckedNodePaths();
  171. this.$nextTick(this.scrollIntoView);
  172. },
  173. syncMultiCheckState() {
  174. const nodes = this.getFlattedNodes(this.leafOnly);
  175. nodes.forEach(node => {
  176. node.syncCheckState(this.checkedValue);
  177. });
  178. },
  179. syncActivePath() {
  180. const { store, multiple, activePath, checkedValue } = this;
  181. if (!isEmpty(activePath)) {
  182. const nodes = activePath.map(node => this.getNodeByValue(node.getValue()));
  183. this.expandNodes(nodes);
  184. } else if (!isEmpty(checkedValue)) {
  185. const value = multiple ? checkedValue[0] : checkedValue;
  186. const checkedNode = this.getNodeByValue(value) || {};
  187. const nodes = (checkedNode.pathNodes || []).slice(0, -1);
  188. this.expandNodes(nodes);
  189. } else {
  190. this.activePath = [];
  191. this.menus = [store.getNodes()];
  192. }
  193. },
  194. expandNodes(nodes) {
  195. nodes.forEach(node => this.handleExpand(node, true /* silent */));
  196. },
  197. calculateCheckedNodePaths() {
  198. const { checkedValue, multiple } = this;
  199. const checkedValues = multiple
  200. ? coerceTruthyValueToArray(checkedValue)
  201. : [ checkedValue ];
  202. this.checkedNodePaths = checkedValues.map(v => {
  203. const checkedNode = this.getNodeByValue(v);
  204. return checkedNode ? checkedNode.pathNodes : [];
  205. });
  206. },
  207. handleKeyDown(e) {
  208. const { target, keyCode } = e;
  209. switch (keyCode) {
  210. case KeyCode.up:
  211. const prev = getSibling(target, -1);
  212. focusNode(prev);
  213. break;
  214. case KeyCode.down:
  215. const next = getSibling(target, 1);
  216. focusNode(next);
  217. break;
  218. case KeyCode.left:
  219. const preMenu = this.$refs.menu[getMenuIndex(target) - 1];
  220. if (preMenu) {
  221. const expandedNode = preMenu.$el.querySelector('.el-cascader-node[aria-expanded="true"]');
  222. focusNode(expandedNode);
  223. }
  224. break;
  225. case KeyCode.right:
  226. const nextMenu = this.$refs.menu[getMenuIndex(target) + 1];
  227. if (nextMenu) {
  228. const firstNode = nextMenu.$el.querySelector('.el-cascader-node[tabindex="-1"]');
  229. focusNode(firstNode);
  230. }
  231. break;
  232. case KeyCode.enter:
  233. checkNode(target);
  234. break;
  235. case KeyCode.esc:
  236. case KeyCode.tab:
  237. this.$emit('close');
  238. break;
  239. default:
  240. return;
  241. }
  242. },
  243. handleExpand(node, silent) {
  244. const { activePath } = this;
  245. const { level } = node;
  246. const path = activePath.slice(0, level - 1);
  247. const menus = this.menus.slice(0, level);
  248. if (!node.isLeaf) {
  249. path.push(node);
  250. menus.push(node.children);
  251. }
  252. this.activePath = path;
  253. this.menus = menus;
  254. if (!silent) {
  255. const pathValues = path.map(node => node.getValue());
  256. const activePathValues = activePath.map(node => node.getValue());
  257. if (!valueEquals(pathValues, activePathValues)) {
  258. this.$emit('active-item-change', pathValues); // Deprecated
  259. this.$emit('expand-change', pathValues);
  260. }
  261. }
  262. },
  263. handleCheckChange(value) {
  264. this.checkedValue = value;
  265. },
  266. lazyLoad(node, onFullfiled) {
  267. const { config } = this;
  268. if (!node) {
  269. node = node || { root: true, level: 0 };
  270. this.store = new Store([], config);
  271. this.menus = [this.store.getNodes()];
  272. }
  273. node.loading = true;
  274. const resolve = dataList => {
  275. const parent = node.root ? null : node;
  276. dataList && dataList.length && this.store.appendNodes(dataList, parent);
  277. node.loading = false;
  278. node.loaded = true;
  279. // dispose default value on lazy load mode
  280. if (Array.isArray(this.checkedValue)) {
  281. const nodeValue = this.checkedValue[this.loadCount++];
  282. const valueKey = this.config.value;
  283. const leafKey = this.config.leaf;
  284. if (Array.isArray(dataList) && dataList.filter(item => item[valueKey] === nodeValue).length > 0) {
  285. const checkedNode = this.store.getNodeByValue(nodeValue);
  286. if (!checkedNode.data[leafKey]) {
  287. this.lazyLoad(checkedNode, () => {
  288. this.handleExpand(checkedNode);
  289. });
  290. }
  291. if (this.loadCount === this.checkedValue.length) {
  292. this.$parent.computePresentText();
  293. }
  294. }
  295. }
  296. onFullfiled && onFullfiled(dataList);
  297. };
  298. config.lazyLoad(node, resolve);
  299. },
  300. /**
  301. * public methods
  302. */
  303. calculateMultiCheckedValue() {
  304. this.checkedValue = this.getCheckedNodes(this.leafOnly)
  305. .map(node => node.getValueByOption());
  306. },
  307. scrollIntoView() {
  308. if (this.$isServer) return;
  309. const menus = this.$refs.menu || [];
  310. menus.forEach(menu => {
  311. const menuElement = menu.$el;
  312. if (menuElement) {
  313. const container = menuElement.querySelector('.el-scrollbar__wrap');
  314. const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
  315. menuElement.querySelector('.el-cascader-node.in-active-path');
  316. scrollIntoView(container, activeNode);
  317. }
  318. });
  319. },
  320. getNodeByValue(val) {
  321. return this.store.getNodeByValue(val);
  322. },
  323. getFlattedNodes(leafOnly) {
  324. const cached = !this.config.lazy;
  325. return this.store.getFlattedNodes(leafOnly, cached);
  326. },
  327. getCheckedNodes(leafOnly) {
  328. const { checkedValue, multiple } = this;
  329. if (multiple) {
  330. const nodes = this.getFlattedNodes(leafOnly);
  331. return nodes.filter(node => node.checked);
  332. } else {
  333. return isEmpty(checkedValue)
  334. ? []
  335. : [this.getNodeByValue(checkedValue)];
  336. }
  337. },
  338. clearCheckedNodes() {
  339. const { config, leafOnly } = this;
  340. const { multiple, emitPath } = config;
  341. if (multiple) {
  342. this.getCheckedNodes(leafOnly)
  343. .filter(node => !node.isDisabled)
  344. .forEach(node => node.doCheck(false));
  345. this.calculateMultiCheckedValue();
  346. } else {
  347. this.checkedValue = emitPath ? [] : null;
  348. }
  349. }
  350. }
  351. };
  352. </script>