diff --git a/components/_util/props-util.js b/components/_util/props-util.js index d483cbd80..8aec27a94 100644 --- a/components/_util/props-util.js +++ b/components/_util/props-util.js @@ -166,10 +166,6 @@ export function getComponentName (opts) { return opts && (opts.Ctor.options.name || opts.tag) } -export function isValidElement (ele) { - return !!ele.tag -} - export function isEmptyElement (ele) { return !(ele.tag || ele.text.trim() !== '') } @@ -206,6 +202,11 @@ export function mergeProps () { return props } +function isValidElement (element) { + const name = element.constructor.name + return element.tag && (name === 'VNode' || name === 'VueComponent') +} + export { hasProp, filterProps, @@ -219,5 +220,6 @@ export { getValueByProp, parseStyleText, initDefaultProps, + isValidElement, } export default hasProp diff --git a/components/_util/store/PropTypes.js b/components/_util/store/PropTypes.js new file mode 100644 index 000000000..603ffe6bb --- /dev/null +++ b/components/_util/store/PropTypes.js @@ -0,0 +1,7 @@ +import PropTypes from '../vue-types' + +export const storeShape = PropTypes.shape({ + subscribe: PropTypes.func.isRequired, + setState: PropTypes.func.isRequired, + getState: PropTypes.func.isRequired, +}) diff --git a/components/_util/store/Provider.jsx b/components/_util/store/Provider.jsx new file mode 100644 index 000000000..0b01a3397 --- /dev/null +++ b/components/_util/store/Provider.jsx @@ -0,0 +1,16 @@ + +import { storeShape } from './PropTypes' +export default { + name: 'StoreProvider', + props: { + store: storeShape.isRequired, + }, + provide () { + return { + _store: this.$props, + } + }, + render () { + return this.$slots.default[0] + }, +} diff --git a/components/_util/store/connect.jsx b/components/_util/store/connect.jsx new file mode 100644 index 000000000..40296f406 --- /dev/null +++ b/components/_util/store/connect.jsx @@ -0,0 +1,85 @@ +import shallowEqual from 'shallowequal' +import omit from 'omit.js' +import { getOptionProps } from '../props-util' +import PropTypes from '../vue-types' + +function getDisplayName (WrappedComponent) { + return WrappedComponent.name || 'Component' +} + +const defaultMapStateToProps = () => ({}) +export default function connect (mapStateToProps) { + const shouldSubscribe = !!mapStateToProps + const finnalMapStateToProps = mapStateToProps || defaultMapStateToProps + return function wrapWithConnect (WrappedComponent) { + const tempProps = omit(WrappedComponent.props || {}, ['store']) + const props = {} + Object.keys(tempProps).forEach(k => { props[k] = PropTypes.any }) + const Connect = { + name: `Connect_${getDisplayName(WrappedComponent)}`, + props, + inject: { + _store: { default: {}}, + }, + data () { + this.store = this._store.store + return { + subscribed: finnalMapStateToProps(this.store.getState(), this.$props), + } + }, + mounted () { + this.trySubscribe() + }, + + beforeDestroy () { + this.tryUnsubscribe() + }, + methods: { + handleChange () { + if (!this.unsubscribe) { + return + } + + const nextState = finnalMapStateToProps(this.store.getState(), this.$props) + if (!shallowEqual(this.nextState, nextState)) { + this.nextState = nextState + this.subscribed = nextState + } + }, + + trySubscribe () { + if (shouldSubscribe) { + this.unsubscribe = this.store.subscribe(this.handleChange) + this.handleChange() + } + }, + + tryUnsubscribe () { + if (this.unsubscribe) { + this.unsubscribe() + this.unsubscribe = null + } + }, + }, + render () { + const { $listeners, $slots, $attrs, $scopedSlots, subscribed, store } = this + const props = getOptionProps(this) + const wrapProps = { + props: { + ...props, + ...subscribed, + store, + }, + on: $listeners, + attrs: $attrs, + slots: $slots, + scopedSlots: $scopedSlots, + } + return ( + + ) + }, + } + return Connect + } +} diff --git a/components/_util/store/create.js b/components/_util/store/create.js new file mode 100644 index 000000000..57ce3e083 --- /dev/null +++ b/components/_util/store/create.js @@ -0,0 +1,30 @@ +export default function create (initialState) { + let state = initialState + const listeners = [] + + function setState (partial) { + state = { ...state, ...partial } + for (let i = 0; i < listeners.length; i++) { + listeners[i]() + } + } + + function getState () { + return state + } + + function subscribe (listener) { + listeners.push(listener) + + return function unsubscribe () { + const index = listeners.indexOf(listener) + listeners.splice(index, 1) + } + } + + return { + setState, + getState, + subscribe, + } +} diff --git a/components/_util/store/index.js b/components/_util/store/index.js new file mode 100644 index 000000000..62d58a9a3 --- /dev/null +++ b/components/_util/store/index.js @@ -0,0 +1,5 @@ +export { default as Provider } from './Provider' + +export { default as connect } from './connect' + +export { default as create } from './create' diff --git a/components/checkbox/Checkbox.jsx b/components/checkbox/Checkbox.jsx index 722ed86e9..3056fc5c1 100644 --- a/components/checkbox/Checkbox.jsx +++ b/components/checkbox/Checkbox.jsx @@ -20,7 +20,6 @@ export default { }, inject: { checkboxGroupContext: { default: null }, - test: { default: null }, }, data () { const { checkboxGroupContext, checked, defaultChecked, value } = this diff --git a/components/dropdown/index.en-US.md b/components/dropdown/index.en-US.md index 4c872bd8c..e0f7c5b56 100644 --- a/components/dropdown/index.en-US.md +++ b/components/dropdown/index.en-US.md @@ -8,7 +8,7 @@ | getPopupContainer | to set the container of the dropdown menu. The default is to create a `div` element in `body`, you can reset it to the scrolling area and make a relative reposition. [example](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | `() => document.body` | | overlay(slot) | the dropdown menu | [Menu](#/us/components/menu) | - | | placement | placement of pop menu: `bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` | -| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` | +| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` | | visible(v-model) | whether the dropdown menu is visible | boolean | - | ### events @@ -30,7 +30,7 @@ You should use [Menu](#/us/components/menu/) as `overlay`. The menu items and di | overlay(slot) | the dropdown menu | [Menu](#/us/components/menu) | - | | placement | placement of pop menu: `bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` | | size | size of the button, the same as [Button](#/us/components/button) | string | `default` | -| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` | +| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` | | type | type of the button, the same as [Button](#/us/components/button) | string | `default` | | visible | whether the dropdown menu is visible | boolean | - | diff --git a/components/dropdown/index.zh-CN.md b/components/dropdown/index.zh-CN.md index 8c4f839ff..b65b5cf1d 100644 --- a/components/dropdown/index.zh-CN.md +++ b/components/dropdown/index.zh-CN.md @@ -8,7 +8,7 @@ | getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 | Function(triggerNode) | `() => document.body` | | overlay(slot) | 菜单 | [Menu](#/cn/components/menu) | - | | placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` | -| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` | +| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` | | visible(v-model) | 菜单是否显示 | boolean | - | `overlay` 菜单使用 [Menu](#/cn/components/menu/),还包括菜单项 `Menu.Item`,分割线 `Menu.Divider`。 @@ -31,7 +31,7 @@ | overlay(slot) | 菜单 | [Menu](#/cn/components/menu/) | - | | placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` | | size | 按钮大小,和 [Button](#/cn/components/button/) 一致 | string | 'default' | -| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` | +| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` | | type | 按钮类型,和 [Button](#/cn/components/button/) 一致 | string | 'default' | | visible(v-model) | 菜单是否显示 | boolean | - | diff --git a/components/trigger/index.md b/components/trigger/index.md index 69f9cb643..bae1a0f2a 100644 --- a/components/trigger/index.md +++ b/components/trigger/index.md @@ -40,7 +40,7 @@ action string[] ['hover'] - which actions cause popup shown. enum of 'hover','click','focus','contextMenu' + which actions cause popup shown. enum of 'hover','click','focus','contextmenu' mouseEnterDelay diff --git a/components/vc-table/assets/animation.less b/components/vc-table/assets/animation.less new file mode 100644 index 000000000..a6d4e9281 --- /dev/null +++ b/components/vc-table/assets/animation.less @@ -0,0 +1,59 @@ + +.move-enter, .move-appear { + opacity: 0; + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-duration: 2.5s; + animation-fill-mode: both; + animation-play-state: paused; +} + +.move-leave { + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-duration: .5s; + animation-fill-mode: both; + animation-play-state: paused; +} + +.move-enter.move-enter-active, .move-appear.move-enter-active { + animation-name: moveLeftIn; + animation-play-state: running; +} + +.move-leave.move-leave-active { + animation-name: moveRightOut; + animation-play-state: running; +} + +@keyframes moveLeftIn { + 0% { + transform-origin: 0 0; + transform: translateX(30px); + opacity: 0; + background: #fff6de; + } + 20% { + transform-origin: 0 0; + transform: translateX(0); + opacity: 1; + } + 80%{ + background: #fff6de; + } + 100%{ + background: transparent; + opacity: 1; + } +} + +@keyframes moveRightOut { + 0% { + transform-origin: 0 0; + transform: translateX(0); + opacity: 1; + } + 100% { + transform-origin: 0 0; + transform: translateX(-30px); + opacity: 0; + } +} diff --git a/components/vc-table/assets/bordered.less b/components/vc-table/assets/bordered.less new file mode 100644 index 000000000..f3ad2e84a --- /dev/null +++ b/components/vc-table/assets/bordered.less @@ -0,0 +1,11 @@ +@tablePrefixCls: rc-table; +@table-border-color: #e9e9e9; + +.@{tablePrefixCls}.bordered { + table { + border-collapse: collapse; + } + th, td { + border: 1px solid @table-border-color; + } +} diff --git a/components/vc-table/assets/index.less b/components/vc-table/assets/index.less new file mode 100644 index 000000000..f6c6faa4a --- /dev/null +++ b/components/vc-table/assets/index.less @@ -0,0 +1,227 @@ +@import 'normalize.css'; + +@tablePrefixCls: rc-table; +@text-color : #666; +@font-size-base : 12px; +@line-height: 1.5; +@table-border-color: #e9e9e9; +@table-head-background-color: #f7f7f7; +@vertical-padding: 16px; +@horizontal-padding: 8px; + +.@{tablePrefixCls} { + font-size: @font-size-base; + color: @text-color; + transition: opacity 0.3s ease; + position: relative; + line-height: @line-height; + overflow: hidden; + + .@{tablePrefixCls}-scroll { + overflow: auto; + table { + width: auto; + min-width: 100%; + } + } + + .@{tablePrefixCls}-header { + overflow: hidden; + background: @table-head-background-color; + } + + &-fixed-header &-body { + background: #fff; + position: relative; + } + + &-fixed-header &-body-inner { + height: 100%; + overflow: scroll; + } + + &-fixed-header &-scroll &-header { + overflow-x: scroll; + padding-bottom: 20px; + margin-bottom: -20px; + overflow-y: scroll; + box-sizing: border-box; + } + + .@{tablePrefixCls}-title { + padding: @vertical-padding @horizontal-padding; + border-top: 1px solid @table-border-color; + } + + .@{tablePrefixCls}-content { + position: relative; + } + + .@{tablePrefixCls}-footer { + padding: @vertical-padding @horizontal-padding; + border-bottom: 1px solid @table-border-color; + } + + .@{tablePrefixCls}-placeholder { + padding: 16px 8px; + background: #fff; + border-bottom: 1px solid @table-border-color; + text-align: center; + position: relative; + &-fixed-columns { + position: absolute; + bottom: 0; + width: 100%; + background: transparent; + pointer-events: none; + } + } + + table { + width: 100%; + border-collapse: separate; + text-align: left; + } + + th { + background: @table-head-background-color; + font-weight: bold; + transition: background .3s ease; + } + + td { + border-bottom: 1px solid @table-border-color; + &:empty:after { + content: '.'; // empty cell placeholder + visibility: hidden; + } + } + + tr { + transition: all .3s ease; + &:hover { + background: #eaf8fe; + } + &.@{tablePrefixCls}-row-hover { + background: #eaf8fe; + } + } + + th, td { + padding: @vertical-padding @horizontal-padding; + white-space: nowrap; + } +} + +.@{tablePrefixCls} { + &-expand-icon-col { + width: 34px; + } + &-row, &-expanded-row { + &-expand-icon { + cursor: pointer; + display: inline-block; + width: 16px; + height: 16px; + text-align: center; + line-height: 16px; + border: 1px solid @table-border-color; + user-select: none; + background: #fff; + } + &-spaced { + visibility: hidden; + } + &-spaced:after { + content: '.' + } + + &-expanded:after { + content: '-' + } + + &-collapsed:after { + content: '+' + } + } + tr&-expanded-row { + background: #f7f7f7; + &:hover { + background: #f7f7f7; + } + } + &-column-hidden { + display: none; + } + &-prev-columns-page, + &-next-columns-page { + cursor: pointer; + color: #666; + z-index: 1; + &:hover { + color: #2db7f5; + } + &-disabled { + cursor: not-allowed; + color: #999; + &:hover { + color: #999; + } + } + } + &-prev-columns-page { + margin-right: 8px; + &:before { + content: '<'; + } + } + &-next-columns-page { + float: right; + &:before { + content: '>'; + } + } + + &-fixed-left, + &-fixed-right { + position: absolute; + top: 0; + overflow: hidden; + table { + width: auto; + background: #fff; + } + } + + &-fixed-left { + left: 0; + box-shadow: 4px 0 4px rgba(100, 100, 100, 0.1); + & .@{tablePrefixCls}-body-inner { + margin-right: -20px; + padding-right: 20px; + } + .@{tablePrefixCls}-fixed-header & .@{tablePrefixCls}-body-inner { + padding-right: 0; + } + } + + &-fixed-right { + right: 0; + box-shadow: -4px 0 4px rgba(100, 100, 100, 0.1); + + // hide expand row content in right fixed Table + // https://github.com/ant-design/ant-design/issues/1898 + .@{tablePrefixCls}-expanded-row { + color: transparent; + pointer-events: none; + } + } + + &&-scroll-position-left &-fixed-left { + box-shadow: none; + } + + &&-scroll-position-right &-fixed-right { + box-shadow: none; + } +} diff --git a/components/vc-table/assets/normalize.css b/components/vc-table/assets/normalize.css new file mode 100644 index 000000000..a5b29a0c0 --- /dev/null +++ b/components/vc-table/assets/normalize.css @@ -0,0 +1,220 @@ +/*! normalize.css v3.0.1 | MIT License | git.io/normalize */ + +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100% +} + +body { + margin: 0 +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block +} + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline +} + +audio:not([controls]) { + display: none; + height: 0 +} + +[hidden], +template { + display: none +} + +a { + background: 0 0 +} + +a:active, +a:hover { + outline: 0 +} + +abbr[title] { + border-bottom: 1px dotted +} + +b, +strong { + font-weight: 700 +} + +dfn { + font-style: italic +} + +h1 { + font-size: 2em; + margin: .67em 0 +} + +mark { + background: #ff0; + color: #000 +} + +small { + font-size: 80% +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline +} + +sup { + top: -.5em +} + +sub { + bottom: -.25em +} + +img { + border: 0 +} + +svg:not(:root) { + overflow: hidden +} + +figure { + margin: 1em 40px +} + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0 +} + +pre { + overflow: auto +} + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em +} + +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0 +} + +button { + overflow: visible +} + +button, +select { + text-transform: none +} + +button, +html input[type=button], +input[type=reset], +input[type=submit] { + -webkit-appearance: button; + cursor: pointer +} + +button[disabled], +html input[disabled] { + cursor: default +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} + +input { + line-height: normal +} + +input[type=checkbox], +input[type=radio] { + box-sizing: border-box; + padding: 0 +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + height: auto +} + +input[type=search] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box +} + +input[type=search]::-webkit-search-cancel-button, +input[type=search]::-webkit-search-decoration { + -webkit-appearance: none +} + +fieldset { + border: 1px solid silver; + margin: 0 2px; + padding: .35em .625em .75em +} + +legend { + border: 0; + padding: 0 +} + +textarea { + overflow: auto +} + +optgroup { + font-weight: 700 +} + +table { + border-collapse: collapse; + border-spacing: 0 +} + +td, +th { + padding: 0 +} \ No newline at end of file diff --git a/components/vc-table/demo/childrenIndent.js b/components/vc-table/demo/childrenIndent.js new file mode 100644 index 000000000..f436835d7 --- /dev/null +++ b/components/vc-table/demo/childrenIndent.js @@ -0,0 +1,93 @@ +/* eslint-disable no-console,func-names,react/no-multi-comp */ +import Table from '../index' +import '../assets/index.less' + +const columns = [{ + title: 'Name', + dataIndex: 'name', + key: 'name', + width: 400, +}, { + title: 'Age', + dataIndex: 'age', + key: 'age', + width: 100, +}, { + title: 'Address', + dataIndex: 'address', + key: 'address', + width: 200, +}, { + title: 'Operations', + dataIndex: 'operation', + key: 'x', + width: 150, +}] + +const data = [{ + key: 1, + name: 'a', + age: 32, + address: 'I am a', + children: [{ + key: 11, + name: 'aa', + age: 33, + address: 'I am aa', + }, { + key: 12, + name: 'ab', + age: 33, + address: 'I am ab', + children: [{ + key: 121, + name: 'aba', + age: 33, + address: 'I am aba', + }], + }, { + key: 13, + name: 'ac', + age: 33, + address: 'I am ac', + children: [{ + key: 131, + name: 'aca', + age: 33, + address: 'I am aca', + children: [{ + key: 1311, + name: 'acaa', + age: 33, + address: 'I am acaa', + }, { + key: 1312, + name: 'acab', + age: 33, + address: 'I am acab', + }], + }], + }], +}, { + key: 2, + name: 'b', + age: 32, + address: 'I am b', +}] + +function onExpand (expanded, record) { + console.log('onExpand', expanded, record) +} +export default { + render () { + return ( + + ) + }, +} + diff --git a/components/vc-table/demo/className.js b/components/vc-table/demo/className.js new file mode 100644 index 000000000..9413ea545 --- /dev/null +++ b/components/vc-table/demo/className.js @@ -0,0 +1,45 @@ +/* eslint-disable no-console,func-names,react/no-multi-comp */ +import Table from '../index' +import '../assets/index.less' + +const data = [ + { a: '123', key: '1' }, + { a: 'cdd', b: 'edd', key: '2' }, + { a: '1333', c: 'eee', d: 2, key: '3' }, +] +export default { + render () { + const columns = [ + { title: 'title1', dataIndex: 'a', + className: 'a', + key: 'a', width: 100 }, + { id: '123', title: 'title2', dataIndex: 'b', + className: 'b', + key: 'b', width: 100 }, + { title: 'title3', dataIndex: 'c', + className: 'c', + key: 'c', width: 200 }, + { + title: 'Operations', dataIndex: '', + className: 'd', + key: 'd', render (h) { + return Operations + }, + }, + ] + return ( +
+

rowClassName and className

+
`row-${i}`} + expandedRowRender={record =>

extra: {record.a}

} + expandedRowClassName={(record, i) => `ex-row-${i}`} + data={data} + class='table' + /> + + ) + }, +} + diff --git a/components/vc-table/index.js b/components/vc-table/index.js new file mode 100644 index 000000000..c4a842cdc --- /dev/null +++ b/components/vc-table/index.js @@ -0,0 +1,9 @@ +import Table from './src/Table' +import Column from './src/Column' +import ColumnGroup from './src/ColumnGroup' + +Table.Column = Column +Table.ColumnGroup = ColumnGroup + +export default Table +export { Column, ColumnGroup } diff --git a/components/vc-table/src/BaseTable.jsx b/components/vc-table/src/BaseTable.jsx new file mode 100644 index 000000000..93a624224 --- /dev/null +++ b/components/vc-table/src/BaseTable.jsx @@ -0,0 +1,191 @@ + +import PropTypes from '../../_util/vue-types' +import ColGroup from './ColGroup' +import TableHeader from './TableHeader' +import TableRow from './TableRow' +import ExpandableRow from './ExpandableRow' +import { mergeProps } from '../../_util/props-util' +import { connect } from '../../_util/store' +function noop () {} +const BaseTable = { + name: 'BaseTable', + props: { + fixed: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + columns: PropTypes.array.isRequired, + tableClassName: PropTypes.string.isRequired, + hasHead: PropTypes.bool.isRequired, + hasBody: PropTypes.bool.isRequired, + store: PropTypes.object.isRequired, + expander: PropTypes.object.isRequired, + getRowKey: PropTypes.func, + isAnyColumnsFixed: PropTypes.bool, + }, + inject: { + table: { default: {}}, + }, + methods: { + handleRowHover (isHover, key) { + this.props.store.setState({ + currentHoverKey: isHover ? key : null, + }) + }, + + renderRows (renderData, indent, ancestorKeys = []) { + const { + columnManager, sComponents: components, + prefixCls, + childrenColumnName, + rowClassName, + // rowRef, + $listeners: { + rowClick: onRowClick = noop, + rowDoubleclick: onRowDoubleClick = noop, + rowContextmenu: onRowContextMenu = noop, + rowMouseenter: onRowMouseEnter = noop, + rowMouseleave: onRowMouseLeave = noop, + row: onRow = noop, + }, + } = this.table + const { getRowKey, fixed, expander, isAnyColumnsFixed } = this + + const rows = [] + + for (let i = 0; i < renderData.length; i++) { + const record = renderData[i] + const key = getRowKey(record, i) + const className = typeof rowClassName === 'string' + ? rowClassName + : rowClassName(record, i, indent) + + const onHoverProps = {} + if (columnManager.isAnyColumnsFixed()) { + onHoverProps.hover = this.handleRowHover + } + + let leafColumns + if (fixed === 'left') { + leafColumns = columnManager.leftLeafColumns() + } else if (fixed === 'right') { + leafColumns = columnManager.rightLeafColumns() + } else { + leafColumns = columnManager.leafColumns() + } + + const rowPrefixCls = `${prefixCls}-row` + const expandableRowProps = { + props: { + ...expander.props, + fixed, + index: i, + prefixCls: rowPrefixCls, + record, + rowKey: key, + needIndentSpaced: expander.needIndentSpaced, + }, + key, + on: { + // ...expander.on, + rowClick: onRowClick, + expandedChange: expander.handleExpandChange, + }, + scopedSlots: { + default: (expandableRow) => { + const tableRowProps = mergeProps({ + props: { + fixed, + indent, + record, + index: i, + prefixCls: rowPrefixCls, + childrenColumnName: childrenColumnName, + columns: leafColumns, + rowKey: key, + ancestorKeys, + components, + isAnyColumnsFixed, + }, + on: { + row: onRow, + rowDoubleclick: onRowDoubleClick, + rowContextmenu: onRowContextMenu, + rowMouseenter: onRowMouseEnter, + rowMouseleave: onRowMouseLeave, + ...onHoverProps, + }, + class: className, + ref: `row_${i}_${indent}`, + }, expandableRow) + return ( + + ) + }, + }, + } + const row = ( + + ) + + rows.push(row) + + expander.renderRows( + this.renderRows, + rows, + record, + i, + indent, + fixed, + key, + ancestorKeys + ) + } + return rows + }, + }, + + render () { + const { sComponents: components, prefixCls, scroll, data, getBodyWrapper } = this.table + const { expander, tableClassName, hasHead, hasBody, fixed, columns } = this + const tableStyle = {} + + if (!fixed && scroll.x) { + // not set width, then use content fixed width + if (scroll.x === true) { + tableStyle.tableLayout = 'fixed' + } else { + tableStyle.width = scroll.x + } + } + + const Table = hasBody ? components.table : 'table' + const BodyWrapper = components.body.wrapper + + let body + if (hasBody) { + body = ( + + {this.renderRows(data, 0)} + + ) + if (getBodyWrapper) { + body = getBodyWrapper(body) + } + } + + return ( +
+ + {hasHead && } + {body} +
+ ) + }, +} + +export default connect()(BaseTable) diff --git a/components/vc-table/src/BodyTable.jsx b/components/vc-table/src/BodyTable.jsx new file mode 100644 index 000000000..6cb8d69be --- /dev/null +++ b/components/vc-table/src/BodyTable.jsx @@ -0,0 +1,117 @@ +import PropTypes from '../../_util/vue-types' +import { measureScrollbar } from './utils' +import BaseTable from './BaseTable' + +export default { + name: 'BodyTable', + props: { + fixed: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + columns: PropTypes.array.isRequired, + tableClassName: PropTypes.string.isRequired, + handleBodyScroll: PropTypes.func.isRequired, + getRowKey: PropTypes.func.isRequired, + expander: PropTypes.object.isRequired, + isAnyColumnsFixed: PropTypes.bool, + }, + inject: { + table: { default: {}}, + }, + render () { + const { prefixCls, scroll } = this.table + const { + columns, + fixed, + tableClassName, + getRowKey, + handleBodyScroll, + expander, + isAnyColumnsFixed, + } = this + let { useFixedHeader } = this.table + const bodyStyle = { ...this.table.bodyStyle } + const innerBodyStyle = {} + + if (scroll.x || fixed) { + bodyStyle.overflowX = bodyStyle.overflowX || 'auto' + // Fix weired webkit render bug + // https://github.com/ant-design/ant-design/issues/7783 + bodyStyle.WebkitTransform = 'translate3d (0, 0, 0)' + } + + if (scroll.y) { + // maxHeight will make fixed-Table scrolling not working + // so we only set maxHeight to body-Table here + if (fixed) { + innerBodyStyle.maxHeight = bodyStyle.maxHeight || scroll.y + innerBodyStyle.overflowY = bodyStyle.overflowY || 'scroll' + } else { + bodyStyle.maxHeight = bodyStyle.maxHeight || scroll.y + } + bodyStyle.overflowY = bodyStyle.overflowY || 'scroll' + useFixedHeader = true + + // Add negative margin bottom for scroll bar overflow bug + const scrollbarWidth = measureScrollbar() + if (scrollbarWidth > 0 && fixed) { + bodyStyle.marginBottom = `-${scrollbarWidth}px` + bodyStyle.paddingBottom = '0px' + } + } + + const baseTable = ( + + ) + + if (fixed && columns.length) { + let refName + if (columns[0].fixed === 'left' || columns[0].fixed === true) { + refName = 'fixedColumnsBodyLeft' + } else if (columns[0].fixed === 'right') { + refName = 'fixedColumnsBodyRight' + } + delete bodyStyle.overflowX + delete bodyStyle.overflowY + return ( +
+
+ {baseTable} +
+
+ ) + } + return ( +
+ {baseTable} +
+ ) + }, + +} + diff --git a/components/vc-table/src/ColGroup.jsx b/components/vc-table/src/ColGroup.jsx new file mode 100644 index 000000000..9f169e57a --- /dev/null +++ b/components/vc-table/src/ColGroup.jsx @@ -0,0 +1,55 @@ +import PropTypes from '../../_util/vue-types' + +export default { + name: 'ColGroup', + props: { + fixed: PropTypes.string, + columns: PropTypes.array, + }, + inject: { + table: { default: {}}, + }, + render () { + const { fixed, table } = this + const { prefixCls, expandIconAsCell, columnManager } = table + + let cols = [] + + if (expandIconAsCell && fixed !== 'right') { + cols.push( + + ) + } + + let leafColumns + + if (fixed === 'left') { + leafColumns = columnManager.leftLeafColumns() + } else if (fixed === 'right') { + leafColumns = columnManager.rightLeafColumns() + } else { + leafColumns = columnManager.leafColumns() + } + cols = cols.concat( + leafColumns.map(c => { + const width = typeof c.width === 'number' ? `${c.width}px` : c.width + return ( + + ) + }) + ) + return ( + + {cols} + + ) + }, + +} + diff --git a/components/vc-table/src/Column.jsx b/components/vc-table/src/Column.jsx new file mode 100644 index 000000000..8ad12648c --- /dev/null +++ b/components/vc-table/src/Column.jsx @@ -0,0 +1,23 @@ +import PropTypes from '../../_util/vue-types' + +export default { + name: 'Column', + props: { + colSpan: PropTypes.number, + title: PropTypes.any, + dataIndex: PropTypes.string, + width: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + fixed: PropTypes.oneOf([ + true, + 'left', + 'right', + ]), + render: PropTypes.func, + // onCellClick: PropTypes.func, + // onCell: PropTypes.func, + // onHeaderCell: PropTypes.func, + }, +} diff --git a/components/vc-table/src/ColumnGroup.jsx b/components/vc-table/src/ColumnGroup.jsx new file mode 100644 index 000000000..ad329cd15 --- /dev/null +++ b/components/vc-table/src/ColumnGroup.jsx @@ -0,0 +1,9 @@ +import PropTypes from '../../_util/vue-types' + +export default { + name: 'ColumnGroup', + props: { + title: PropTypes.any, + }, + isTableColumnGroup: true, +} diff --git a/components/vc-table/src/ColumnManager.jsx b/components/vc-table/src/ColumnManager.jsx new file mode 100644 index 000000000..b0696baaf --- /dev/null +++ b/components/vc-table/src/ColumnManager.jsx @@ -0,0 +1,149 @@ +export default class ColumnManager { + constructor (columns, elements) { + this.columns = columns || this.normalize(elements) + this._cached = {} + } + + isAnyColumnsFixed () { + return this._cache('isAnyColumnsFixed', () => { + return this.columns.some(column => !!column.fixed) + }) + } + + isAnyColumnsLeftFixed () { + return this._cache('isAnyColumnsLeftFixed', () => { + return this.columns.some( + column => column.fixed === 'left' || column.fixed === true + ) + }) + } + + isAnyColumnsRightFixed () { + return this._cache('isAnyColumnsRightFixed', () => { + return this.columns.some( + column => column.fixed === 'right' + ) + }) + } + + leftColumns () { + return this._cache('leftColumns', () => { + return this.groupedColumns().filter( + column => column.fixed === 'left' || column.fixed === true + ) + }) + } + + rightColumns () { + return this._cache('rightColumns', () => { + return this.groupedColumns().filter( + column => column.fixed === 'right' + ) + }) + } + + leafColumns () { + return this._cache('leafColumns', () => + this._leafColumns(this.columns) + ) + } + + leftLeafColumns () { + return this._cache('leftLeafColumns', () => + this._leafColumns(this.leftColumns()) + ) + } + + rightLeafColumns () { + return this._cache('rightLeafColumns', () => + this._leafColumns(this.rightColumns()) + ) + } + + // add appropriate rowspan and colspan to column + groupedColumns () { + return this._cache('groupedColumns', () => { + const _groupColumns = (columns, currentRow = 0, parentColumn = {}, rows = []) => { + // track how many rows we got + rows[currentRow] = rows[currentRow] || [] + const grouped = [] + const setRowSpan = column => { + const rowSpan = rows.length - currentRow + if (column && + !column.children && // parent columns are supposed to be one row + rowSpan > 1 && + (!column.rowSpan || column.rowSpan < rowSpan) + ) { + column.rowSpan = rowSpan + } + } + columns.forEach((column, index) => { + const newColumn = { ...column } + rows[currentRow].push(newColumn) + parentColumn.colSpan = parentColumn.colSpan || 0 + if (newColumn.children && newColumn.children.length > 0) { + newColumn.children = _groupColumns(newColumn.children, currentRow + 1, newColumn, rows) + parentColumn.colSpan = parentColumn.colSpan + newColumn.colSpan + } else { + parentColumn.colSpan++ + } + // update rowspan to all same row columns + for (let i = 0; i < rows[currentRow].length - 1; ++i) { + setRowSpan(rows[currentRow][i]) + } + // last column, update rowspan immediately + if (index + 1 === columns.length) { + setRowSpan(newColumn) + } + grouped.push(newColumn) + }) + return grouped + } + return _groupColumns(this.columns) + }) + } + + normalize (elements) { + const columns = [] + elements.forEach(element => { + if (!element.tag) { + return + } + debugger + const column = { ...element.props } + if (element.key) { + column.key = element.key + } + if (element.type.isTableColumnGroup) { + column.children = this.normalize(column.children) + } + columns.push(column) + }) + return columns + } + + reset (columns, elements) { + this.columns = columns || this.normalize(elements) + this._cached = {} + } + + _cache (name, fn) { + if (name in this._cached) { + return this._cached[name] + } + this._cached[name] = fn() + return this._cached[name] + } + + _leafColumns (columns) { + const leafColumns = [] + columns.forEach(column => { + if (!column.children) { + leafColumns.push(column) + } else { + leafColumns.push(...this._leafColumns(column.children)) + } + }) + return leafColumns + } +} diff --git a/components/vc-table/src/ExpandIcon.jsx b/components/vc-table/src/ExpandIcon.jsx new file mode 100644 index 000000000..490386d43 --- /dev/null +++ b/components/vc-table/src/ExpandIcon.jsx @@ -0,0 +1,34 @@ +import PropTypes from '../../_util/vue-types' +import BaseMixin from '../../_util/BaseMixin' +export default { + mixins: [BaseMixin], + name: 'ExpandIcon', + props: { + record: PropTypes.object, + prefixCls: PropTypes.string, + expandable: PropTypes.any, + expanded: PropTypes.bool, + needIndentSpaced: PropTypes.bool, + }, + methods: { + onExpand (e) { + this.__emit('expand', this.record, e) + }, + }, + + render () { + const { expandable, prefixCls, onExpand, needIndentSpaced, expanded } = this + if (expandable) { + const expandClassName = expanded ? 'expanded' : 'collapsed' + return ( + + ) + } else if (needIndentSpaced) { + return + } + return null + }, +} diff --git a/components/vc-table/src/ExpandableRow.jsx b/components/vc-table/src/ExpandableRow.jsx new file mode 100644 index 000000000..cfd431197 --- /dev/null +++ b/components/vc-table/src/ExpandableRow.jsx @@ -0,0 +1,128 @@ +import PropTypes from '../../_util/vue-types' +import ExpandIcon from './ExpandIcon' +import BaseMixin from '../../_util/BaseMixin' +import { connect } from '../../_util/store' + +const ExpandableRow = { + mixins: [BaseMixin], + name: 'ExpandableRow', + props: { + prefixCls: PropTypes.string.isRequired, + rowKey: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + fixed: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + record: PropTypes.object.isRequired, + indentSize: PropTypes.number, + needIndentSpaced: PropTypes.bool.isRequired, + expandRowByClick: PropTypes.bool, + expanded: PropTypes.bool.isRequired, + expandIconAsCell: PropTypes.bool, + expandIconColumnIndex: PropTypes.number, + childrenColumnName: PropTypes.string, + expandedRowRender: PropTypes.func, + // onExpandedChange: PropTypes.func.isRequired, + // onRowClick: PropTypes.func, + // children: PropTypes.func.isRequired, + }, + + beforeDestroy () { + this.handleDestroy() + }, + methods: { + hasExpandIcon (columnIndex) { + const { expandRowByClick } = this + return !this.expandIconAsCell && + !expandRowByClick && + columnIndex === this.expandIconColumnIndex + }, + + handleExpandChange (record, event) { + const { expanded, rowKey } = this + this.__emit('expandedChange', !expanded, record, event, rowKey) + }, + + handleDestroy () { + const { rowKey, record } = this + this.__emit('expandedChange', false, record, null, rowKey, true) + }, + + handleRowClick (record, index, event) { + const { expandRowByClick } = this + if (expandRowByClick) { + this.handleExpandChange(record, event) + } + this.__emit('rowClick', record, index, event) + }, + + renderExpandIcon () { + const { prefixCls, expanded, record, needIndentSpaced } = this + + return ( + + ) + }, + + renderExpandIconCell (cells) { + if (!this.expandIconAsCell) { + return + } + const { prefixCls } = this + + cells.push( + + {this.renderExpandIcon()} + + ) + }, + }, + + render () { + const { + childrenColumnName, + expandedRowRender, + indentSize, + record, + fixed, + $scopedSlots, + } = this + + this.expandIconAsCell = fixed !== 'right' ? this.expandIconAsCell : false + this.expandIconColumnIndex = fixed !== 'right' ? this.expandIconColumnIndex : -1 + const childrenData = record[childrenColumnName] + this.expandable = !!(childrenData || expandedRowRender) + const expandableRowProps = { + props: { + indentSize, + hasExpandIcon: this.hasExpandIcon, + renderExpandIcon: this.renderExpandIcon, + renderExpandIconCell: this.renderExpandIconCell, + }, + + on: { + rowClick: this.handleRowClick, + }, + + } + + return $scopedSlots.default && $scopedSlots.default(expandableRowProps) + }, +} + +export default connect(({ expandedRowKeys }, { rowKey }) => ({ + expanded: !!~expandedRowKeys.indexOf(rowKey), +}))(ExpandableRow) diff --git a/components/vc-table/src/ExpandableTable.jsx b/components/vc-table/src/ExpandableTable.jsx new file mode 100644 index 000000000..fc8402b3c --- /dev/null +++ b/components/vc-table/src/ExpandableTable.jsx @@ -0,0 +1,223 @@ +import PropTypes from '../../_util/vue-types' +import BaseMixin from '../../_util/BaseMixin' +import { connect } from '../../_util/store' +import TableRow from './TableRow' +import { remove } from './utils' +import { initDefaultProps, getOptionProps } from '../../_util/props-util' + +export const ExpandableTableProps = () => ({ + expandIconAsCell: PropTypes.bool, + expandedRowKeys: PropTypes.array, + expandedRowClassName: PropTypes.func, + defaultExpandAllRows: PropTypes.bool, + defaultExpandedRowKeys: PropTypes.array, + expandIconColumnIndex: PropTypes.number, + expandedRowRender: PropTypes.func, + childrenColumnName: PropTypes.string, + indentSize: PropTypes.number, + // onExpand: PropTypes.func, + // onExpandedRowsChange: PropTypes.func, + columnManager: PropTypes.object.isRequired, + store: PropTypes.object.isRequired, + prefixCls: PropTypes.string.isRequired, + data: PropTypes.array, + getRowKey: PropTypes.func, +}) + +const ExpandableTable = { + name: 'ExpandableTable', + mixins: [BaseMixin], + props: initDefaultProps(ExpandableTableProps(), { + expandIconAsCell: false, + expandedRowClassName: () => '', + expandIconColumnIndex: 0, + defaultExpandAllRows: false, + defaultExpandedRowKeys: [], + childrenColumnName: 'children', + indentSize: 15, + }), + + data () { + const { + data, + childrenColumnName, + defaultExpandAllRows, + expandedRowKeys, + defaultExpandedRowKeys, + getRowKey, + } = this + + let finnalExpandedRowKeys = [] + let rows = [...data] + + if (defaultExpandAllRows) { + for (let i = 0; i < rows.length; i++) { + const row = rows[i] + finnalExpandedRowKeys.push(getRowKey(row, i)) + rows = rows.concat(row[childrenColumnName] || []) + } + } else { + finnalExpandedRowKeys = expandedRowKeys || defaultExpandedRowKeys + } + + // this.columnManager = props.columnManager + // this.store = props.store + + this.store.setState({ + expandedRowsHeight: {}, + expandedRowKeys: finnalExpandedRowKeys, + }) + return {} + }, + watch: { + expandedRowKeys (val) { + this.store.setState({ + expandedRowKeys: val, + }) + }, + }, + methods: { + handleExpandChange (expanded, record, event, rowKey, destroy = false) { + if (event) { + event.preventDefault() + event.stopPropagation() + } + + let { expandedRowKeys } = this.store.getState() + + if (expanded) { + // row was expaned + expandedRowKeys = [...expandedRowKeys, rowKey] + } else { + // row was collapse + const expandedRowIndex = expandedRowKeys.indexOf(rowKey) + if (expandedRowIndex !== -1) { + expandedRowKeys = remove(expandedRowKeys, rowKey) + } + } + + if (!this.expandedRowKeys) { + this.store.setState({ expandedRowKeys }) + } + this.__emit('expandedRowsChange', expandedRowKeys) + if (!destroy) { + this.__emit('expand', expanded, record) + } + }, + + renderExpandIndentCell (rows, fixed) { + const { prefixCls, expandIconAsCell } = this + if (!expandIconAsCell || fixed === 'right' || !rows.length) { + return + } + + const iconColumn = { + key: 'rc-table-expand-icon-cell', + className: `${prefixCls}-expand-icon-th`, + title: '', + rowSpan: rows.length, + } + + rows[0].unshift({ ...iconColumn, column: iconColumn }) + }, + + renderExpandedRow (record, index, render, className, ancestorKeys, indent, fixed) { + const { prefixCls, expandIconAsCell, indentSize } = this + let colCount + if (fixed === 'left') { + colCount = this.columnManager.leftLeafColumns().length + } else if (fixed === 'right') { + colCount = this.columnManager.rightLeafColumns().length + } else { + colCount = this.columnManager.leafColumns().length + } + const columns = [{ + key: 'extra-row', + render: () => ({ + props: { + colSpan: colCount, + }, + children: fixed !== 'right' ? render(record, index, indent) : ' ', + }), + }] + if (expandIconAsCell && fixed !== 'right') { + columns.unshift({ + key: 'expand-icon-placeholder', + render: () => null, + }) + } + const parentKey = ancestorKeys[ancestorKeys.length - 1] + const rowKey = `${parentKey}-extra-row` + const components = { + body: { + row: 'tr', + cell: 'td', + }, + } + return ( + {}} + /> + ) + }, + + renderRows (renderRows, rows, record, index, indent, fixed, parentKey, ancestorKeys) { + const { expandedRowClassName, expandedRowRender, childrenColumnName } = this + const childrenData = record[childrenColumnName] + const nextAncestorKeys = [...ancestorKeys, parentKey] + const nextIndent = indent + 1 + + if (expandedRowRender) { + rows.push( + this.renderExpandedRow( + record, + index, + expandedRowRender, + expandedRowClassName(record, index, indent), + nextAncestorKeys, + nextIndent, + fixed, + ), + ) + } + + if (childrenData) { + rows.push( + ...renderRows( + childrenData, + nextIndent, + nextAncestorKeys, + ) + ) + } + }, + }, + + render () { + const { data, childrenColumnName, $scopedSlots, $listeners } = this + const props = getOptionProps(this) + const needIndentSpaced = data.some(record => record[childrenColumnName]) + + return $scopedSlots.default && $scopedSlots.default({ + props, + on: $listeners, + needIndentSpaced, + renderRows: this.renderRows, + handleExpandChange: this.handleExpandChange, + renderExpandIndentCell: this.renderExpandIndentCell, + }) + }, +} + +export default connect()(ExpandableTable) diff --git a/components/vc-table/src/HeadTable.jsx b/components/vc-table/src/HeadTable.jsx new file mode 100644 index 000000000..99c6468f0 --- /dev/null +++ b/components/vc-table/src/HeadTable.jsx @@ -0,0 +1,59 @@ +import PropTypes from '../../_util/vue-types' +import { measureScrollbar } from './utils' +import BaseTable from './BaseTable' + +export default { + name: 'HeadTable', + props: { + fixed: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + columns: PropTypes.array.isRequired, + tableClassName: PropTypes.string.isRequired, + handleBodyScrollLeft: PropTypes.func.isRequired, + expander: PropTypes.object.isRequired, + }, + inject: { + table: { default: {}}, + }, + render () { + const { columns, fixed, tableClassName, handleBodyScrollLeft, expander, table } = this + const { prefixCls, scroll, showHeader } = table + let { useFixedHeader } = table + const headStyle = {} + + if (scroll.y) { + useFixedHeader = true + // Add negative margin bottom for scroll bar overflow bug + const scrollbarWidth = measureScrollbar('horizontal') + if (scrollbarWidth > 0 && !fixed) { + headStyle.marginBottom = `-${scrollbarWidth}px` + headStyle.paddingBottom = '0px' + } + } + + if (!useFixedHeader || !showHeader) { + return null + } + return ( +
+ +
+ ) + }, + +} diff --git a/components/vc-table/src/Table.jsx b/components/vc-table/src/Table.jsx new file mode 100644 index 000000000..84217873a --- /dev/null +++ b/components/vc-table/src/Table.jsx @@ -0,0 +1,507 @@ + +import PropTypes from '../../_util/vue-types' +import { debounce, warningOnce } from './utils' +import shallowequal from 'shallowequal' +import addEventListener from '../../_util/Dom/addEventListener' +import { Provider, create } from '../../_util/store' +import merge from 'lodash/merge' +import ColumnManager from './ColumnManager' +import classes from 'component-classes' +import HeadTable from './HeadTable' +import BodyTable from './BodyTable' +import ExpandableTable from './ExpandableTable' +import { initDefaultProps, getOptionProps } from '../../_util/props-util' +import BaseMixin from '../../_util/BaseMixin' + +export default { + name: 'Table', + mixins: [BaseMixin], + props: initDefaultProps({ + data: PropTypes.array, + useFixedHeader: PropTypes.bool, + columns: PropTypes.array, + prefixCls: PropTypes.string, + bodyStyle: PropTypes.object, + rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + // onRow: PropTypes.func, + // onHeaderRow: PropTypes.func, + // onRowClick: PropTypes.func, + // onRowDoubleClick: PropTypes.func, + // onRowContextMenu: PropTypes.func, + // onRowMouseEnter: PropTypes.func, + // onRowMouseLeave: PropTypes.func, + showHeader: PropTypes.bool, + title: PropTypes.func, + id: PropTypes.string, + footer: PropTypes.func, + emptyText: PropTypes.any, + scroll: PropTypes.object, + rowRef: PropTypes.func, + getBodyWrapper: PropTypes.func, + components: PropTypes.shape({ + table: PropTypes.any, + header: PropTypes.shape({ + wrapper: PropTypes.any, + row: PropTypes.any, + cell: PropTypes.any, + }), + body: PropTypes.shape({ + wrapper: PropTypes.any, + row: PropTypes.any, + cell: PropTypes.any, + }), + }), + expandIconAsCell: PropTypes.bool, + expandedRowKeys: PropTypes.array, + expandedRowClassName: PropTypes.func, + defaultExpandAllRows: PropTypes.bool, + defaultExpandedRowKeys: PropTypes.array, + expandIconColumnIndex: PropTypes.number, + expandedRowRender: PropTypes.func, + childrenColumnName: PropTypes.string, + indentSize: PropTypes.number, + }, { + data: [], + useFixedHeader: false, + rowKey: 'key', + rowClassName: () => '', + prefixCls: 'rc-table', + bodyStyle: {}, + showHeader: true, + scroll: {}, + rowRef: () => null, + emptyText: () => 'No Data', + }), + + // static childContextTypes = { + // table: PropTypes.any, + // components: PropTypes.any, + // }, + + created () { + [ + 'rowClick', + 'rowDoubleclick', + 'rowContextmenu', + 'rowMouseenter', + 'rowMouseleave', + ].forEach(name => { + warningOnce( + this.$listeners[name] === undefined, + `${name} is deprecated, please use onRow instead.`, + ) + }) + + warningOnce( + this.getBodyWrapper === undefined, + 'getBodyWrapper is deprecated, please use custom components instead.', + ) + + // this.columnManager = new ColumnManager(this.columns, this.$slots.default) + + this.store = create({ + currentHoverKey: null, + fixedColumnsHeadRowsHeight: [], + fixedColumnsBodyRowsHeight: [], + }) + + this.setScrollPosition('left') + + this.debouncedWindowResize = debounce(this.handleWindowResize, 150) + }, + data () { + this.preData = [...this.data] + return { + columnManager: new ColumnManager(this.columns, this.$slots.default), + sComponents: merge({ + table: 'table', + header: { + wrapper: 'thead', + row: 'tr', + cell: 'th', + }, + body: { + wrapper: 'tbody', + row: 'tr', + cell: 'td', + }, + }, this.components), + } + }, + provide () { + return { + table: this, + } + }, + watch: { + components (val) { + this._components = merge({ + table: 'table', + header: { + wrapper: 'thead', + row: 'tr', + cell: 'th', + }, + body: { + wrapper: 'tbody', + row: 'tr', + cell: 'td', + }, + }, this.components) + }, + columns (val) { + if (val) { + this.columnManager.reset(val) + } + }, + data (val) { + if (val.length === 0 && this.hasScrollX()) { + this.$nextTick(() => { + this.resetScrollX() + }) + } + }, + }, + + mounted () { + this.$nextTick(() => { + if (this.columnManager.isAnyColumnsFixed()) { + this.handleWindowResize() + this.resizeEvent = addEventListener( + window, 'resize', this.debouncedWindowResize + ) + } + }) + }, + + componentWillReceiveProps (nextProps) { + if (nextProps.columns && nextProps.columns !== this.props.columns) { + this.columnManager.reset(nextProps.columns) + } else if (nextProps.children !== this.props.children) { + this.columnManager.reset(null, nextProps.children) + } + }, + + updated (prevProps) { + if (this.columnManager.isAnyColumnsFixed()) { + this.handleWindowResize() + if (!this.resizeEvent) { + this.resizeEvent = addEventListener( + window, 'resize', this.debouncedWindowResize + ) + } + } + }, + + beforeDestroy () { + if (this.resizeEvent) { + this.resizeEvent.remove() + } + if (this.debouncedWindowResize) { + this.debouncedWindowResize.cancel() + } + }, + methods: { + getRowKey (record, index) { + const rowKey = this.rowKey + const key = (typeof rowKey === 'function') + ? rowKey(record, index) : record[rowKey] + warningOnce( + key !== undefined, + 'Each record in table should have a unique `key` prop,' + + 'or set `rowKey` to an unique primary key.' + ) + return key === undefined ? index : key + }, + + setScrollPosition (position) { + this.scrollPosition = position + if (this.tableNode) { + const { prefixCls } = this + if (position === 'both') { + classes(this.tableNode) + .remove(new RegExp(`^${prefixCls}-scroll-position-.+$`)) + .add(`${prefixCls}-scroll-position-left`) + .add(`${prefixCls}-scroll-position-right`) + } else { + classes(this.tableNode) + .remove(new RegExp(`^${prefixCls}-scroll-position-.+$`)) + .add(`${prefixCls}-scroll-position-${position}`) + } + } + }, + + setScrollPositionClassName () { + const node = this.bodyTable + const scrollToLeft = node.scrollLeft === 0 + const scrollToRight = node.scrollLeft + 1 >= + node.children[0].getBoundingClientRect().width - + node.getBoundingClientRect().width + if (scrollToLeft && scrollToRight) { + this.setScrollPosition('both') + } else if (scrollToLeft) { + this.setScrollPosition('left') + } else if (scrollToRight) { + this.setScrollPosition('right') + } else if (this.scrollPosition !== 'middle') { + this.setScrollPosition('middle') + } + }, + + handleWindowResize () { + this.syncFixedTableRowHeight() + this.setScrollPositionClassName() + }, + + syncFixedTableRowHeight () { + const tableRect = this.tableNode.getBoundingClientRect() + // If tableNode's height less than 0, suppose it is hidden and don't recalculate rowHeight. + // see: https://github.com/ant-design/ant-design/issues/4836 + if (tableRect.height !== undefined && tableRect.height <= 0) { + return + } + const { prefixCls } = this.props + const headRows = this.headTable + ? this.headTable.querySelectorAll('thead') + : this.bodyTable.querySelectorAll('thead') + const bodyRows = this.bodyTable.querySelectorAll(`.${prefixCls}-row`) || [] + const fixedColumnsHeadRowsHeight = [].map.call( + headRows, row => row.getBoundingClientRect().height || 'auto' + ) + const fixedColumnsBodyRowsHeight = [].map.call( + bodyRows, row => row.getBoundingClientRect().height || 'auto' + ) + const state = this.store.getState() + if (shallowequal(state.fixedColumnsHeadRowsHeight, fixedColumnsHeadRowsHeight) && + shallowequal(state.fixedColumnsBodyRowsHeight, fixedColumnsBodyRowsHeight)) { + return + } + + this.store.setState({ + fixedColumnsHeadRowsHeight, + fixedColumnsBodyRowsHeight, + }) + }, + + resetScrollX () { + if (this.headTable) { + this.headTable.scrollLeft = 0 + } + if (this.bodyTable) { + this.bodyTable.scrollLeft = 0 + } + }, + + hasScrollX () { + const { scroll = {}} = this + return 'x' in scroll + }, + + handleBodyScrollLeft (e) { + // Fix https://github.com/ant-design/ant-design/issues/7635 + if (e.currentTarget !== e.target) { + return + } + const target = e.target + const { scroll = {}} = this + const { headTable, bodyTable } = this + if (target.scrollLeft !== this.lastScrollLeft && scroll.x) { + if (target === bodyTable && headTable) { + headTable.scrollLeft = target.scrollLeft + } else if (target === headTable && bodyTable) { + bodyTable.scrollLeft = target.scrollLeft + } + this.setScrollPositionClassName() + } + // Remember last scrollLeft for scroll direction detecting. + this.lastScrollLeft = target.scrollLeft + }, + + handleBodyScrollTop (e) { + const target = e.target + const { scroll = {}} = this + const { headTable, bodyTable, fixedColumnsBodyLeft, fixedColumnsBodyRight } = this + if (target.scrollTop !== this.lastScrollTop && scroll.y && target !== headTable) { + const scrollTop = target.scrollTop + if (fixedColumnsBodyLeft && target !== fixedColumnsBodyLeft) { + fixedColumnsBodyLeft.scrollTop = scrollTop + } + if (fixedColumnsBodyRight && target !== fixedColumnsBodyRight) { + fixedColumnsBodyRight.scrollTop = scrollTop + } + if (bodyTable && target !== bodyTable) { + bodyTable.scrollTop = scrollTop + } + } + // Remember last scrollTop for scroll direction detecting. + this.lastScrollTop = target.scrollTop + }, + + handleBodyScroll (e) { + this.handleBodyScrollLeft(e) + this.handleBodyScrollTop(e) + }, + + renderMainTable () { + const { scroll, prefixCls } = this + const isAnyColumnsFixed = this.columnManager.isAnyColumnsFixed() + const scrollable = isAnyColumnsFixed || scroll.x || scroll.y + + const table = [ + this.renderTable({ + columns: this.columnManager.groupedColumns(), + isAnyColumnsFixed, + }), + this.renderEmptyText(), + this.renderFooter(), + ] + + return scrollable ? ( +
{table}
+ ) : table + }, + + renderLeftFixedTable () { + const { prefixCls } = this + + return ( +
+ {this.renderTable({ + columns: this.columnManager.leftColumns(), + fixed: 'left', + })} +
+ ) + }, + renderRightFixedTable () { + const { prefixCls } = this + + return ( +
+ {this.renderTable({ + columns: this.columnManager.rightColumns(), + fixed: 'right', + })} +
+ ) + }, + + renderTable (options) { + const { columns, fixed, isAnyColumnsFixed } = options + const { prefixCls, scroll = {}} = this + const tableClassName = (scroll.x || fixed) ? `${prefixCls}-fixed` : '' + + const headTable = ( + + ) + + const bodyTable = ( + + ) + + return [headTable, bodyTable] + }, + + renderTitle () { + const { title, prefixCls } = this + return title ? ( +
+ {title(this.props.data)} +
+ ) : null + }, + + renderFooter () { + const { footer, prefixCls } = this + return footer ? ( +
+ {footer(this.props.data)} +
+ ) : null + }, + + renderEmptyText () { + const { emptyText, prefixCls, data } = this + if (data.length) { + return null + } + const emptyClassName = `${prefixCls}-placeholder` + return ( +
+ {(typeof emptyText === 'function') ? emptyText() : emptyText} +
+ ) + }, + }, + + render () { + const props = getOptionProps(this) + const { $listeners, columnManager, getRowKey } = this + const prefixCls = props.prefixCls + + let className = props.prefixCls + if (props.useFixedHeader || (props.scroll && props.scroll.y)) { + className += ` ${prefixCls}-fixed-header` + } + if (this.scrollPosition === 'both') { + className += ` ${prefixCls}-scroll-position-left ${prefixCls}-scroll-position-right` + } else { + className += ` ${prefixCls}-scroll-position-${this.scrollPosition}` + } + const hasLeftFixed = columnManager.isAnyColumnsLeftFixed() + const hasRightFixed = columnManager.isAnyColumnsRightFixed() + + const expandableTableProps = { + props: { + ...props, + columnManager, + getRowKey, + }, + on: { ...$listeners }, + scopedSlots: { + default: (expander) => { + this.expander = expander + return ( +
+ {this.renderTitle()} +
+ {this.renderMainTable()} + {hasLeftFixed && this.renderLeftFixedTable()} + {hasRightFixed && this.renderRightFixedTable()} +
+
+ ) + }, + }, + } + return ( + + + + ) + }, +} diff --git a/components/vc-table/src/TableCell.jsx b/components/vc-table/src/TableCell.jsx new file mode 100644 index 000000000..8ae309633 --- /dev/null +++ b/components/vc-table/src/TableCell.jsx @@ -0,0 +1,110 @@ +import PropTypes from '../../_util/vue-types' +import get from 'lodash/get' +import { isValidElement } from '../../_util/props-util' + +export default { + name: 'TableCell', + props: { + record: PropTypes.object, + prefixCls: PropTypes.string, + index: PropTypes.number, + indent: PropTypes.number, + indentSize: PropTypes.number, + column: PropTypes.object, + expandIcon: PropTypes.any, + component: PropTypes.any, + }, + methods: { + isInvalidRenderCellText (text) { + // debugger + return text && !isValidElement(text) && + Object.prototype.toString.call(text) === '[object Object]' + }, + + handleClick (e) { + const { record, column: { onCellClick }} = this + if (onCellClick) { + onCellClick(record, e) + } + }, + }, + + render (h) { + const { + record, + indentSize, + prefixCls, + indent, + index, + expandIcon, + column, + component: BodyCell, + } = this + const { dataIndex, render, className = '' } = column + const cls = column.class || className + // We should return undefined if no dataIndex is specified, but in order to + // be compatible with object-path's behavior, we return the record object instead. + let text + if (typeof dataIndex === 'number') { + text = get(record, dataIndex) + } else if (!dataIndex || dataIndex.length === 0) { + text = record + } else { + text = get(record, dataIndex) + } + const tdProps = { + props: {}, + attrs: {}, + class: cls, + on: { + click: this.handleClick, + }, + } + let colSpan + let rowSpan + + if (render) { + text = render(h, text, record, index) + if (this.isInvalidRenderCellText(text)) { + tdProps.attrs = text.attrs || text.props || {} + colSpan = tdProps.attrs.colSpan + rowSpan = tdProps.attrs.rowSpan + text = text.children + } + } + + if (column.onCell) { + tdProps.attrs = { ...tdProps.attrs, ...column.onCell(record) } + } + + // Fix https://github.com/ant-design/ant-design/issues/1202 + if (this.isInvalidRenderCellText(text)) { + text = null + } + + const indentText = expandIcon ? ( + + ) : null + + if (rowSpan === 0 || colSpan === 0) { + return null + } + + if (column.align) { + tdProps.style = { textAlign: column.align } + } + + return ( + + {indentText} + {expandIcon} + {text} + + ) + }, +} diff --git a/components/vc-table/src/TableHeader.jsx b/components/vc-table/src/TableHeader.jsx new file mode 100644 index 000000000..9e6271b8e --- /dev/null +++ b/components/vc-table/src/TableHeader.jsx @@ -0,0 +1,88 @@ +import PropTypes from '../../_util/vue-types' +import TableHeaderRow from './TableHeaderRow' + +function getHeaderRows (columns, currentRow = 0, rows) { + rows = rows || [] + rows[currentRow] = rows[currentRow] || [] + + columns.forEach(column => { + if (column.rowSpan && rows.length < column.rowSpan) { + while (rows.length < column.rowSpan) { + rows.push([]) + } + } + const cell = { + key: column.key, + className: column.className || '', + children: column.title, + column, + } + if (column.children) { + getHeaderRows(column.children, currentRow + 1, rows) + } + if ('colSpan' in column) { + cell.colSpan = column.colSpan + } + if ('rowSpan' in column) { + cell.rowSpan = column.rowSpan + } + if (cell.colSpan !== 0) { + rows[currentRow].push(cell) + } + }) + return rows.filter(row => row.length > 0) +} + +export default { + name: 'TableHeader', + props: { + fixed: PropTypes.string, + columns: PropTypes.array.isRequired, + expander: PropTypes.object.isRequired, + + }, + inject: { + table: { default: {}}, + }, + methods: { + onHeaderRow () { + this.table.__emit('headerRow', ...arguments) + }, + }, + + render () { + const { sComponents: components, prefixCls, showHeader } = this.table + const { expander, columns, fixed, onHeaderRow } = this + + if (!showHeader) { + return null + } + + const rows = getHeaderRows(columns) + + expander.renderExpandIndentCell(rows, fixed) + + const HeaderWrapper = components.header.wrapper + + return ( + + { + rows.map((row, index) => ( + + )) + } + + ) + }, + +} + diff --git a/components/vc-table/src/TableHeaderRow.jsx b/components/vc-table/src/TableHeaderRow.jsx new file mode 100644 index 000000000..e87532b51 --- /dev/null +++ b/components/vc-table/src/TableHeaderRow.jsx @@ -0,0 +1,78 @@ +import PropTypes from '../../_util/vue-types' +import { connect } from '../../_util/store' +import { mergeProps } from '../../_util/props-util' + +const TableHeaderRow = { + props: { + index: PropTypes.number, + fixed: PropTypes.string, + columns: PropTypes.array, + rows: PropTypes.array, + row: PropTypes.array, + components: PropTypes.object, + height: PropTypes.any, + }, + name: 'TableHeaderRow', + render () { + const { row, index, height, components, $listeners = {}} = this + const onHeaderRow = $listeners.headerRow + const HeaderRow = components.header.row + const HeaderCell = components.header.cell + const rowProps = onHeaderRow(row.map(cell => cell.column), index) + const customStyle = rowProps ? rowProps.style : {} + const style = { height, ...customStyle } + + return ( + + {row.map((cell, i) => { + const { column, children, className, ...cellProps } = cell + const cls = cell.class || className + const customProps = column.onHeaderCell ? column.onHeaderCell(column) : {} + if (column.align) { + cellProps.style = { textAlign: column.align } + } + const headerCellProps = mergeProps({ + attrs: { + ...cellProps, + }, + class: cls, + }, { + ...customProps, + key: column.key || column.dataIndex || i, + }) + return ( + + {children} + + ) + })} + + ) + }, +} + +function getRowHeight (state, props) { + const { fixedColumnsHeadRowsHeight } = state + const { columns, rows, fixed } = props + const headerHeight = fixedColumnsHeadRowsHeight[0] + + if (!fixed) { + return null + } + + if (headerHeight && columns) { + if (headerHeight === 'auto') { + return 'auto' + } + return `${headerHeight / rows.length}px` + } + return null +} + +export default connect((state, props) => { + return { + height: getRowHeight(state, props), + } +})(TableHeaderRow) diff --git a/components/vc-table/src/TableRow.jsx b/components/vc-table/src/TableRow.jsx new file mode 100644 index 000000000..a64570267 --- /dev/null +++ b/components/vc-table/src/TableRow.jsx @@ -0,0 +1,298 @@ +import PropTypes from '../../_util/vue-types' +import { connect } from '../../_util/store' +import TableCell from './TableCell' +import { warningOnce } from './utils' +import { initDefaultProps, mergeProps } from '../../_util/props-util' +import BaseMixin from '../../_util/BaseMixin' +function noop () {} +const TableRow = { + name: 'TableRow', + mixins: [BaseMixin], + props: initDefaultProps({ + // onRow: PropTypes.func, + // onRowClick: PropTypes.func, + // onRowDoubleClick: PropTypes.func, + // onRowContextMenu: PropTypes.func, + // onRowMouseEnter: PropTypes.func, + // onRowMouseLeave: PropTypes.func, + record: PropTypes.object, + prefixCls: PropTypes.string, + // onHover: PropTypes.func, + columns: PropTypes.array, + height: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + index: PropTypes.number, + rowKey: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + className: PropTypes.string, + indent: PropTypes.number, + indentSize: PropTypes.number, + hasExpandIcon: PropTypes.func.isRequired, + hovered: PropTypes.bool.isRequired, + visible: PropTypes.bool.isRequired, + store: PropTypes.object.isRequired, + fixed: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + renderExpandIcon: PropTypes.func, + renderExpandIconCell: PropTypes.func, + components: PropTypes.any, + expandedRow: PropTypes.bool, + isAnyColumnsFixed: PropTypes.bool, + ancestorKeys: PropTypes.array.isRequired, + expandIconColumnIndex: PropTypes.number, + expandRowByClick: PropTypes.bool, + // visible: PropTypes.bool, + // hovered: PropTypes.bool, + // height: PropTypes.any, + }, { + expandIconColumnIndex: 0, + expandRowByClick: false, + hasExpandIcon () {}, + renderExpandIcon () {}, + renderExpandIconCell () {}, + }), + + data () { + this.shouldRender = this.visible + return {} + }, + + mounted () { + if (this.shouldRender) { + this.$nextTick(() => { + this.saveRowRef() + }) + } + }, + watch: { + visible (val) { + if (val) { + this.shouldRender = true + } + }, + }, + + componentWillReceiveProps (nextProps) { + if (this.props.visible || (!this.props.visible && nextProps.visible)) { + this.shouldRender = true + } + }, + + shouldComponentUpdate (nextProps) { + return !!(this.props.visible || nextProps.visible) + }, + + updated () { + if (this.shouldRender && !this.rowRef) { + this.$nextTick(() => { + this.saveRowRef() + }) + } + }, + methods: { + onRowClick (event) { + const { record, index } = this + this.__emit('rowClick', record, index, event) + }, + + onRowDoubleClick (event) { + const { record, index } = this + this.__emit('rowDoubleClick', record, index, event) + }, + + onContextMenu (event) { + const { record, index } = this + this.__emit('rowContextmenu', record, index, event) + }, + + onMouseEnter (event) { + const { record, index, rowKey } = this + this.__emit('hover', true, rowKey) + this.__emit('rowMouseenter', record, index, event) + }, + + onMouseLeave (event) { + const { record, index, rowKey } = this + this.__emit('hover', false, rowKey) + this.__emit('rowMouseleave', record, index, event) + }, + + setExpanedRowHeight () { + const { store, rowKey } = this + let { expandedRowsHeight } = store.getState() + const height = this.rowRef.getBoundingClientRect().height + expandedRowsHeight = { + ...expandedRowsHeight, + [rowKey]: height, + } + store.setState({ expandedRowsHeight }) + }, + + setRowHeight () { + const { store, index } = this + const fixedColumnsBodyRowsHeight = store.getState().fixedColumnsBodyRowsHeight.slice() + const height = this.rowRef.getBoundingClientRect().height + fixedColumnsBodyRowsHeight[index] = height + store.setState({ fixedColumnsBodyRowsHeight }) + }, + + getStyle () { + const { height, visible } = this + + if (height && height !== this.style.height) { + this.style = { ...this.style, height } + } + + if (!visible && !this.style.display) { + this.style = { ...this.style, display: 'none' } + } + + return this.style + }, + + saveRowRef () { + this.rowRef = this.$el + + const { isAnyColumnsFixed, fixed, expandedRow, ancestorKeys } = this + + if (!isAnyColumnsFixed) { + return + } + + if (!fixed && expandedRow) { + this.setExpanedRowHeight() + } + + if (!fixed && ancestorKeys.length >= 0) { + this.setRowHeight() + } + }, + }, + + render () { + if (!this.shouldRender) { + return null + } + + const { + prefixCls, + columns, + record, + index, + // onRow, + indent, + indentSize, + hovered, + height, + visible, + components, + hasExpandIcon, + renderExpandIcon, + renderExpandIconCell, + $listeners, + } = this + const { row: onRow = noop } = $listeners + const BodyRow = components.body.row + const BodyCell = components.body.cell + + let className = '' + + if (hovered) { + className += ` ${prefixCls}-hover` + } + + const cells = [] + + renderExpandIconCell(cells) + + for (let i = 0; i < columns.length; i++) { + const column = columns[i] + + warningOnce( + column.onCellClick === undefined, + 'column[onCellClick] is deprecated, please use column[onCell] instead.', + ) + + cells.push( + + ) + } + + const rowClassName = + `${prefixCls} ${className} ${prefixCls}-level-${indent}`.trim() + + const rowProps = onRow(record, index) + const customStyle = rowProps ? rowProps.style : {} + let style = { height } + + if (!visible) { + style.display = 'none' + } + + style = { ...style, ...customStyle } + const bodyRowProps = mergeProps({ + on: { + click: this.onRowClick, + dblclick: this.onRowDoubleClick, + mouseenter: this.onMouseEnter, + mouseleave: this.onMouseLeave, + contextmenu: this.onContextMenu, + }, + class: rowClassName, + }, { ...rowProps, style }) + return ( + + {cells} + + ) + }, +} + +function getRowHeight (state, props) { + const { expandedRowsHeight, fixedColumnsBodyRowsHeight } = state + const { fixed, index, rowKey } = props + + if (!fixed) { + return null + } + + if (expandedRowsHeight[rowKey]) { + return expandedRowsHeight[rowKey] + } + + if (fixedColumnsBodyRowsHeight[index]) { + return fixedColumnsBodyRowsHeight[index] + } + + return null +} + +export default connect((state, props) => { + const { currentHoverKey, expandedRowKeys } = state + const { rowKey, ancestorKeys } = props + const visible = ancestorKeys.length === 0 || ancestorKeys.every(k => ~expandedRowKeys.indexOf(k)) + + return ({ + visible, + hovered: currentHoverKey === rowKey, + height: getRowHeight(state, props), + }) +})(TableRow) diff --git a/components/vc-table/src/utils.js b/components/vc-table/src/utils.js new file mode 100644 index 000000000..5d3ef7c37 --- /dev/null +++ b/components/vc-table/src/utils.js @@ -0,0 +1,84 @@ +import warning from 'warning' + +let scrollbarSize + +// Measure scrollbar width for padding body during modal show/hide +const scrollbarMeasure = { + position: 'absolute', + top: '-9999px', + width: '50px', + height: '50px', + overflow: 'scroll', +} + +export function measureScrollbar (direction = 'vertical') { + if (typeof document === 'undefined' || typeof window === 'undefined') { + return 0 + } + if (scrollbarSize) { + return scrollbarSize + } + const scrollDiv = document.createElement('div') + for (const scrollProp in scrollbarMeasure) { + if (scrollbarMeasure.hasOwnProperty(scrollProp)) { + scrollDiv.style[scrollProp] = scrollbarMeasure[scrollProp] + } + } + document.body.appendChild(scrollDiv) + let size = 0 + if (direction === 'vertical') { + size = scrollDiv.offsetWidth - scrollDiv.clientWidth + } else if (direction === 'horizontal') { + size = scrollDiv.offsetHeight - scrollDiv.clientHeight + } + + document.body.removeChild(scrollDiv) + scrollbarSize = size + return scrollbarSize +} + +export function debounce (func, wait, immediate) { + let timeout + function debounceFunc () { + const context = this + const args = arguments + // https://fb.me/react-event-pooling + if (args[0] && args[0].persist) { + args[0].persist() + } + const later = () => { + timeout = null + if (!immediate) { + func.apply(context, args) + } + } + const callNow = immediate && !timeout + clearTimeout(timeout) + timeout = setTimeout(later, wait) + if (callNow) { + func.apply(context, args) + } + } + debounceFunc.cancel = function cancel () { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + } + return debounceFunc +} + +const warned = {} +export function warningOnce (condition, format, args) { + if (!warned[format]) { + warning(condition, format, args) + warned[format] = !condition + } +} + +export function remove (array, item) { + const index = array.indexOf(item) + const front = array.slice(0, index) + const last = array.slice(index + 1, array.length) + return front.concat(last) +} diff --git a/components/vc-tree/assets/icons.png b/components/vc-tree/assets/icons.png new file mode 100644 index 000000000..ffda01ef1 Binary files /dev/null and b/components/vc-tree/assets/icons.png differ diff --git a/components/vc-tree/assets/index.less b/components/vc-tree/assets/index.less new file mode 100644 index 000000000..cde71e949 --- /dev/null +++ b/components/vc-tree/assets/index.less @@ -0,0 +1,192 @@ +@treePrefixCls: rc-tree; +.@{treePrefixCls} { + margin: 0; + padding: 5px; + li { + padding: 0; + margin: 0; + list-style: none; + white-space: nowrap; + outline: 0; + .draggable { + color: #333; + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; + /* Required to make elements draggable in old WebKit */ + -khtml-user-drag: element; + -webkit-user-drag: element; + } + &.drag-over { + > .draggable { + background-color: #316ac5; + color: white; + border: 1px #316ac5 solid; + opacity: 0.8; + } + } + &.drag-over-gap-top { + > .draggable { + border-top: 2px blue solid; + } + } + &.drag-over-gap-bottom { + > .draggable { + border-bottom: 2px blue solid; + } + } + &.filter-node { + > .@{treePrefixCls}-node-content-wrapper { + color: #a60000!important; + font-weight: bold!important; + } + } + ul { + margin: 0; + padding: 0 0 0 18px; + } + .@{treePrefixCls}-node-content-wrapper { + display: inline-block; + padding: 1px 3px 0 0; + margin: 0; + cursor: pointer; + height: 17px; + text-decoration: none; + vertical-align: top; + } + span { + &.@{treePrefixCls}-switcher, + &.@{treePrefixCls}-checkbox, + &.@{treePrefixCls}-iconEle { + line-height: 16px; + margin-right: 2px; + width: 16px; + height: 16px; + display: inline-block; + vertical-align: middle; + border: 0 none; + cursor: pointer; + outline: none; + background-color: transparent; + background-repeat: no-repeat; + background-attachment: scroll; + background-image: url('data:image/png;base64,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'); + + &.@{treePrefixCls}-icon__customize { + background-image: none; + } + } + &.@{treePrefixCls}-icon_loading { + margin-right: 2px; + vertical-align: top; + background: url('data:image/gif;base64,R0lGODlhEAAQAKIGAMLY8YSx5HOm4Mjc88/g9Ofw+v///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAGACwAAAAAEAAQAAADMGi6RbUwGjKIXCAA016PgRBElAVlG/RdLOO0X9nK61W39qvqiwz5Ls/rRqrggsdkAgAh+QQFCgAGACwCAAAABwAFAAADD2hqELAmiFBIYY4MAutdCQAh+QQFCgAGACwGAAAABwAFAAADD1hU1kaDOKMYCGAGEeYFCQAh+QQFCgAGACwKAAIABQAHAAADEFhUZjSkKdZqBQG0IELDQAIAIfkEBQoABgAsCgAGAAUABwAAAxBoVlRKgyjmlAIBqCDCzUoCACH5BAUKAAYALAYACgAHAAUAAAMPaGpFtYYMAgJgLogA610JACH5BAUKAAYALAIACgAHAAUAAAMPCAHWFiI4o1ghZZJB5i0JACH5BAUKAAYALAAABgAFAAcAAAMQCAFmIaEp1motpDQySMNFAgA7') no-repeat scroll 0 0 transparent; + } + &.@{treePrefixCls}-switcher { + &.@{treePrefixCls}-switcher-noop { + cursor: auto; + } + &.@{treePrefixCls}-switcher_open { + background-position: -93px -56px; + } + &.@{treePrefixCls}-switcher_close { + background-position: -75px -56px; + } + } + &.@{treePrefixCls}-checkbox { + width: 13px; + height: 13px; + margin: 0 3px; + background-position: 0 0; + &-checked { + background-position: -14px 0; + } + &-indeterminate { + background-position: -14px -28px; + } + &-disabled { + background-position: 0 -56px; + } + &.@{treePrefixCls}-checkbox-checked.@{treePrefixCls}-checkbox-disabled { + background-position: -14px -56px; + } + &.@{treePrefixCls}-checkbox-indeterminate.@{treePrefixCls}-checkbox-disabled { + position: relative; + background: #ccc; + border-radius: 3px; + &::after { + content: ' '; + -webkit-transform: scale(1); + transform: scale(1); + position: absolute; + left: 3px; + top: 5px; + width: 5px; + height: 0; + border: 2px solid #fff; + border-top: 0; + border-left: 0; + } + } + } + } + } + &:not(.@{treePrefixCls}-show-line) { + .@{treePrefixCls}-switcher-noop { + background: none; + } + } + &.@{treePrefixCls}-show-line { + li:not(:last-child) { + > ul { + background: url('data:image/gif;base64,R0lGODlhCQACAIAAAMzMzP///yH5BAEAAAEALAAAAAAJAAIAAAIEjI9pUAA7') 0 0 repeat-y; + } + > .@{treePrefixCls}-switcher-noop { + background-position: -56px -18px; + } + } + li:last-child { + > .@{treePrefixCls}-switcher-noop { + background-position: -56px -36px; + } + } + } + &-child-tree { + display: none; + &-open { + display: block; + } + } + &-treenode-disabled { + >span:not(.@{treePrefixCls}-switcher), + >a, + >a span { + color: #ccc; + cursor: not-allowed; + } + } + &-node-selected { + background-color: #ffe6b0; + border: 1px #ffb951 solid; + opacity: 0.8; + } + &-icon__open { + margin-right: 2px; + background-position: -110px -16px; + vertical-align: top; + } + &-icon__close { + margin-right: 2px; + background-position: -110px 0; + vertical-align: top; + } + &-icon__docu { + margin-right: 2px; + background-position: -110px -32px; + vertical-align: top; + } + &-icon__customize { + margin-right: 2px; + vertical-align: top; + } +} diff --git a/components/vc-tree/assets/line.gif b/components/vc-tree/assets/line.gif new file mode 100644 index 000000000..d561d36a9 Binary files /dev/null and b/components/vc-tree/assets/line.gif differ diff --git a/components/vc-tree/assets/loading.gif b/components/vc-tree/assets/loading.gif new file mode 100644 index 000000000..e8c289293 Binary files /dev/null and b/components/vc-tree/assets/loading.gif differ diff --git a/components/vc-tree/demo/basic.jsx b/components/vc-tree/demo/basic.jsx new file mode 100644 index 000000000..322d26d96 --- /dev/null +++ b/components/vc-tree/demo/basic.jsx @@ -0,0 +1,110 @@ +/* eslint no-console:0 */ +/* eslint no-alert:0 */ +import PropTypes from '../../_util/vue-types' +import Tree, { TreeNode } from '../index' +import '../assets/index.less' +import './basic.less' + +export default { + props: { + keys: PropTypes.array.def(['0-0-0-0']), + }, + data () { + const keys = this.keys + return { + defaultExpandedKeys: keys, + defaultSelectedKeys: keys, + defaultCheckedKeys: keys, + switchIt: true, + showMore: false, + } + }, + methods: { + onExpand (expandedKeys) { + console.log('onExpand', expandedKeys, arguments) + }, + onSelect (selectedKeys, info) { + console.log('selected', selectedKeys, info) + this.selKey = info.node.$options.propsData.eventKey + }, + onCheck (checkedKeys, info) { + console.log('onCheck', checkedKeys, info) + }, + onEdit () { + setTimeout(() => { + console.log('current key: ', this.selKey) + }, 0) + }, + onDel (e) { + if (!window.confirm('sure to delete?')) { + return + } + e.stopPropagation() + }, + toggleChildren () { + this.showMore = !this.showMore + }, + }, + + render () { + const customLabel = ( + operations: + Edit  +   + Delete + ) + return (
+

simple

+ {/* + + + + + + + + + + + + + + + */} + +

Check on Click TreeNode

+ + + + + + + + {this.showMore ? + + + : null} + + +
) + }, +} + diff --git a/components/vc-tree/demo/basic.less b/components/vc-tree/demo/basic.less new file mode 100644 index 000000000..cca0a1860 --- /dev/null +++ b/components/vc-tree/demo/basic.less @@ -0,0 +1,6 @@ +.rc-tree li a.rc-tree-node-selected{ + .cus-label { + background-color: white; + border: none; + } +} diff --git a/components/vc-tree/index.js b/components/vc-tree/index.js new file mode 100644 index 000000000..8f31b413f --- /dev/null +++ b/components/vc-tree/index.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports = require('./src/') diff --git a/components/vc-tree/src/Tree.jsx b/components/vc-tree/src/Tree.jsx new file mode 100644 index 000000000..6c042a530 --- /dev/null +++ b/components/vc-tree/src/Tree.jsx @@ -0,0 +1,602 @@ +import PropTypes from '../../_util/vue-types' +import classNames from 'classnames' +import warning from 'warning' +import { initDefaultProps, getOptionProps } from '../../_util/props-util' +import { cloneElement } from '../../_util/vnode' +import BaseMixin from '../../_util/BaseMixin' +import { + traverseTreeNodes, getStrictlyValue, + getFullKeyList, getPosition, getDragNodesKeys, + calcExpandedKeys, calcSelectedKeys, + calcCheckedKeys, calcDropPosition, + arrAdd, arrDel, posToArr, +} from './util' + +/** + * Thought we still use `cloneElement` to pass `key`, + * other props can pass with context for future refactor. + */ +export const contextTypes = { + rcTree: PropTypes.shape({ + root: PropTypes.object, + + prefixCls: PropTypes.string, + selectable: PropTypes.bool, + showIcon: PropTypes.bool, + draggable: PropTypes.bool, + checkable: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.object, + ]), + checkStrictly: PropTypes.bool, + disabled: PropTypes.bool, + openTransitionName: PropTypes.string, + openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + + loadData: PropTypes.func, + filterTreeNode: PropTypes.func, + renderTreeNode: PropTypes.func, + + isKeyChecked: PropTypes.func, + + // onNodeExpand: PropTypes.func, + // onNodeSelect: PropTypes.func, + // onNodeMouseEnter: PropTypes.func, + // onNodeMouseLeave: PropTypes.func, + // onNodeContextMenu: PropTypes.func, + // onNodeDragStart: PropTypes.func, + // onNodeDragEnter: PropTypes.func, + // onNodeDragOver: PropTypes.func, + // onNodeDragLeave: PropTypes.func, + // onNodeDragEnd: PropTypes.func, + // onNodeDrop: PropTypes.func, + // onBatchNodeCheck: PropTypes.func, + // onCheckConductFinished: PropTypes.func, + }), +} + +const Tree = { + name: 'Tree', + mixins: [BaseMixin], + props: initDefaultProps({ + prefixCls: PropTypes.string, + showLine: PropTypes.bool, + showIcon: PropTypes.bool, + focusable: PropTypes.bool, + selectable: PropTypes.bool, + disabled: PropTypes.bool, + multiple: PropTypes.bool, + checkable: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.node, + ]), + checkStrictly: PropTypes.bool, + draggable: PropTypes.bool, + autoExpandParent: PropTypes.bool, + defaultExpandAll: PropTypes.bool, + defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string), + expandedKeys: PropTypes.arrayOf(PropTypes.string), + defaultCheckedKeys: PropTypes.arrayOf(PropTypes.string), + checkedKeys: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string), + PropTypes.object, + ]), + defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string), + selectedKeys: PropTypes.arrayOf(PropTypes.string), + // onExpand: PropTypes.func, + // onCheck: PropTypes.func, + // onSelect: PropTypes.func, + loadData: PropTypes.func, + // onMouseEnter: PropTypes.func, + // onMouseLeave: PropTypes.func, + // onRightClick: PropTypes.func, + // onDragStart: PropTypes.func, + // onDragEnter: PropTypes.func, + // onDragOver: PropTypes.func, + // onDragLeave: PropTypes.func, + // onDragEnd: PropTypes.func, + // onDrop: PropTypes.func, + filterTreeNode: PropTypes.func, + openTransitionName: PropTypes.string, + openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + children: PropTypes.any, + }, { + prefixCls: 'rc-tree', + showLine: false, + showIcon: true, + selectable: true, + multiple: false, + checkable: false, + disabled: false, + checkStrictly: false, + draggable: false, + autoExpandParent: true, + defaultExpandAll: false, + defaultExpandedKeys: [], + defaultCheckedKeys: [], + defaultSelectedKeys: [], + }), + + // static childContextTypes = contextTypes; + + data () { + const props = getOptionProps(this) + const { + defaultExpandAll, + defaultExpandedKeys, + defaultCheckedKeys, + defaultSelectedKeys, + } = props + const children = this.$slots.default + // Sync state with props + const { checkedKeys = [], halfCheckedKeys = [] } = + calcCheckedKeys(defaultCheckedKeys, props, children) || {} + + // Cache for check status to optimize + this.checkedBatch = null + this.propsToStateMap = { + expandedKeys: 'sExpandedKeys', + selectedKeys: 'sSelectedKeys', + checkedKeys: 'sCheckedKeys', + halfCheckedKeys: 'sHalfCheckedKeys', + } + return { + sExpandedKeys: defaultExpandAll + ? getFullKeyList(children) + : calcExpandedKeys(defaultExpandedKeys, props, children), + sSelectedKeys: calcSelectedKeys(defaultSelectedKeys, props, children), + sCheckedKeys: checkedKeys, + sHalfCheckedKeys: halfCheckedKeys, + + ...(this.getSyncProps(props) || {}), + dragOverNodeKey: '', + dropPosition: null, + } + }, + provide () { + return { + vcTree: this, + } + }, + + watch: { + children (val) { + const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(this.checkedKeys || this.sCheckedKeys, this.$props, this.$slots.default) || {} + this.sCheckedKeys = checkedKeys + this.sHalfCheckedKeys = halfCheckedKeys + }, + expandedKeys (val) { + this.sExpandedKeys = calcExpandedKeys(this.expandedKeys, this.$props, this.$slots.default) + }, + selectedKeys (val) { + this.sSelectedKeys = calcSelectedKeys(this.selectedKeys, this.$props, this.$slots.default) + }, + checkedKeys (val) { + const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(this.checkedKeys, this.$props, this.$slots.default) || {} + this.sCheckedKeys = checkedKeys + this.sHalfCheckedKeys = halfCheckedKeys + }, + }, + + // componentWillReceiveProps (nextProps) { + // // React 16 will not trigger update if new state is null + // this.setState(this.getSyncProps(nextProps, this.props)) + // }, + + methods: { + onNodeDragStart (event, node) { + const { sExpandedKeys } = this + const { eventKey, children } = node.props + + this.dragNode = node + + this.setState({ + dragNodesKeys: getDragNodesKeys(children, node), + sExpandedKeys: arrDel(sExpandedKeys, eventKey), + }) + this.__emit('dragstart', { event, node }) + }, + + /** + * [Legacy] Select handler is less small than node, + * so that this will trigger when drag enter node or select handler. + * This is a little tricky if customize css without padding. + * Better for use mouse move event to refresh drag state. + * But let's just keep it to avoid event trigger logic change. + */ + onNodeDragEnter (event, node) { + const { sExpandedKeys } = this + const { pos, eventKey } = node.props + + const dropPosition = calcDropPosition(event, node) + + // Skip if drag node is self + if ( + this.dragNode.props.eventKey === eventKey && + dropPosition === 0 + ) { + this.setState({ + dragOverNodeKey: '', + dropPosition: null, + }) + return + } + + // Ref: https://github.com/react-component/tree/issues/132 + // Add timeout to let onDragLevel fire before onDragEnter, + // so that we can clean drag props for onDragLeave node. + // Macro task for this: + // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script + setTimeout(() => { + // Update drag over node + this.setState({ + dragOverNodeKey: eventKey, + dropPosition, + }) + + // Side effect for delay drag + if (!this.delayedDragEnterLogic) { + this.delayedDragEnterLogic = {} + } + Object.keys(this.delayedDragEnterLogic).forEach((key) => { + clearTimeout(this.delayedDragEnterLogic[key]) + }) + this.delayedDragEnterLogic[pos] = setTimeout(() => { + const newExpandedKeys = arrAdd(sExpandedKeys, eventKey) + this.setState({ + sExpandedKeys: newExpandedKeys, + }) + this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys }) + }, 400) + }, 0) + }, + onNodeDragOver (event, node) { + this.__emit('dragover', { event, node }) + }, + onNodeDragLeave (event, node) { + this.setState({ + dragOverNodeKey: '', + }) + this.__emit('dragleave', { event, node }) + }, + onNodeDragEnd (event, node) { + this.setState({ + dragOverNodeKey: '', + }) + this.__emit('dragend', { event, node }) + }, + onNodeDrop (event, node) { + const { dragNodesKeys, dropPosition } = this + + const { eventKey, pos } = node.props + + this.setState({ + dragOverNodeKey: '', + dropNodeKey: eventKey, + }) + + if (dragNodesKeys.indexOf(eventKey) !== -1) { + warning(false, 'Can not drop to dragNode(include it\'s children node)') + return + } + + const posArr = posToArr(pos) + + const dropResult = { + event, + node, + dragNode: this.dragNode, + dragNodesKeys: dragNodesKeys.slice(), + dropPosition: dropPosition + Number(posArr[posArr.length - 1]), + } + + if (dropPosition !== 0) { + dropResult.dropToGap = true + } + this.__emit('drop', dropResult) + }, + + onNodeSelect (e, treeNode) { + const { sSelectedKeys, multiple, $slots: { default: children }} = this + const { selected, eventKey } = getOptionProps(treeNode) + const targetSelected = !selected + let selectedKeys = sSelectedKeys + // Update selected keys + if (!targetSelected) { + selectedKeys = arrDel(selectedKeys, eventKey) + } else if (!multiple) { + selectedKeys = [eventKey] + } else { + selectedKeys = arrAdd(selectedKeys, eventKey) + } + + // [Legacy] Not found related usage in doc or upper libs + // [Legacy] TODO: add optimize prop to skip node process + const selectedNodes = [] + if (selectedKeys.length) { + traverseTreeNodes(children, ({ node, key }) => { + if (selectedKeys.indexOf(key) !== -1) { + selectedNodes.push(node) + } + }) + } + + this.setUncontrolledState({ selectedKeys }) + + const eventObj = { + event: 'select', + selected: targetSelected, + node: treeNode, + selectedNodes, + } + this.__emit('select', selectedKeys, eventObj) + }, + + /** + * This will cache node check status to optimize update process. + * When Tree get trigger `onCheckConductFinished` will flush all the update. + */ + onBatchNodeCheck (key, checked, halfChecked, startNode) { + if (startNode) { + this.checkedBatch = { + treeNode: startNode, + checked, + list: [], + } + } + + // This code should never called + if (!this.checkedBatch) { + this.checkedBatch = { + list: [], + } + warning( + false, + 'Checked batch not init. This should be a bug. Please fire a issue.' + ) + } + + this.checkedBatch.list.push({ key, checked, halfChecked }) + }, + + /** + * When top `onCheckConductFinished` called, will execute all batch update. + * And trigger `onCheck` event. + */ + onCheckConductFinished () { + const { sCheckedKeys, sHalfCheckedKeys, checkStrictly, $slots: { default: children }} = this + + // Use map to optimize update speed + const checkedKeySet = {} + const halfCheckedKeySet = {} + + sCheckedKeys.forEach(key => { + checkedKeySet[key] = true + }) + sHalfCheckedKeys.forEach(key => { + halfCheckedKeySet[key] = true + }) + + // Batch process + this.checkedBatch.list.forEach(({ key, checked, halfChecked }) => { + checkedKeySet[key] = checked + halfCheckedKeySet[key] = halfChecked + }) + const newCheckedKeys = Object.keys(checkedKeySet).filter(key => checkedKeySet[key]) + const newHalfCheckedKeys = Object.keys(halfCheckedKeySet).filter(key => halfCheckedKeySet[key]) + + // Trigger onChecked + let selectedObj + + const eventObj = { + event: 'check', + node: this.checkedBatch.treeNode, + checked: this.checkedBatch.checked, + } + + if (checkStrictly) { + selectedObj = getStrictlyValue(newCheckedKeys, newHalfCheckedKeys) + + // [Legacy] TODO: add optimize prop to skip node process + eventObj.checkedNodes = [] + traverseTreeNodes(children, ({ node, key }) => { + if (checkedKeySet[key]) { + eventObj.checkedNodes.push(node) + } + }) + + this.setUncontrolledState({ checkedKeys: newCheckedKeys }) + } else { + selectedObj = newCheckedKeys + + // [Legacy] TODO: add optimize prop to skip node process + eventObj.checkedNodes = [] + eventObj.checkedNodesPositions = [] // [Legacy] TODO: not in API + eventObj.halfCheckedKeys = newHalfCheckedKeys // [Legacy] TODO: not in API + traverseTreeNodes(children, ({ node, pos, key }) => { + if (checkedKeySet[key]) { + eventObj.checkedNodes.push(node) + eventObj.checkedNodesPositions.push({ node, pos }) + } + }) + + this.setUncontrolledState({ + checkedKeys: newCheckedKeys, + halfCheckedKeys: newHalfCheckedKeys, + }) + } + this.__emit('check', selectedObj, eventObj) + + // Clean up + this.checkedBatch = null + }, + + onNodeExpand (e, treeNode) { + const { sExpandedKeys, loadData } = this + let expandedKeys = [...sExpandedKeys] + const { eventKey, expanded } = getOptionProps(treeNode) + + // Update selected keys + const index = expandedKeys.indexOf(eventKey) + const targetExpanded = !expanded + + warning( + (expanded && index !== -1) || (!expanded && index === -1) + , 'Expand state not sync with index check') + + if (targetExpanded) { + expandedKeys = arrAdd(expandedKeys, eventKey) + } else { + expandedKeys = arrDel(expandedKeys, eventKey) + } + + this.setUncontrolledState({ expandedKeys }) + this.__emit('expand', expandedKeys, { node: treeNode, expanded: targetExpanded }) + + // Async Load data + if (targetExpanded && loadData) { + return loadData(treeNode).then(() => { + // [Legacy] Refresh logic + this.setUncontrolledState({ expandedKeys }) + }) + } + + return null + }, + + onNodeMouseEnter (event, node) { + this.__emit('mouseenter', { event, node }) + }, + + onNodeMouseLeave (event, node) { + this.__emit('mouseleave', { event, node }) + }, + + onNodeContextMenu (event, node) { + event.preventDefault() + this.__emit('rightClick', { event, node }) + }, + + /** + * Sync state with props if needed + */ + getSyncProps (props = {}, prevProps) { + let needSync = false + const newState = {} + const myPrevProps = prevProps || {} + const children = this.$slots.default + function checkSync (name) { + if (props[name] !== myPrevProps[name]) { + needSync = true + return true + } + return false + } + + // Children change will affect check box status. + // And no need to check when prev props not provided + if (prevProps && checkSync('children')) { + const { checkedKeys = [], halfCheckedKeys = [] } = + calcCheckedKeys(props.checkedKeys || this.sCheckedKeys, props, children) || {} + newState.sCheckedKeys = checkedKeys + newState.sHalfCheckedKeys = halfCheckedKeys + } + + if (checkSync('expandedKeys')) { + newState.sExpandedKeys = calcExpandedKeys(props.expandedKeys, props, children) + } + + if (checkSync('selectedKeys')) { + newState.sSelectedKeys = calcSelectedKeys(props.selectedKeys, props, children) + } + + if (checkSync('checkedKeys')) { + const { checkedKeys = [], halfCheckedKeys = [] } = + calcCheckedKeys(props.checkedKeys, props, children) || {} + newState.sCheckedKeys = checkedKeys + newState.sHalfCheckedKeys = halfCheckedKeys + } + + return needSync ? newState : null + }, + + /** + * Only update the value which is not in props + */ + setUncontrolledState (state) { + let needSync = false + const newState = {} + const props = getOptionProps(this) + Object.keys(state).forEach(name => { + if (name in props) return + + needSync = true + const key = this.propsToStateMap[name] + newState[key] = state[name] + }) + + this.setState(needSync ? newState : null) + }, + + isKeyChecked (key) { + const { sCheckedKeys = [] } = this + return sCheckedKeys.indexOf(key) !== -1 + }, + + /** + * [Legacy] Original logic use `key` as tracking clue. + * We have to use `cloneElement` to pass `key`. + */ + renderTreeNode (child, index, level = 0) { + const { + sExpandedKeys = [], sSelectedKeys = [], sHalfCheckedKeys = [], + dragOverNodeKey, dropPosition, + } = this + const pos = getPosition(level, index) + const key = child.key || pos + + return cloneElement(child, { + props: { + eventKey: key, + expanded: sExpandedKeys.indexOf(key) !== -1, + selected: sSelectedKeys.indexOf(key) !== -1, + checked: this.isKeyChecked(key), + halfChecked: sHalfCheckedKeys.indexOf(key) !== -1, + pos, + + // [Legacy] Drag props + dragOver: dragOverNodeKey === key && dropPosition === 0, + dragOverGapTop: dragOverNodeKey === key && dropPosition === -1, + dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1, + }, + + }) + }, + }, + + render () { + const { + prefixCls, focusable, + showLine, + $slots: { default: children = [] }, + } = this + const domProps = {} + + return ( +
    {}} + > + {children.map(this.renderTreeNode)} +
