cascader-panel.vue 10 KB

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