+ ) + }, +} + +export default Tree diff --git a/components/vc-tree/src/TreeNode.jsx b/components/vc-tree/src/TreeNode.jsx new file mode 100644 index 000000000..fb5ce8a86 --- /dev/null +++ b/components/vc-tree/src/TreeNode.jsx @@ -0,0 +1,589 @@ +import PropTypes from '../../_util/vue-types' +import classNames from 'classnames' +import warning from 'warning' +import { contextTypes } from './Tree' +import { getPosition, getNodeChildren, isCheckDisabled, traverseTreeNodes } from './util' +import { initDefaultProps, getOptionProps, filterEmpty } from '../../_util/props-util' +import BaseMixin from '../../_util/BaseMixin' +import getTransitionProps from '../../_util/getTransitionProps' + +const ICON_OPEN = 'open' +const ICON_CLOSE = 'close' + +const LOAD_STATUS_NONE = 0 +const LOAD_STATUS_LOADING = 1 +const LOAD_STATUS_LOADED = 2 +const LOAD_STATUS_FAILED = 0 // Action align, let's make failed same as init. + +const defaultTitle = '---' + +let onlyTreeNodeWarned = false // Only accept TreeNode + +export const nodeContextTypes = { + ...contextTypes, + vcTreeNode: PropTypes.shape({ + onUpCheckConduct: PropTypes.func, + }), +} + +const TreeNode = { + name: 'TreeNode', + mixins: [BaseMixin], + props: initDefaultProps({ + eventKey: PropTypes.string, // Pass by parent `cloneElement` + prefixCls: PropTypes.string, + // className: PropTypes.string, + root: PropTypes.object, + // onSelect: PropTypes.func, + + // By parent + expanded: PropTypes.bool, + selected: PropTypes.bool, + checked: PropTypes.bool, + halfChecked: PropTypes.bool, + title: PropTypes.any, + pos: PropTypes.string, + dragOver: PropTypes.bool, + dragOverGapTop: PropTypes.bool, + dragOverGapBottom: PropTypes.bool, + + // By user + isLeaf: PropTypes.bool, + selectable: PropTypes.bool, + disabled: PropTypes.bool, + disableCheckbox: PropTypes.bool, + icon: PropTypes.any, + }, { + title: defaultTitle, + }), + + data () { + return { + loadStatus: LOAD_STATUS_NONE, + dragNodeHighlight: false, + } + }, + inject: { + vcTree: { default: {}}, + }, + provide () { + return { + vcTree: this.vcTree, + vcTreeNode: this, + } + }, + + // Isomorphic needn't load data in server side + mounted () { + this.syncLoadData(this.$props) + }, + watch: { + expanded (val) { + this.syncLoadData({ expanded: val }) + }, + }, + + methods: { + onUpCheckConduct (treeNode, nodeChecked, nodeHalfChecked) { + const { pos: nodePos } = getOptionProps(treeNode) + const { eventKey, pos, checked, halfChecked } = this + const { + vcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck, onCheckConductFinished }, + vcTreeNode: { onUpCheckConduct } = {}, + } = this + + // Stop conduct when current node is disabled + if (isCheckDisabled(this)) { + onCheckConductFinished() + return + } + + const children = this.getNodeChildren() + + let checkedCount = nodeChecked ? 1 : 0 + + // Statistic checked count + children.forEach((node, index) => { + const childPos = getPosition(pos, index) + + if (nodePos === childPos || isCheckDisabled(node)) { + return + } + if (isKeyChecked(node.key || childPos)) { + checkedCount += 1 + } + }) + + // Static enabled children count + const enabledChildrenCount = children + .filter(node => !isCheckDisabled(node)) + .length + + // checkStrictly will not conduct check status + const nextChecked = checkStrictly ? checked : enabledChildrenCount === checkedCount + const nextHalfChecked = checkStrictly // propagated or child checked + ? halfChecked : (nodeHalfChecked || (checkedCount > 0 && !nextChecked)) + + // Add into batch update + if (checked !== nextChecked || halfChecked !== nextHalfChecked) { + onBatchNodeCheck(eventKey, nextChecked, nextHalfChecked) + + if (onUpCheckConduct) { + onUpCheckConduct(this, nextChecked, nextHalfChecked) + } else { + // Flush all the update + onCheckConductFinished() + } + } else { + // Flush all the update + onCheckConductFinished() + } + }, + + onDownCheckConduct (nodeChecked) { + const { $slots } = this + const children = $slots.default || [] + const { vcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck }} = this + if (checkStrictly) return + + traverseTreeNodes(children, ({ node, key }) => { + if (isCheckDisabled(node)) return false + + if (nodeChecked !== isKeyChecked(key)) { + onBatchNodeCheck(key, nodeChecked, false) + } + }) + }, + + onSelectorClick (e) { + if (this.isSelectable()) { + this.onSelect(e) + } else { + this.onCheck(e) + } + }, + + onSelect (e) { + if (this.isDisabled()) return + + const { vcTree: { onNodeSelect }} = this + e.preventDefault() + onNodeSelect(e, this) + }, + + onCheck (e) { + if (this.isDisabled()) return + + const { disableCheckbox, checked, eventKey } = this + const { + vcTree: { checkable, onBatchNodeCheck, onCheckConductFinished }, + vcTreeNode: { onUpCheckConduct } = {}, + } = this + + if (!checkable || disableCheckbox) return + + e.preventDefault() + const targetChecked = !checked + onBatchNodeCheck(eventKey, targetChecked, false, this) + + // Children conduct + this.onDownCheckConduct(targetChecked) + + // Parent conduct + if (onUpCheckConduct) { + onUpCheckConduct(this, targetChecked, false) + } else { + onCheckConductFinished() + } + }, + + onMouseEnter (e) { + const { vcTree: { onNodeMouseEnter }} = this + onNodeMouseEnter(e, this) + }, + + onMouseLeave (e) { + const { vcTree: { onNodeMouseLeave }} = this + onNodeMouseLeave(e, this) + }, + + onContextMenu (e) { + const { vcTree: { onNodeContextMenu }} = this + onNodeContextMenu(e, this) + }, + + onDragStart (e) { + const { vcTree: { onNodeDragStart }} = this + + e.stopPropagation() + this.setState({ + dragNodeHighlight: true, + }) + onNodeDragStart(e, this) + + try { + // ie throw error + // firefox-need-it + e.dataTransfer.setData('text/plain', '') + } catch (error) { + // empty + } + }, + + onDragEnter (e) { + const { vcTree: { onNodeDragEnter }} = this + + e.preventDefault() + e.stopPropagation() + onNodeDragEnter(e, this) + }, + + onDragOver (e) { + const { vcTree: { onNodeDragOver }} = this + + e.preventDefault() + e.stopPropagation() + onNodeDragOver(e, this) + }, + + onDragLeave (e) { + const { vcTree: { onNodeDragLeave }} = this + + e.stopPropagation() + onNodeDragLeave(e, this) + }, + + onDragEnd (e) { + const { vcTree: { onNodeDragEnd }} = this + + e.stopPropagation() + this.setState({ + dragNodeHighlight: false, + }) + onNodeDragEnd(e, this) + }, + + onDrop (e) { + const { vcTree: { onNodeDrop }} = this + + e.preventDefault() + e.stopPropagation() + this.setState({ + dragNodeHighlight: false, + }) + onNodeDrop(e, this) + }, + + // Disabled item still can be switch + onExpand (e) { + const { vcTree: { onNodeExpand }} = this + const callbackPromise = onNodeExpand(e, this) + + // Promise like + if (callbackPromise && callbackPromise.then) { + this.setState({ loadStatus: LOAD_STATUS_LOADING }) + + callbackPromise.then(() => { + this.setState({ loadStatus: LOAD_STATUS_LOADED }) + }).catch(() => { + this.setState({ loadStatus: LOAD_STATUS_FAILED }) + }) + } + }, + + // Drag usage + setSelectHandle (node) { + this.selectHandle = node + }, + + getNodeChildren () { + const { $slots: { default: children }} = this + const originList = filterEmpty(children) + const targetList = getNodeChildren(originList) + + if (originList.length !== targetList.length && !onlyTreeNodeWarned) { + onlyTreeNodeWarned = true + warning(false, 'Tree only accept TreeNode as children.') + } + + return targetList + }, + + getNodeState () { + const { expanded } = this + + if (this.isLeaf2()) { + return null + } + + return expanded ? ICON_OPEN : ICON_CLOSE + }, + + isLeaf2 () { + const { isLeaf, loadStatus } = this + const { vcTree: { loadData }} = this + + const hasChildren = this.getNodeChildren().length !== 0 + + return ( + isLeaf || + (!loadData && !hasChildren) || + (loadData && loadStatus === LOAD_STATUS_LOADED && !hasChildren) + ) + }, + + isDisabled () { + const { disabled } = this + const { vcTree: { disabled: treeDisabled }} = this + + // Follow the logic of Selectable + if (disabled === false) { + return false + } + + return !!(treeDisabled || disabled) + }, + + isSelectable () { + const { selectable } = this + const { vcTree: { selectable: treeSelectable }} = this + + // Ignore when selectable is undefined or null + if (typeof selectable === 'boolean') { + return selectable + } + + return treeSelectable + }, + + // Load data to avoid default expanded tree without data + syncLoadData (props) { + const { loadStatus } = this + const { expanded } = props + const { vcTree: { loadData }} = this + + if (loadData && loadStatus === LOAD_STATUS_NONE && expanded && !this.isLeaf2()) { + this.setState({ loadStatus: LOAD_STATUS_LOADING }) + + loadData(this).then(() => { + this.setState({ loadStatus: LOAD_STATUS_LOADED }) + }).catch(() => { + this.setState({ loadStatus: LOAD_STATUS_FAILED }) + }) + } + }, + + // Switcher + renderSwitcher () { + const { expanded } = this + const { vcTree: { prefixCls }} = this + + if (this.isLeaf2()) { + return + } + + return ( + + ) + }, + + // Checkbox + renderCheckbox () { + const { checked, halfChecked, disableCheckbox } = this + const { vcTree: { prefixCls, checkable }} = this + const disabled = this.isDisabled() + + if (!checkable) return null + + // [Legacy] Custom element should be separate with `checkable` in future + const $custom = typeof checkable !== 'boolean' ? checkable : null + + return ( + + {$custom} + + ) + }, + + renderIcon () { + const { loadStatus } = this + const { vcTree: { prefixCls }} = this + + return ( + + ) + }, + + // Icon + Title + renderSelector () { + const { title, selected, icon, loadStatus, dragNodeHighlight } = this + const { vcTree: { prefixCls, showIcon, draggable, loadData }} = this + const disabled = this.isDisabled() + + const wrapClass = `${prefixCls}-node-content-wrapper` + + // Icon - Still show loading icon when loading without showIcon + let $icon + + if (showIcon) { + $icon = icon ? ( + + {typeof icon === 'function' + ? icon(this.$props) : icon} + + ) : this.renderIcon() + } else if (loadData && loadStatus === LOAD_STATUS_LOADING) { + $icon = this.renderIcon() + } + + // Title + const $title = {title} + + return ( + + {$icon}{$title} + + ) + }, + + // Children list wrapped with `Animation` + renderChildren () { + const { expanded, pos } = this + const { vcTree: { + prefixCls, + openTransitionName, openAnimation, + renderTreeNode, + }} = this + + // [Legacy] Animation control + const renderFirst = this.renderFirst + this.renderFirst = 1 + let transitionAppear = true + if (!renderFirst && expanded) { + transitionAppear = false + } + + let animProps = {} + if (openTransitionName) { + animProps = getTransitionProps(openTransitionName, { appear: transitionAppear }) + } else if (typeof openAnimation === 'object') { + animProps = { ...openAnimation } + if (!transitionAppear) { + delete animProps.props.appear + } + } + + // Children TreeNode + const nodeList = this.getNodeChildren() + + if (nodeList.length === 0) { + return null + } + + let $children + if (expanded) { + $children = ( +
    + {nodeList.map((node, index) => ( + renderTreeNode(node, index, pos) + ))} +
+ ) + } + + return ( + + {$children} + + ) + }, + }, + + render () { + const { + dragOver, dragOverGapTop, dragOverGapBottom, + } = this + const { vcTree: { + prefixCls, + filterTreeNode, + }} = this + const disabled = this.isDisabled() + + return ( +
  • + {this.renderSwitcher()} + {this.renderCheckbox()} + {this.renderSelector()} + {this.renderChildren()} +
  • + ) + }, +} + +TreeNode.isTreeNode = 1 + +export default TreeNode diff --git a/components/vc-tree/src/index.js b/components/vc-tree/src/index.js new file mode 100644 index 000000000..e8c0e81b5 --- /dev/null +++ b/components/vc-tree/src/index.js @@ -0,0 +1,25 @@ +import { getOptionProps } from '../../_util/props-util' +import Tree from './Tree' +import TreeNode from './TreeNode' +Tree.TreeNode = TreeNode + +// +const NewTree = { + TreeNode: TreeNode, + props: Tree.props, + render () { + const { $listeners, $slots } = this + const treeProps = { + props: { + ...getOptionProps(this), + children: $slots.default, + }, + on: $listeners, + } + return ( + {$slots.default} + ) + }, +} +export { TreeNode } +export default NewTree diff --git a/components/vc-tree/src/util.js b/components/vc-tree/src/util.js new file mode 100644 index 000000000..712853249 --- /dev/null +++ b/components/vc-tree/src/util.js @@ -0,0 +1,398 @@ +/* eslint no-loop-func: 0*/ +import warning from 'warning' +import { getSlotOptions, getOptionProps } from '../../_util/props-util' + +export function arrDel (list, value) { + const clone = list.slice() + const index = clone.indexOf(value) + if (index >= 0) { + clone.splice(index, 1) + } + return clone +} + +export function arrAdd (list, value) { + const clone = list.slice() + if (clone.indexOf(value) === -1) { + clone.push(value) + } + return clone +} + +export function posToArr (pos) { + return pos.split('-') +} + +// Only used when drag, not affect SSR. +export function getOffset (ele) { + if (!ele.getClientRects().length) { + return { top: 0, left: 0 } + } + + const rect = ele.getBoundingClientRect() + if (rect.width || rect.height) { + const doc = ele.ownerDocument + const win = doc.defaultView + const docElem = doc.documentElement + + return { + top: rect.top + win.pageYOffset - docElem.clientTop, + left: rect.left + win.pageXOffset - docElem.clientLeft, + } + } + + return rect +} + +export function getPosition (level, index) { + return `${level}-${index}` +} + +export function getNodeChildren (children = []) { + return children + .filter(child => getSlotOptions(child).isTreeNode) +} + +export function isCheckDisabled (node) { + const { disabled, disableCheckbox } = getOptionProps(node) || {} + return !!(disabled || disableCheckbox) +} + +export function traverseTreeNodes (treeNodes, subTreeData, callback) { + if (typeof subTreeData === 'function') { + callback = subTreeData + subTreeData = false + } + + function processNode (node, index, parent) { + const children = node ? node.componentOptions.children : treeNodes + const pos = node ? getPosition(parent.pos, index) : 0 + + // Filter children + const childList = getNodeChildren(children) + + // Process node if is not root + if (node) { + const data = { + node, + index, + pos, + key: node.key || pos, + parentPos: parent.node ? parent.pos : null, + } + + // Children data is not must have + if (subTreeData) { + // Statistic children + const subNodes = [] + childList.forEach((subNode, subIndex) => { + // Provide limit snapshot + const subPos = getPosition(pos, index) + subNodes.push({ + node: subNode, + key: subNode.key || subPos, + pos: subPos, + index: subIndex, + }) + }) + data.subNodes = subNodes + } + + // Can break traverse by return false + if (callback(data) === false) { + return + } + } + + // Process children node + childList.forEach((subNode, subIndex) => { + processNode(subNode, subIndex, { node, pos }) + }) + } + + processNode(null) +} + +/** + * [Legacy] Return halfChecked when it has value. + * @param checkedKeys + * @param halfChecked + * @returns {*} + */ +export function getStrictlyValue (checkedKeys, halfChecked) { + if (halfChecked) { + return { checked: checkedKeys, halfChecked } + } + return checkedKeys +} + +export function getFullKeyList (treeNodes) { + const keyList = [] + traverseTreeNodes(treeNodes, ({ key }) => { + keyList.push(key) + }) + return keyList +} + +/** + * Check position relation. + * @param parentPos + * @param childPos + * @param directly only directly parent can be true + * @returns {boolean} + */ +export function isParent (parentPos, childPos, directly = false) { + if (!parentPos || !childPos || parentPos.length > childPos.length) return false + + const parentPath = posToArr(parentPos) + const childPath = posToArr(childPos) + + // Directly check + if (directly && parentPath.length !== childPath.length - 1) return false + + const len = parentPath.length + for (let i = 0; i < len; i += 1) { + if (parentPath[i] !== childPath[i]) return false + } + + return true +} + +/** + * Statistic TreeNodes info + * @param treeNodes + * @returns {{}} + */ +export function getNodesStatistic (treeNodes) { + const statistic = { + keyNodes: {}, + posNodes: {}, + nodeList: [], + } + + traverseTreeNodes(treeNodes, true, ({ node, index, pos, key, subNodes, parentPos }) => { + const data = { node, index, pos, key, subNodes, parentPos } + statistic.keyNodes[key] = data + statistic.posNodes[pos] = data + statistic.nodeList.push(data) + }) + + return statistic +} + +export function getDragNodesKeys (treeNodes, node) { + const { eventKey, pos } = getOptionProps(node) + const dragNodesKeys = [] + + traverseTreeNodes(treeNodes, ({ pos: nodePos, key }) => { + if (isParent(pos, nodePos)) { + dragNodesKeys.push(key) + } + }) + dragNodesKeys.push(eventKey || pos) + return dragNodesKeys +} + +export function calcDropPosition (event, treeNode) { + const offsetTop = getOffset(treeNode.selectHandle).top + const offsetHeight = treeNode.selectHandle.offsetHeight + const pageY = event.pageY + const gapHeight = 2 // [Legacy] TODO: remove hard code + if (pageY > offsetTop + offsetHeight - gapHeight) { + return 1 + } + if (pageY < offsetTop + gapHeight) { + return -1 + } + return 0 +} + +/** + * Auto expand all related node when sub node is expanded + * @param keyList + * @param props + * @returns [string] + */ +export function calcExpandedKeys (keyList, props, children = []) { + if (!keyList) { + return [] + } + + const { autoExpandParent } = props + + // Do nothing if not auto expand parent + if (!autoExpandParent) { + return keyList + } + + // Fill parent expanded keys + const { keyNodes, nodeList } = getNodesStatistic(children) + const needExpandKeys = {} + const needExpandPathList = [] + + // Fill expanded nodes + keyList.forEach((key) => { + const node = keyNodes[key] + if (node) { + needExpandKeys[key] = true + needExpandPathList.push(node.pos) + } + }) + + // Match parent by path + nodeList.forEach(({ pos, key }) => { + if (needExpandPathList.some(childPos => isParent(pos, childPos))) { + needExpandKeys[key] = true + } + }) + + const calcExpandedKeyList = Object.keys(needExpandKeys) + + // [Legacy] Return origin keyList if calc list is empty + return calcExpandedKeyList.length ? calcExpandedKeyList : keyList +} + +/** + * Return selectedKeys according with multiple prop + * @param selectedKeys + * @param props + * @returns [string] + */ +export function calcSelectedKeys (selectedKeys, props) { + if (!selectedKeys) { + return undefined + } + + const { multiple } = props + if (multiple) { + return selectedKeys.slice() + } + + if (selectedKeys.length) { + return [selectedKeys[0]] + } + return selectedKeys +} + +/** + * Check conduct is by key level. It pass though up & down. + * When conduct target node is check means already conducted will be skip. + * @param treeNodes + * @param checkedKeys + * @returns {{checkedKeys: Array, halfCheckedKeys: Array}} + */ +export function calcCheckStateConduct (treeNodes, checkedKeys) { + const { keyNodes, posNodes } = getNodesStatistic(treeNodes) + + const tgtCheckedKeys = {} + const tgtHalfCheckedKeys = {} + + // Conduct up + function conductUp (key, halfChecked) { + if (tgtCheckedKeys[key]) return + + const { subNodes = [], parentPos, node } = keyNodes[key] + if (isCheckDisabled(node)) return + + const allSubChecked = !halfChecked && subNodes + .filter(sub => !isCheckDisabled(sub.node)) + .every(sub => tgtCheckedKeys[sub.key]) + + if (allSubChecked) { + tgtCheckedKeys[key] = true + } else { + tgtHalfCheckedKeys[key] = true + } + + if (parentPos !== null) { + conductUp(posNodes[parentPos].key, !allSubChecked) + } + } + + // Conduct down + function conductDown (key) { + if (tgtCheckedKeys[key]) return + const { subNodes = [], node } = keyNodes[key] + + if (isCheckDisabled(node)) return + + tgtCheckedKeys[key] = true + + subNodes.forEach((sub) => { + conductDown(sub.key) + }) + } + + function conduct (key) { + if (!keyNodes[key]) { + warning(false, `'${key}' does not exist in the tree.`) + return + } + + const { subNodes = [], parentPos, node } = keyNodes[key] + if (isCheckDisabled(node)) return + + tgtCheckedKeys[key] = true + + // Conduct down + subNodes + .filter(sub => !isCheckDisabled(sub.node)) + .forEach((sub) => { + conductDown(sub.key) + }) + + // Conduct up + if (parentPos !== null) { + conductUp(posNodes[parentPos].key) + } + } + + checkedKeys.forEach((key) => { + conduct(key) + }) + + return { + checkedKeys: Object.keys(tgtCheckedKeys), + halfCheckedKeys: Object.keys(tgtHalfCheckedKeys) + .filter(key => !tgtCheckedKeys[key]), + } +} + +/** + * Calculate the value of checked and halfChecked keys. + * This should be only run in init or props changed. + */ +export function calcCheckedKeys (keys, props, children = []) { + const { checkable, checkStrictly } = props + + if (!checkable || !keys) { + return null + } + + // Convert keys to object format + let keyProps + if (Array.isArray(keys)) { + // [Legacy] Follow the api doc + keyProps = { + checkedKeys: keys, + halfCheckedKeys: undefined, + } + } else if (typeof keys === 'object') { + keyProps = { + checkedKeys: keys.checked || undefined, + halfCheckedKeys: keys.halfChecked || undefined, + } + } else { + warning(false, '`CheckedKeys` is not an array or an object') + return null + } + + // Do nothing if is checkStrictly mode + if (checkStrictly) { + return keyProps + } + + // Conduct calculate the check status + const { checkedKeys = [] } = keyProps + return calcCheckStateConduct(children, checkedKeys) +} diff --git a/package-lock.json b/package-lock.json index be35897c4..b2cdd9e77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vue-antd-ui", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -206,7 +206,7 @@ }, "abab": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "resolved": "http://registry.npm.taobao.org/abab/download/abab-1.0.4.tgz", "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", "dev": true, "optional": true @@ -251,7 +251,7 @@ }, "acorn-globals": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "resolved": "http://registry.npm.taobao.org/acorn-globals/download/acorn-globals-1.0.9.tgz", "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", "dev": true, "optional": true, @@ -261,7 +261,7 @@ "dependencies": { "acorn": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-2.7.0.tgz", "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", "dev": true, "optional": true @@ -2491,7 +2491,7 @@ }, "component-classes": { "version": "1.2.6", - "resolved": "http://registry.npm.taobao.org/component-classes/download/component-classes-1.2.6.tgz", + "resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz", "integrity": "sha1-xkI5TDYYpNiwuJGe/Mu9kw5c1pE=", "requires": { "component-indexof": "0.0.3" @@ -3086,13 +3086,13 @@ }, "cssom": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "resolved": "http://registry.npm.taobao.org/cssom/download/cssom-0.3.2.tgz", "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", "dev": true }, "cssstyle": { "version": "0.2.37", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "resolved": "http://registry.npm.taobao.org/cssstyle/download/cssstyle-0.2.37.tgz", "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", "dev": true, "optional": true, @@ -8076,7 +8076,7 @@ }, "jsdom": { "version": "7.2.2", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", + "resolved": "http://registry.npm.taobao.org/jsdom/download/jsdom-7.2.2.tgz", "integrity": "sha1-QLQCdwwr2iNGkJa+6Rq2deOx/G4=", "dev": true, "optional": true, @@ -8100,14 +8100,14 @@ "dependencies": { "acorn": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-2.7.0.tgz", "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", "dev": true, "optional": true }, "parse5": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "resolved": "http://registry.npm.taobao.org/parse5/download/parse5-1.5.1.tgz", "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", "dev": true, "optional": true @@ -8897,11 +8897,6 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, "lodash.create": { "version": "3.1.1", "resolved": "https://registry.npm.taobao.org/lodash.create/download/lodash.create-3.1.1.tgz", @@ -8913,11 +8908,6 @@ "lodash._isiterateecall": "3.0.9" } }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npm.taobao.org/lodash.debounce/download/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" - }, "lodash.escape": { "version": "3.2.0", "resolved": "http://registry.npm.taobao.org/lodash.escape/download/lodash.escape-3.2.0.tgz", @@ -8945,11 +8935,6 @@ "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "dev": true }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npm.taobao.org/lodash.isequal/download/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -15976,7 +15961,7 @@ }, "symbol-tree": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "resolved": "http://registry.npm.taobao.org/symbol-tree/download/symbol-tree-3.2.2.tgz", "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true, "optional": true @@ -16267,7 +16252,7 @@ }, "tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "resolved": "http://registry.npm.taobao.org/tr46/download/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true, "optional": true @@ -17031,7 +17016,7 @@ "dependencies": { "cheerio": { "version": "0.20.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.20.0.tgz", + "resolved": "http://registry.npm.taobao.org/cheerio/download/cheerio-0.20.0.tgz", "integrity": "sha1-XHEPK6uVZTJyhCugHG6mGzVF7DU=", "dev": true, "requires": { @@ -17045,7 +17030,7 @@ }, "domhandler": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "resolved": "http://registry.npm.taobao.org/domhandler/download/domhandler-2.3.0.tgz", "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", "dev": true, "requires": { @@ -17054,7 +17039,7 @@ }, "domutils": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "resolved": "http://registry.npm.taobao.org/domutils/download/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "dev": true, "requires": { @@ -17064,7 +17049,7 @@ }, "htmlparser2": { "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "resolved": "http://registry.npm.taobao.org/htmlparser2/download/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "dev": true, "requires": { @@ -17077,7 +17062,7 @@ "dependencies": { "entities": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "resolved": "http://registry.npm.taobao.org/entities/download/entities-1.0.0.tgz", "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", "dev": true } @@ -17085,13 +17070,13 @@ }, "isarray": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "resolved": "http://registry.npm.taobao.org/isarray/download/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, "loader-utils": { "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "resolved": "http://registry.npm.taobao.org/loader-utils/download/loader-utils-0.2.17.tgz", "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", "dev": true, "requires": { @@ -17103,7 +17088,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npm.taobao.org/readable-stream/download/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -17115,7 +17100,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "http://registry.npm.taobao.org/string_decoder/download/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } @@ -17319,7 +17304,7 @@ }, "webidl-conversions": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz", + "resolved": "http://registry.npm.taobao.org/webidl-conversions/download/webidl-conversions-2.0.1.tgz", "integrity": "sha1-O/glj30xjHRDw28uFpQCoaZwNQY=", "dev": true, "optional": true @@ -18038,7 +18023,7 @@ }, "whatwg-url-compat": { "version": "0.6.5", - "resolved": "https://registry.npmjs.org/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz", + "resolved": "http://registry.npm.taobao.org/whatwg-url-compat/download/whatwg-url-compat-0.6.5.tgz", "integrity": "sha1-AImBEa9om7CXVBzVpFymyHmERb8=", "dev": true, "optional": true, @@ -18168,7 +18153,7 @@ }, "xml-name-validator": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "resolved": "http://registry.npm.taobao.org/xml-name-validator/download/xml-name-validator-2.0.1.tgz", "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", "dev": true, "optional": true @@ -18250,4 +18235,4 @@ "dev": true } } -} \ No newline at end of file +}